Merge branch 'features-01f34a-rc4' into features-01f34a
# Conflicts: # viewport/src/editor.rs # viewport/src/handle.rs
This commit is contained in:
commit
d50f463ebb
|
|
@ -168,7 +168,7 @@ class IcedViewportView: NSView {
|
||||||
}
|
}
|
||||||
if cmd && shift {
|
if cmd && shift {
|
||||||
switch chars {
|
switch chars {
|
||||||
case "z":
|
case "g", "z":
|
||||||
keyDown(with: event)
|
keyDown(with: event)
|
||||||
return true
|
return true
|
||||||
default: break
|
default: break
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ use iced_widget::canvas;
|
||||||
use iced_widget::container;
|
use iced_widget::container;
|
||||||
use iced_widget::markdown;
|
use iced_widget::markdown;
|
||||||
use iced_widget::text_editor::{self, Action, Binding, Cursor, KeyPress, Motion, Position, Status, Style};
|
use iced_widget::text_editor::{self, Action, Binding, Cursor, KeyPress, Motion, Position, Status, Style};
|
||||||
|
use iced_widget::text_input;
|
||||||
use iced_wgpu::core::text::highlighter::Format;
|
use iced_wgpu::core::text::highlighter::Format;
|
||||||
|
use iced_wgpu::core::widget::Id as WidgetId;
|
||||||
use crate::palette;
|
use crate::palette;
|
||||||
use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings};
|
use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings};
|
||||||
|
|
||||||
|
|
@ -30,6 +32,16 @@ pub enum Message {
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
ZoomReset,
|
ZoomReset,
|
||||||
|
Undo,
|
||||||
|
Redo,
|
||||||
|
ToggleFind,
|
||||||
|
HideFind,
|
||||||
|
FindQueryChanged(String),
|
||||||
|
FindNext,
|
||||||
|
FindPrev,
|
||||||
|
ReplaceQueryChanged(String),
|
||||||
|
ReplaceOne,
|
||||||
|
ReplaceAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const RESULT_PREFIX: &str = "→ ";
|
pub const RESULT_PREFIX: &str = "→ ";
|
||||||
|
|
@ -37,6 +49,47 @@ pub const ERROR_PREFIX: &str = "⚠ ";
|
||||||
|
|
||||||
const EVAL_DEBOUNCE_MS: u128 = 300;
|
const EVAL_DEBOUNCE_MS: u128 = 300;
|
||||||
|
|
||||||
|
pub const FIND_INPUT_ID: &str = "find_input";
|
||||||
|
pub const REPLACE_INPUT_ID: &str = "replace_input";
|
||||||
|
const UNDO_MAX: usize = 200;
|
||||||
|
const COALESCE_MS: u128 = 500;
|
||||||
|
|
||||||
|
struct UndoSnapshot {
|
||||||
|
text: String,
|
||||||
|
cursor_line: usize,
|
||||||
|
cursor_col: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
|
enum EditKind {
|
||||||
|
Insert,
|
||||||
|
Backspace,
|
||||||
|
Delete,
|
||||||
|
Enter,
|
||||||
|
Paste,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FindState {
|
||||||
|
pub visible: bool,
|
||||||
|
pub query: String,
|
||||||
|
pub replacement: String,
|
||||||
|
pub matches: Vec<(usize, usize)>,
|
||||||
|
pub current: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FindState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
visible: false,
|
||||||
|
query: String::new(),
|
||||||
|
replacement: String::new(),
|
||||||
|
matches: Vec::new(),
|
||||||
|
current: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct EditorState {
|
pub struct EditorState {
|
||||||
pub content: text_editor::Content<iced_wgpu::Renderer>,
|
pub content: text_editor::Content<iced_wgpu::Renderer>,
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
|
|
@ -46,6 +99,14 @@ pub struct EditorState {
|
||||||
scroll_offset: f32,
|
scroll_offset: f32,
|
||||||
eval_dirty: bool,
|
eval_dirty: bool,
|
||||||
last_edit: Instant,
|
last_edit: Instant,
|
||||||
|
|
||||||
|
undo_stack: Vec<UndoSnapshot>,
|
||||||
|
redo_stack: Vec<UndoSnapshot>,
|
||||||
|
last_edit_kind: EditKind,
|
||||||
|
last_edit_time: Instant,
|
||||||
|
|
||||||
|
pub find: FindState,
|
||||||
|
pub pending_focus: Option<WidgetId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn md_style() -> markdown::Style {
|
fn md_style() -> markdown::Style {
|
||||||
|
|
@ -100,6 +161,12 @@ impl EditorState {
|
||||||
scroll_offset: 0.0,
|
scroll_offset: 0.0,
|
||||||
eval_dirty: false,
|
eval_dirty: false,
|
||||||
last_edit: Instant::now(),
|
last_edit: Instant::now(),
|
||||||
|
undo_stack: Vec::new(),
|
||||||
|
redo_stack: Vec::new(),
|
||||||
|
last_edit_kind: EditKind::Other,
|
||||||
|
last_edit_time: Instant::now(),
|
||||||
|
find: FindState::new(),
|
||||||
|
pending_focus: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,6 +315,135 @@ impl EditorState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn take_pending_focus(&mut self) -> Option<WidgetId> {
|
||||||
|
self.pending_focus.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(&self) -> UndoSnapshot {
|
||||||
|
let cursor = self.content.cursor();
|
||||||
|
UndoSnapshot {
|
||||||
|
text: self.get_clean_text(),
|
||||||
|
cursor_line: cursor.position.line,
|
||||||
|
cursor_col: cursor.position.column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_undo_snapshot(&mut self) {
|
||||||
|
let snap = self.snapshot();
|
||||||
|
self.undo_stack.push(snap);
|
||||||
|
if self.undo_stack.len() > UNDO_MAX {
|
||||||
|
self.undo_stack.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_snapshot(&mut self, kind: EditKind) {
|
||||||
|
let now = Instant::now();
|
||||||
|
let elapsed = now.duration_since(self.last_edit_time).as_millis();
|
||||||
|
let should_snap = kind != self.last_edit_kind
|
||||||
|
|| elapsed > COALESCE_MS
|
||||||
|
|| kind == EditKind::Enter
|
||||||
|
|| kind == EditKind::Paste;
|
||||||
|
|
||||||
|
if should_snap {
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_edit_kind = kind;
|
||||||
|
self.last_edit_time = now;
|
||||||
|
self.redo_stack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_edit(action: &text_editor::Action) -> Option<EditKind> {
|
||||||
|
match action {
|
||||||
|
Action::Edit(edit) => match edit {
|
||||||
|
text_editor::Edit::Insert(_) => Some(EditKind::Insert),
|
||||||
|
text_editor::Edit::Enter => Some(EditKind::Enter),
|
||||||
|
text_editor::Edit::Backspace => Some(EditKind::Backspace),
|
||||||
|
text_editor::Edit::Delete => Some(EditKind::Delete),
|
||||||
|
text_editor::Edit::Paste(_) => Some(EditKind::Paste),
|
||||||
|
_ => Some(EditKind::Other),
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_snapshot(&mut self, snap: &UndoSnapshot) {
|
||||||
|
self.set_text(&snap.text);
|
||||||
|
self.run_eval();
|
||||||
|
let text = self.content.text();
|
||||||
|
let display_line = from_clean_line(&text, snap.cursor_line);
|
||||||
|
self.content.move_to(Cursor {
|
||||||
|
position: Position { line: display_line, column: snap.cursor_col },
|
||||||
|
selection: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform_undo(&mut self) {
|
||||||
|
if self.undo_stack.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current = self.snapshot();
|
||||||
|
self.redo_stack.push(current);
|
||||||
|
let snap = self.undo_stack.pop().unwrap();
|
||||||
|
self.restore_snapshot(&snap);
|
||||||
|
self.last_edit_kind = EditKind::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform_redo(&mut self) {
|
||||||
|
if self.redo_stack.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current = self.snapshot();
|
||||||
|
self.undo_stack.push(current);
|
||||||
|
let snap = self.redo_stack.pop().unwrap();
|
||||||
|
self.restore_snapshot(&snap);
|
||||||
|
self.last_edit_kind = EditKind::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_find_matches(&mut self) {
|
||||||
|
self.find.matches.clear();
|
||||||
|
self.find.current = 0;
|
||||||
|
if self.find.query.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let text = self.get_clean_text();
|
||||||
|
let query_lower = self.find.query.to_lowercase();
|
||||||
|
let text_lower = text.to_lowercase();
|
||||||
|
|
||||||
|
let mut line = 0usize;
|
||||||
|
let mut col = 0usize;
|
||||||
|
let mut byte = 0usize;
|
||||||
|
|
||||||
|
for (i, ch) in text_lower.char_indices() {
|
||||||
|
while byte < i {
|
||||||
|
byte += 1;
|
||||||
|
}
|
||||||
|
if ch == '\n' {
|
||||||
|
line += 1;
|
||||||
|
col = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if text_lower[i..].starts_with(&query_lower) {
|
||||||
|
self.find.matches.push((line, col));
|
||||||
|
}
|
||||||
|
col += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate_to_match(&mut self) {
|
||||||
|
if self.find.matches.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let idx = self.find.current.min(self.find.matches.len() - 1);
|
||||||
|
let (line, col) = self.find.matches[idx];
|
||||||
|
let text = self.content.text();
|
||||||
|
let display_line = from_clean_line(&text, line);
|
||||||
|
self.content.move_to(Cursor {
|
||||||
|
position: Position { line: display_line, column: col },
|
||||||
|
selection: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, message: Message) {
|
pub fn update(&mut self, message: Message) {
|
||||||
match message {
|
match message {
|
||||||
Message::EditorAction(action) => {
|
Message::EditorAction(action) => {
|
||||||
|
|
@ -255,6 +451,10 @@ impl EditorState {
|
||||||
let is_enter = matches!(&action, Action::Edit(text_editor::Edit::Enter));
|
let is_enter = matches!(&action, Action::Edit(text_editor::Edit::Enter));
|
||||||
let is_paste = matches!(&action, Action::Edit(text_editor::Edit::Paste(_)));
|
let is_paste = matches!(&action, Action::Edit(text_editor::Edit::Paste(_)));
|
||||||
|
|
||||||
|
if let Some(kind) = Self::classify_edit(&action) {
|
||||||
|
self.maybe_snapshot(kind);
|
||||||
|
}
|
||||||
|
|
||||||
if let Action::Scroll { lines } = &action {
|
if let Action::Scroll { lines } = &action {
|
||||||
let lh = self.line_height();
|
let lh = self.line_height();
|
||||||
self.scroll_offset += *lines as f32 * lh;
|
self.scroll_offset += *lines as f32 * lh;
|
||||||
|
|
@ -386,6 +586,107 @@ impl EditorState {
|
||||||
Message::ZoomReset => {
|
Message::ZoomReset => {
|
||||||
self.font_size = 14.0;
|
self.font_size = 14.0;
|
||||||
}
|
}
|
||||||
|
Message::Undo => {
|
||||||
|
self.perform_undo();
|
||||||
|
}
|
||||||
|
Message::Redo => {
|
||||||
|
self.perform_redo();
|
||||||
|
}
|
||||||
|
Message::ToggleFind => {
|
||||||
|
self.find.visible = !self.find.visible;
|
||||||
|
if self.find.visible {
|
||||||
|
self.pending_focus = Some(WidgetId::new(FIND_INPUT_ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::HideFind => {
|
||||||
|
self.find.visible = false;
|
||||||
|
}
|
||||||
|
Message::FindQueryChanged(q) => {
|
||||||
|
self.find.query = q;
|
||||||
|
self.update_find_matches();
|
||||||
|
if !self.find.matches.is_empty() {
|
||||||
|
self.find.current = 0;
|
||||||
|
self.navigate_to_match();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::FindNext => {
|
||||||
|
if !self.find.matches.is_empty() {
|
||||||
|
self.find.current = (self.find.current + 1) % self.find.matches.len();
|
||||||
|
self.navigate_to_match();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::FindPrev => {
|
||||||
|
if !self.find.matches.is_empty() {
|
||||||
|
self.find.current = if self.find.current == 0 {
|
||||||
|
self.find.matches.len() - 1
|
||||||
|
} else {
|
||||||
|
self.find.current - 1
|
||||||
|
};
|
||||||
|
self.navigate_to_match();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::ReplaceQueryChanged(r) => {
|
||||||
|
self.find.replacement = r;
|
||||||
|
}
|
||||||
|
Message::ReplaceOne => {
|
||||||
|
if self.find.matches.is_empty() || self.find.query.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
self.redo_stack.clear();
|
||||||
|
|
||||||
|
let (match_line, match_col) = self.find.matches[self.find.current];
|
||||||
|
let clean = self.get_clean_text();
|
||||||
|
let mut lines: Vec<String> = clean.lines().map(|l| l.to_string()).collect();
|
||||||
|
if match_line < lines.len() {
|
||||||
|
let line = &lines[match_line];
|
||||||
|
let query_len = self.find.query.len();
|
||||||
|
let line_lower = line.to_lowercase();
|
||||||
|
let query_lower = self.find.query.to_lowercase();
|
||||||
|
if let Some(byte_start) = nth_char_byte_offset(line, match_col) {
|
||||||
|
if line_lower[byte_start..].starts_with(&query_lower) {
|
||||||
|
let before = &line[..byte_start];
|
||||||
|
let after = &line[byte_start + query_len..];
|
||||||
|
lines[match_line] = format!("{before}{}{after}", self.find.replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let new_text = lines.join("\n");
|
||||||
|
self.set_text(&new_text);
|
||||||
|
self.run_eval();
|
||||||
|
self.update_find_matches();
|
||||||
|
if !self.find.matches.is_empty() {
|
||||||
|
self.find.current = self.find.current.min(self.find.matches.len() - 1);
|
||||||
|
self.navigate_to_match();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::ReplaceAll => {
|
||||||
|
if self.find.matches.is_empty() || self.find.query.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
self.redo_stack.clear();
|
||||||
|
|
||||||
|
let clean = self.get_clean_text();
|
||||||
|
let query_lower = self.find.query.to_lowercase();
|
||||||
|
let mut result = String::with_capacity(clean.len());
|
||||||
|
let mut i = 0;
|
||||||
|
let clean_lower = clean.to_lowercase();
|
||||||
|
let qlen = self.find.query.len();
|
||||||
|
while i < clean.len() {
|
||||||
|
if clean_lower[i..].starts_with(&query_lower) {
|
||||||
|
result.push_str(&self.find.replacement);
|
||||||
|
i += qlen;
|
||||||
|
} else {
|
||||||
|
let ch = clean[i..].chars().next().unwrap();
|
||||||
|
result.push(ch);
|
||||||
|
i += ch.len_utf8();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.set_text(&result);
|
||||||
|
self.run_eval();
|
||||||
|
self.update_find_matches();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -503,15 +804,132 @@ impl EditorState {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut col_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> =
|
let mut col_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
||||||
vec![main_content];
|
|
||||||
|
|
||||||
|
if self.find.visible {
|
||||||
|
col_items.push(self.find_bar());
|
||||||
|
}
|
||||||
|
|
||||||
|
col_items.push(main_content);
|
||||||
col_items.push(status_bar.into());
|
col_items.push(status_bar.into());
|
||||||
|
|
||||||
iced_widget::column(col_items)
|
iced_widget::column(col_items)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_bar(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
||||||
|
let p = palette::current();
|
||||||
|
|
||||||
|
let search_input = text_input::TextInput::new("Find...", &self.find.query)
|
||||||
|
.on_input(Message::FindQueryChanged)
|
||||||
|
.on_submit(Message::FindNext)
|
||||||
|
.id(WidgetId::new(FIND_INPUT_ID))
|
||||||
|
.font(Font::MONOSPACE)
|
||||||
|
.size(13.0)
|
||||||
|
.padding(Padding { top: 3.0, right: 6.0, bottom: 3.0, left: 6.0 })
|
||||||
|
.width(Length::FillPortion(3))
|
||||||
|
.style(find_input_style);
|
||||||
|
|
||||||
|
let replace_input = text_input::TextInput::new("Replace...", &self.find.replacement)
|
||||||
|
.on_input(Message::ReplaceQueryChanged)
|
||||||
|
.on_submit(Message::ReplaceOne)
|
||||||
|
.id(WidgetId::new(REPLACE_INPUT_ID))
|
||||||
|
.font(Font::MONOSPACE)
|
||||||
|
.size(13.0)
|
||||||
|
.padding(Padding { top: 3.0, right: 6.0, bottom: 3.0, left: 6.0 })
|
||||||
|
.width(Length::FillPortion(3))
|
||||||
|
.style(find_input_style);
|
||||||
|
|
||||||
|
let match_label = if self.find.matches.is_empty() {
|
||||||
|
if self.find.query.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
"0/0".into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", self.find.current + 1, self.find.matches.len())
|
||||||
|
};
|
||||||
|
|
||||||
|
let label: Element<'_, Message, Theme, iced_wgpu::Renderer> =
|
||||||
|
iced_widget::text(match_label)
|
||||||
|
.font(Font::MONOSPACE)
|
||||||
|
.size(11.0)
|
||||||
|
.color(p.overlay1)
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let btn = |txt: String, msg: Message| -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
||||||
|
iced_widget::button(
|
||||||
|
iced_widget::text(txt).font(Font::MONOSPACE).size(11.0)
|
||||||
|
)
|
||||||
|
.on_press(msg)
|
||||||
|
.padding(Padding { top: 2.0, right: 6.0, bottom: 2.0, left: 6.0 })
|
||||||
|
.style(find_btn_style)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let row = iced_widget::row![
|
||||||
|
search_input,
|
||||||
|
label,
|
||||||
|
btn("Prev".into(), Message::FindPrev),
|
||||||
|
btn("Next".into(), Message::FindNext),
|
||||||
|
replace_input,
|
||||||
|
btn("Repl".into(), Message::ReplaceOne),
|
||||||
|
btn("All".into(), Message::ReplaceAll),
|
||||||
|
btn("X".into(), Message::HideFind),
|
||||||
|
]
|
||||||
|
.spacing(4.0)
|
||||||
|
.align_y(alignment::Vertical::Center);
|
||||||
|
|
||||||
|
iced_widget::container(row)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(Padding { top: 4.0, right: 8.0, bottom: 4.0, left: 8.0 })
|
||||||
|
.style(|_theme: &Theme| {
|
||||||
|
let p = palette::current();
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(p.mantle)),
|
||||||
|
border: Border::default(),
|
||||||
|
text_color: None,
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_input_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
|
||||||
|
let p = palette::current();
|
||||||
|
text_input::Style {
|
||||||
|
background: Background::Color(p.surface0),
|
||||||
|
border: Border {
|
||||||
|
color: p.surface2,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
icon: p.overlay2,
|
||||||
|
placeholder: p.overlay0,
|
||||||
|
value: p.text,
|
||||||
|
selection: Color { a: 0.4, ..p.blue },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_btn_style(
|
||||||
|
_theme: &Theme,
|
||||||
|
_status: iced_widget::button::Status,
|
||||||
|
) -> iced_widget::button::Style {
|
||||||
|
let p = palette::current();
|
||||||
|
iced_widget::button::Style {
|
||||||
|
background: Some(Background::Color(p.surface1)),
|
||||||
|
text_color: p.text,
|
||||||
|
border: Border {
|
||||||
|
color: p.surface2,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Gutter {
|
struct Gutter {
|
||||||
|
|
@ -748,6 +1166,12 @@ fn macos_key_binding(key_press: KeyPress) -> Option<Binding<Message>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
match key.as_ref() {
|
match key.as_ref() {
|
||||||
|
keyboard::Key::Character("z") if modifiers.logo() && modifiers.shift() => {
|
||||||
|
Some(Binding::Custom(Message::Redo))
|
||||||
|
}
|
||||||
|
keyboard::Key::Character("z") if modifiers.logo() => {
|
||||||
|
Some(Binding::Custom(Message::Undo))
|
||||||
|
}
|
||||||
keyboard::Key::Character("=" | "+") if modifiers.logo() => {
|
keyboard::Key::Character("=" | "+") if modifiers.logo() => {
|
||||||
Some(Binding::Custom(Message::ZoomIn))
|
Some(Binding::Custom(Message::ZoomIn))
|
||||||
}
|
}
|
||||||
|
|
@ -839,3 +1263,7 @@ fn leading_whitespace(line: &str) -> &str {
|
||||||
let end = line.len() - line.trim_start().len();
|
let end = line.len() - line.trim_start().len();
|
||||||
&line[..end]
|
&line[..end]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn nth_char_byte_offset(s: &str, char_idx: usize) -> Option<usize> {
|
||||||
|
s.char_indices().nth(char_idx).map(|(i, _)| i)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -159,22 +159,35 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
let mut messages: Vec<Message> = Vec::new();
|
let mut messages: Vec<Message> = Vec::new();
|
||||||
|
|
||||||
for event in &handle.events {
|
for event in &handle.events {
|
||||||
if let Event::Keyboard(keyboard::Event::KeyPressed {
|
match event {
|
||||||
key: keyboard::Key::Character(c),
|
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
modifiers,
|
key: keyboard::Key::Character(c),
|
||||||
..
|
modifiers,
|
||||||
}) = event
|
..
|
||||||
{
|
}) if modifiers.logo() => {
|
||||||
if modifiers.logo() {
|
|
||||||
match c.as_str() {
|
match c.as_str() {
|
||||||
"p" => messages.push(Message::TogglePreview),
|
"p" => messages.push(Message::TogglePreview),
|
||||||
"t" => messages.push(Message::InsertTable),
|
"t" => messages.push(Message::InsertTable),
|
||||||
"b" => messages.push(Message::ToggleBold),
|
"b" => messages.push(Message::ToggleBold),
|
||||||
"i" => messages.push(Message::ToggleItalic),
|
"i" => messages.push(Message::ToggleItalic),
|
||||||
"e" => messages.push(Message::SmartEval),
|
"e" => messages.push(Message::SmartEval),
|
||||||
|
"z" if modifiers.shift() => messages.push(Message::Redo),
|
||||||
|
"z" => messages.push(Message::Undo),
|
||||||
|
"f" => messages.push(Message::ToggleFind),
|
||||||
|
"g" if modifiers.shift() => messages.push(Message::FindPrev),
|
||||||
|
"g" => messages.push(Message::FindNext),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
|
key: keyboard::Key::Named(keyboard::key::Named::Escape),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if handle.state.find.visible {
|
||||||
|
messages.push(Message::HideFind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,6 +207,7 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
handle.state.tick();
|
handle.state.tick();
|
||||||
|
let pending_focus = handle.state.take_pending_focus();
|
||||||
|
|
||||||
let theme = Theme::Dark;
|
let theme = Theme::Dark;
|
||||||
let style = Style {
|
let style = Style {
|
||||||
|
|
@ -207,6 +221,12 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
&mut handle.renderer,
|
&mut handle.renderer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(focus_id) = pending_focus {
|
||||||
|
use iced_wgpu::core::widget::operation::focusable;
|
||||||
|
let mut op = focusable::focus(focus_id);
|
||||||
|
ui.operate(&handle.renderer, &mut op);
|
||||||
|
}
|
||||||
|
|
||||||
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
|
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
|
||||||
handle.cache = ui.into_cache();
|
handle.cache = ui.into_cache();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue