diff --git a/Cargo.lock b/Cargo.lock index 469de53..d6a4de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,8 @@ version = "0.1.0" dependencies = [ "abacus-core", "druid", + "ropey", + "syntect", ] [[package]] @@ -1905,6 +1907,16 @@ dependencies = [ "syn", ] +[[package]] +name = "ropey" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd22239fafefc42138ca5da064f3c17726a80d2379d817a3521240e78dd0064" +dependencies = [ + "smallvec", + "str_indices", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2085,6 +2097,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str_indices" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0" + [[package]] name = "strength_reduce" version = "0.2.3" diff --git a/abacus-ui/Cargo.toml b/abacus-ui/Cargo.toml index 6cdea7d..e0bb4b1 100644 --- a/abacus-ui/Cargo.toml +++ b/abacus-ui/Cargo.toml @@ -7,4 +7,6 @@ edition = "2021" [dependencies] druid = { git = "https://github.com/linebender/druid.git" } -abacus-core = { path = "../abacus-core" } \ No newline at end of file +abacus-core = { path = "../abacus-core" } +syntect = "5.0.0" +ropey = "1.5.0" \ No newline at end of file diff --git a/abacus-ui/src/editor.rs b/abacus-ui/src/editor.rs index b96dc32..39260a2 100644 --- a/abacus-ui/src/editor.rs +++ b/abacus-ui/src/editor.rs @@ -1,8 +1,15 @@ +use std::ops::Range; + use druid::{ - piet::{Text, TextLayout, TextLayoutBuilder}, - Color, Data, Event, FontFamily, Lens, LifeCycle, RenderContext, Widget, + piet::{Text, TextAttribute, TextLayout, TextLayoutBuilder}, + Color, Data, Event, FontFamily, Lens, LifeCycle, Rect, RenderContext, Widget, }; +use ropey::Rope; +use syntect::easy::HighlightLines; +use syntect::highlighting::{Style, ThemeSet}; +use syntect::parsing::SyntaxSet; + #[derive(Clone, Data, PartialEq, Eq)] pub enum EditMode { Normal, @@ -13,44 +20,150 @@ pub struct AbacusEditor; #[derive(Data, Lens, Clone)] pub struct EditorData { - pub raw_content: String, + #[data(same_fn = "PartialEq::eq")] + pub content: Rope, pub cursor_pos: usize, pub mode: EditMode, + pub cursor_opactiy: f64, + pub cursor_fade: f64, + pub selection_pos: usize, } impl Default for EditorData { fn default() -> Self { Self { - raw_content: "let x = 1;\nx+1".to_string(), + content: Rope::from_str("let x = 1;\nx+1"), cursor_pos: 5, + selection_pos: 5, mode: EditMode::Normal, + cursor_opactiy: 0.1, + cursor_fade: 1.0, } } } impl EditorData { + pub fn move_cursor(&mut self, idx: usize) { + self.cursor_pos = idx; + self.deselect(); + } + + pub fn select_range(&self) -> Range { + if self.cursor_pos > self.selection_pos { + self.selection_pos..self.cursor_pos + } else { + self.cursor_pos..self.selection_pos + } + } + pub fn push_str(&mut self, s: &str) { - self.raw_content.push_str(s); - self.cursor_to_end(); + if self.selection_pos != self.cursor_pos { + self.content.remove(self.select_range()); + self.content.insert(self.select_range().start, s); + } else { + self.content.insert(self.cursor_pos, s); + } + self.move_cursor(self.select_range().start + s.len()); } pub fn push(&mut self, c: char) { - self.raw_content.push(c); - self.cursor_to_end(); + self.content.insert_char(self.cursor_pos, c); + self.cursor_right(); } - pub fn cursor_to_end(&mut self) { - self.cursor_pos = self.raw_content.len(); + // pub fn cursor_to_end(&mut self) { + // self.move_cursor(self.content.len_chars()); + // } + + pub fn deselect(&mut self) { + self.selection_pos = self.cursor_pos; } pub fn delete_char_back(&mut self) { - self.raw_content.pop(); - self.cursor_to_end(); + if !self.select_range().is_empty() { + let range = self.select_range(); + self.content.remove(self.select_range()); + self.move_cursor(range.start); + return; + } + + if self.cursor_pos > 0 { + if self.cursor_pos == self.content.len_chars() { + self.content + .remove(self.content.len_chars()..self.content.len_chars()); + + // self.raw_content.pop(); + } else { + self.content.remove((self.cursor_pos - 1)..self.cursor_pos); + } + self.cursor_left(); + } + } + + pub fn cursor_left(&mut self) { + if self.cursor_pos > 0 { + self.move_cursor(self.cursor_pos - 1); + } + } + + pub fn cursor_up(&mut self) { + let line_idx = self.content.char_to_line(self.cursor_pos); + + if line_idx > 0 { + let start_of_current_line = self.content.line_to_char(line_idx); + let line_pos = self.cursor_pos - start_of_current_line; + let up_line_start = self.content.line_to_char(line_idx - 1); + let up_line = self.content.line(line_idx - 1); + dbg!(line_pos.min(up_line.len_chars())); + self.move_cursor(up_line_start + line_pos.min(up_line.len_chars() - 1)); + } + } + + pub fn cursor_down(&mut self) { + let line_idx = self.content.char_to_line(self.cursor_pos); + if line_idx < self.content.len_lines() - 1 { + let start_of_current_line = self.content.line_to_char(line_idx); + let line_pos = self.cursor_pos - start_of_current_line; + let start_of_next_line = self.content.line_to_char(line_idx + 1); + let next_line_len = self.content.line(line_idx + 1).len_chars(); + + self.move_cursor(start_of_next_line + line_pos.min(next_line_len)); + } + } + + pub fn cursor_right(&mut self) { + if self.cursor_pos < self.content.len_chars() { + self.move_cursor(self.cursor_pos + 1); + } + } + + pub fn cursor_to_end_of_line(&mut self) { + let line_idx = self.content.char_to_line(self.cursor_pos); + let start_of_line = self.content.line_to_char(line_idx); + let line = self.content.line(line_idx); + if line_idx == self.content.len_lines() - 1 { + self.move_cursor(start_of_line + line.len_chars()); + } else { + self.move_cursor(start_of_line + line.len_chars() - 1); + } + } + + pub fn cursor_to_start_of_line(&mut self) { + let start_of_line = self + .content + .line_to_char(self.content.char_to_line(self.cursor_pos)); + + self.move_cursor(start_of_line); + } + + pub fn select_all(&mut self) { + self.selection_pos = self.content.len_chars(); + self.cursor_pos = 0; } } impl Widget for AbacusEditor { - fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &EditorData, env: &druid::Env) { + fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &EditorData, _env: &druid::Env) { let size = ctx.size(); let rect = size.to_rect(); @@ -60,25 +173,94 @@ impl Widget for AbacusEditor { ctx.fill(rect, &Color::rgb8(20, 20, 20)); } - let layout = ctx + if data.content.len_chars() == 0 { + return; + } + + let ps = SyntaxSet::load_defaults_newlines(); + let ts = ThemeSet::load_defaults(); + let syntax = ps.find_syntax_by_extension("rs").unwrap(); + let mut h = HighlightLines::new(syntax, &ts.themes["base16-mocha.dark"]); + + let mut layout = ctx .text() - .new_text_layout(data.raw_content.clone()) - .font(FontFamily::MONOSPACE, 24.0) - .text_color(Color::rgb8(255, 255, 255)) - .build() - .unwrap(); + .new_text_layout(data.content.to_string()) + .font(FontFamily::MONOSPACE, 22.0); - dbg!(layout.rects_for_range((data.cursor_pos - 1)..data.cursor_pos)); - ctx.fill( - layout - .rects_for_range((data.cursor_pos - 1)..data.cursor_pos) + let mut pos = 0; + for line in data.content.lines() { + let s = line.to_string(); + let ranges: Vec<(Style, &str)> = h.highlight_line(&s, &ps).unwrap(); + + for (style, txt) in ranges { + layout = layout.range_attribute( + pos..(pos + txt.len()), + TextAttribute::TextColor(Color::rgba8( + style.foreground.r, + style.foreground.g, + style.foreground.b, + style.foreground.a, + )), + ); + pos += txt.len(); + } + } + + let layout = layout.build().unwrap(); + + // paint cursor + if data.mode == EditMode::Insert { + if let Some(char_rect) = layout + .rects_for_range((data.cursor_pos.max(1) - 1)..data.cursor_pos) .last() - .unwrap(), - &Color::rgb8(50, 50, 50), - ); - ctx.draw_text(&layout, (1.0, 1.0)); + { + let cursor_rect = Rect::new( + char_rect.max_x() + 3.0, + char_rect.min_y() + 2.0, + char_rect.max_x() + 5.0, + char_rect.max_y() - 2.0, + ); - dbg!(ctx.is_focused()); + ctx.fill( + cursor_rect, + &Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8), + ); + } + } + + if data.mode == EditMode::Normal { + if let Some(char_rect) = layout + .rects_for_range((data.cursor_pos.max(1) - 1)..data.cursor_pos) + .last() + { + if char_rect.width() == 0. { + let rect = Rect::new( + char_rect.min_x(), + char_rect.min_y(), + char_rect.min_x() + 10., + char_rect.max_y(), + ); + ctx.fill( + rect, + &Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8), + ); + } else { + ctx.fill( + char_rect, + &Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8), + ); + }; + } + } + + if data.selection_pos != data.cursor_pos { + let rects = layout.rects_for_range(data.select_range()); + for rect in rects.iter() { + ctx.fill(rect, &Color::rgb8(90, 90, 90)); + } + } + + ctx.draw_text(&layout, (1.0, 1.0)); } fn event( @@ -86,33 +268,137 @@ impl Widget for AbacusEditor { ctx: &mut druid::EventCtx, event: &druid::Event, data: &mut EditorData, - env: &druid::Env, + _env: &druid::Env, ) { match event { - Event::KeyUp(e) => match &e.key { - druid::keyboard_types::Key::Character(c) => { + Event::KeyDown(e) => match &e.key { + druid::keyboard_types::Key::Character(ch) + if data.mode == EditMode::Insert && e.mods.ctrl() => + { + match ch.as_ref() { + "a" => { + if e.mods.ctrl() { + data.select_all(); + } + } + "u" => {} + _ => {} + } + } + druid::keyboard_types::Key::Character(c) if data.mode == EditMode::Insert => { data.push_str(c); ctx.request_paint(); } + druid::keyboard_types::Key::Character(ch) if data.mode == EditMode::Normal => { + match ch.as_ref() { + "i" => { + data.mode = EditMode::Insert; + } + "A" => { + data.mode = EditMode::Insert; + data.cursor_to_end_of_line(); + } + "a" => { + if e.mods.ctrl() { + data.select_all(); + } + } + "h" => { + data.cursor_left(); + } + "j" => { + data.cursor_down(); + } + "k" => { + data.cursor_up(); + } + "l" => { + data.cursor_right(); + } + _ => {} + } + } druid::keyboard_types::Key::Enter => { data.push('\n'); ctx.request_paint(); + ctx.request_layout(); } druid::keyboard_types::Key::Backspace => { data.delete_char_back(); ctx.request_paint(); } druid::keyboard_types::Key::Escape => { - data.mode == EditMode::Normal; + data.mode = EditMode::Normal; + } + druid::keyboard_types::Key::ArrowLeft if !e.mods.shift() => { + data.cursor_left(); + } + druid::keyboard_types::Key::ArrowRight if !e.mods.shift() => { + data.cursor_right(); + } + druid::keyboard_types::Key::ArrowUp if !e.mods.shift() => { + data.cursor_up(); + } + druid::keyboard_types::Key::ArrowDown if !e.mods.shift() => { + data.cursor_down(); + } + druid::keyboard_types::Key::Tab => { + data.push_str(" "); + ctx.request_paint(); + } + druid::keyboard_types::Key::End => { + data.cursor_to_end_of_line(); + data.deselect(); + } + druid::keyboard_types::Key::Home => { + data.cursor_to_start_of_line(); + data.deselect(); } e => { dbg!(e); } }, - Event::MouseDown(_) => { + Event::MouseDown(e) => { if !ctx.is_focused() { ctx.request_focus(); } + + let layout = ctx + .text() + .new_text_layout(data.content.to_string()) + .font(FontFamily::MONOSPACE, 24.0) + .text_color(Color::rgb8(255, 255, 255)) + .build() + .unwrap(); + + let pos = layout.hit_test_point(e.pos); + data.cursor_pos = pos.idx; + data.selection_pos = pos.idx; + } + Event::MouseMove(e) => { + let layout = ctx + .text() + .new_text_layout(data.content.to_string()) + .font(FontFamily::MONOSPACE, 24.0) + .text_color(Color::rgb8(255, 255, 255)) + .build() + .unwrap(); + let pos = layout.hit_test_point(e.pos); + if e.buttons.has_left() { + data.selection_pos = pos.idx; + } + } + Event::AnimFrame(e) => { + data.cursor_opactiy += ((*e as f64) * 0.00000065) * data.cursor_fade; + if data.cursor_opactiy >= 255.0 { + data.cursor_opactiy = 255.0; + data.cursor_fade *= -1.0; + } else if data.cursor_opactiy <= 0.0 { + data.cursor_opactiy = 0.1; + data.cursor_fade *= -1.0; + } + ctx.request_paint(); + ctx.request_anim_frame(); } _ => {} } @@ -122,12 +408,13 @@ impl Widget for AbacusEditor { &mut self, ctx: &mut druid::LifeCycleCtx, event: &druid::LifeCycle, - data: &EditorData, - env: &druid::Env, + _data: &EditorData, + _env: &druid::Env, ) { match event { LifeCycle::FocusChanged(_) => { ctx.request_paint(); + ctx.request_anim_frame(); } LifeCycle::WidgetAdded => { // ctx.register_text_input(document) @@ -141,10 +428,10 @@ impl Widget for AbacusEditor { fn update( &mut self, - ctx: &mut druid::UpdateCtx, - old_data: &EditorData, - data: &EditorData, - env: &druid::Env, + _ctx: &mut druid::UpdateCtx, + _old_data: &EditorData, + _data: &EditorData, + _env: &druid::Env, ) { } @@ -153,8 +440,16 @@ impl Widget for AbacusEditor { ctx: &mut druid::LayoutCtx, bc: &druid::BoxConstraints, data: &EditorData, - env: &druid::Env, + _env: &druid::Env, ) -> druid::Size { + let layout = ctx + .text() + .new_text_layout(data.content.to_string()) + .font(FontFamily::MONOSPACE, 22.0) + .build() + .unwrap(); + + bc.shrink_max_height_to(layout.size().height); bc.max() } } diff --git a/abacus-ui/src/main.rs b/abacus-ui/src/main.rs index aeac267..9354d6c 100644 --- a/abacus-ui/src/main.rs +++ b/abacus-ui/src/main.rs @@ -1,26 +1,14 @@ mod editor; -use druid::widget::{Align, Flex, Label, Padding, RawLabel}; -use druid::{AppLauncher, Color, Data, PlatformError, Widget, WidgetExt, WindowDesc}; -use editor::EditorData; +use druid::widget::{Flex, Padding}; +use druid::{AppLauncher, Data, PlatformError, Widget, WindowDesc}; fn build_ui() -> impl Widget { Padding::new( 10.0, - Flex::row() - .with_flex_child( - Flex::column().with_flex_child(editor::AbacusEditor, 1.0), - 1.0, - ) - .with_flex_child( - Flex::column() - .with_flex_child( - Label::new("top right").background(Color::rgb8(0, 0, 255)), - 1.0, - ) - .with_flex_child(Align::centered(Label::new("bottom right")), 1.0), - 1.0, - ), + Flex::column() + .with_flex_child(Padding::new(10.0, editor::AbacusEditor), 2.0) + .with_flex_child(editor::AbacusEditor, 1.0), ) }