undo/redo stack + find/replace bar with Cmd+F/G shortcuts
This commit is contained in:
parent
01f34a4f34
commit
57488a2861
|
|
@ -159,7 +159,7 @@ class IcedViewportView: NSView {
|
|||
|
||||
if cmd && !shift {
|
||||
switch chars {
|
||||
case "a", "b", "c", "i", "v", "x", "z", "p", "t",
|
||||
case "a", "b", "c", "e", "f", "g", "i", "v", "x", "z", "p", "t",
|
||||
"=", "+", "-", "0":
|
||||
keyDown(with: event)
|
||||
return true
|
||||
|
|
@ -168,7 +168,7 @@ class IcedViewportView: NSView {
|
|||
}
|
||||
if cmd && shift {
|
||||
switch chars {
|
||||
case "z":
|
||||
case "g", "z":
|
||||
keyDown(with: event)
|
||||
return true
|
||||
default: break
|
||||
|
|
@ -214,4 +214,16 @@ class IcedViewportView: NSView {
|
|||
viewport_free_string(cstr)
|
||||
return result
|
||||
}
|
||||
|
||||
func sendCommand(_ command: UInt32) {
|
||||
guard let h = viewportHandle else { return }
|
||||
viewport_send_command(h, command)
|
||||
}
|
||||
|
||||
func setTheme(_ name: String) {
|
||||
guard let h = viewportHandle else { return }
|
||||
name.withCString { cstr in
|
||||
viewport_set_theme(h, cstr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use iced_wgpu::core::keyboard;
|
||||
use iced_wgpu::core::keyboard::key;
|
||||
|
|
@ -11,7 +12,10 @@ use iced_widget::canvas;
|
|||
use iced_widget::container;
|
||||
use iced_widget::markdown;
|
||||
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::widget::Id as WidgetId;
|
||||
use crate::palette;
|
||||
use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -28,11 +32,62 @@ pub enum Message {
|
|||
ZoomIn,
|
||||
ZoomOut,
|
||||
ZoomReset,
|
||||
Undo,
|
||||
Redo,
|
||||
ToggleFind,
|
||||
HideFind,
|
||||
FindQueryChanged(String),
|
||||
FindNext,
|
||||
FindPrev,
|
||||
ReplaceQueryChanged(String),
|
||||
ReplaceOne,
|
||||
ReplaceAll,
|
||||
}
|
||||
|
||||
pub const RESULT_PREFIX: &str = "→ ";
|
||||
pub const ERROR_PREFIX: &str = "⚠ ";
|
||||
|
||||
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 content: text_editor::Content<iced_wgpu::Renderer>,
|
||||
pub font_size: f32,
|
||||
|
|
@ -40,20 +95,29 @@ pub struct EditorState {
|
|||
pub parsed: Vec<markdown::Item>,
|
||||
pub lang: Option<String>,
|
||||
scroll_offset: f32,
|
||||
|
||||
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 {
|
||||
let p = palette::current();
|
||||
markdown::Style {
|
||||
font: Font::default(),
|
||||
inline_code_highlight: Highlight {
|
||||
background: Color::from_rgb(0.188, 0.188, 0.259).into(),
|
||||
background: p.surface0.into(),
|
||||
border: border::rounded(4),
|
||||
},
|
||||
inline_code_padding: padding::left(2).right(2),
|
||||
inline_code_color: Color::from_rgb(0.651, 0.890, 0.631),
|
||||
inline_code_color: p.green,
|
||||
inline_code_font: Font::MONOSPACE,
|
||||
code_block_font: Font::MONOSPACE,
|
||||
link_color: Color::from_rgb(0.537, 0.706, 0.980),
|
||||
link_color: p.blue,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +155,12 @@ impl EditorState {
|
|||
parsed: Vec::new(),
|
||||
lang: Some("rust".into()),
|
||||
scroll_offset: 0.0,
|
||||
undo_stack: Vec::new(),
|
||||
redo_stack: Vec::new(),
|
||||
last_edit_kind: EditKind::Other,
|
||||
last_edit_time: Instant::now(),
|
||||
find: FindState::new(),
|
||||
pending_focus: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,11 +256,144 @@ 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) {
|
||||
match message {
|
||||
Message::EditorAction(action) => {
|
||||
let is_edit = action.is_edit();
|
||||
|
||||
if let Some(kind) = Self::classify_edit(&action) {
|
||||
self.maybe_snapshot(kind);
|
||||
}
|
||||
|
||||
if let Action::Scroll { lines } = &action {
|
||||
let lh = self.line_height();
|
||||
self.scroll_offset += *lines as f32 * lh;
|
||||
|
|
@ -312,6 +515,107 @@ impl EditorState {
|
|||
Message::ZoomReset => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -330,12 +634,15 @@ impl EditorState {
|
|||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(|_theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.08, 0.08, 0.10))),
|
||||
border: Border::default(),
|
||||
text_color: Some(Color::from_rgb(0.804, 0.839, 0.957)),
|
||||
shadow: Shadow::default(),
|
||||
snap: false,
|
||||
.style(|_theme: &Theme| {
|
||||
let p = palette::current();
|
||||
container::Style {
|
||||
background: Some(Background::Color(p.base)),
|
||||
border: Border::default(),
|
||||
text_color: Some(p.text),
|
||||
shadow: Shadow::default(),
|
||||
snap: false,
|
||||
}
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
|
|
@ -348,12 +655,15 @@ impl EditorState {
|
|||
.padding(Padding { top: top_pad, right: 8.0, bottom: 8.0, left: 8.0 })
|
||||
.wrapping(Wrapping::Word)
|
||||
.key_binding(macos_key_binding)
|
||||
.style(|_theme, _status| Style {
|
||||
background: Background::Color(Color::from_rgb(0.08, 0.08, 0.10)),
|
||||
border: Border::default(),
|
||||
placeholder: Color::from_rgb(0.4, 0.4, 0.4),
|
||||
value: Color::WHITE,
|
||||
selection: Color::from_rgba(0.3, 0.5, 0.8, 0.4),
|
||||
.style(|_theme, _status| {
|
||||
let p = palette::current();
|
||||
Style {
|
||||
background: Background::Color(p.base),
|
||||
border: Border::default(),
|
||||
placeholder: p.overlay0,
|
||||
value: p.text,
|
||||
selection: Color { a: 0.4, ..p.blue },
|
||||
}
|
||||
});
|
||||
|
||||
let settings = SyntaxSettings {
|
||||
|
|
@ -365,7 +675,7 @@ impl EditorState {
|
|||
settings,
|
||||
|highlight, _theme| Format {
|
||||
color: Some(syntax::highlight_color(highlight.kind)),
|
||||
font: None,
|
||||
font: syntax::highlight_font(highlight.kind),
|
||||
},
|
||||
)
|
||||
.into();
|
||||
|
|
@ -401,29 +711,149 @@ impl EditorState {
|
|||
iced_widget::text(format!("{mode_label} Ln {line}, Col {col}"))
|
||||
.font(Font::MONOSPACE)
|
||||
.size(11.0)
|
||||
.color(Color::from_rgb(0.55, 0.55, 0.55))
|
||||
.color(palette::current().overlay1)
|
||||
.into(),
|
||||
])
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(Padding { top: 3.0, right: 10.0, bottom: 3.0, left: 10.0 })
|
||||
.style(|_theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.12, 0.12, 0.14))),
|
||||
border: Border::default(),
|
||||
text_color: None,
|
||||
shadow: Shadow::default(),
|
||||
snap: false,
|
||||
.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,
|
||||
}
|
||||
});
|
||||
|
||||
let mut col_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> =
|
||||
vec![main_content];
|
||||
let mut col_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
||||
|
||||
if self.find.visible {
|
||||
col_items.push(self.find_bar());
|
||||
}
|
||||
|
||||
col_items.push(main_content);
|
||||
col_items.push(status_bar.into());
|
||||
|
||||
iced_widget::column(col_items)
|
||||
.height(Length::Fill)
|
||||
.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 {
|
||||
|
|
@ -463,7 +893,7 @@ impl canvas::Program<Message, Theme, iced_wgpu::Renderer> for Gutter {
|
|||
frame.fill_rectangle(
|
||||
Point::ORIGIN,
|
||||
bounds.size(),
|
||||
Color::from_rgb(0.06, 0.06, 0.08),
|
||||
palette::current().crust,
|
||||
);
|
||||
|
||||
let first_visible = (self.scroll_offset / lh).floor() as usize;
|
||||
|
|
@ -481,10 +911,11 @@ impl canvas::Program<Message, Theme, iced_wgpu::Renderer> for Gutter {
|
|||
if y + lh < 0.0 || y > bounds.height {
|
||||
continue;
|
||||
}
|
||||
let p = palette::current();
|
||||
let color = if line_idx == self.cursor_line {
|
||||
Color::from_rgb(0.55, 0.55, 0.62)
|
||||
p.overlay1
|
||||
} else {
|
||||
Color::from_rgb(0.35, 0.35, 0.42)
|
||||
p.surface2
|
||||
};
|
||||
frame.fill_text(canvas::Text {
|
||||
content: format!("{}", line_idx + 1),
|
||||
|
|
@ -645,6 +1076,12 @@ fn macos_key_binding(key_press: KeyPress) -> Option<Binding<Message>> {
|
|||
}
|
||||
|
||||
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() => {
|
||||
Some(Binding::Custom(Message::ZoomIn))
|
||||
}
|
||||
|
|
@ -736,3 +1173,7 @@ fn leading_whitespace(line: &str) -> &str {
|
|||
let end = line.len() - line.trim_start().len();
|
||||
&line[..end]
|
||||
}
|
||||
|
||||
fn nth_char_byte_offset(s: &str, char_idx: usize) -> Option<usize> {
|
||||
s.char_indices().nth(char_idx).map(|(i, _)| i)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use raw_window_handle::{
|
|||
};
|
||||
|
||||
use crate::editor::{EditorState, Message};
|
||||
use crate::palette;
|
||||
use crate::ViewportHandle;
|
||||
|
||||
struct MacClipboard;
|
||||
|
|
@ -158,22 +159,35 @@ pub fn render(handle: &mut ViewportHandle) {
|
|||
let mut messages: Vec<Message> = Vec::new();
|
||||
|
||||
for event in &handle.events {
|
||||
if let Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Character(c),
|
||||
modifiers,
|
||||
..
|
||||
}) = event
|
||||
{
|
||||
if modifiers.logo() {
|
||||
match event {
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Character(c),
|
||||
modifiers,
|
||||
..
|
||||
}) if modifiers.logo() => {
|
||||
match c.as_str() {
|
||||
"p" => messages.push(Message::TogglePreview),
|
||||
"t" => messages.push(Message::InsertTable),
|
||||
"b" => messages.push(Message::ToggleBold),
|
||||
"i" => messages.push(Message::ToggleItalic),
|
||||
"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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +206,8 @@ pub fn render(handle: &mut ViewportHandle) {
|
|||
handle.state.update(msg);
|
||||
}
|
||||
|
||||
let pending_focus = handle.state.take_pending_focus();
|
||||
|
||||
let theme = Theme::Dark;
|
||||
let style = Style {
|
||||
text_color: Color::WHITE,
|
||||
|
|
@ -204,13 +220,18 @@ pub fn render(handle: &mut ViewportHandle) {
|
|||
&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);
|
||||
handle.cache = ui.into_cache();
|
||||
|
||||
let bg = Color::from_rgb(0.08, 0.08, 0.10);
|
||||
handle
|
||||
.renderer
|
||||
.present(Some(bg), handle.format, &view, &handle.viewport);
|
||||
.present(Some(palette::current().base), handle.format, &view, &handle.viewport);
|
||||
|
||||
frame.present();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue