use abacus_core::Output; use clipboard::ClipboardProvider; use druid::{ piet::{PietTextLayout, Text, TextAttribute, TextLayout, TextLayoutBuilder}, widget::{Container, Flex, Label, Padding, Svg, SvgData}, Color, Event, FontDescriptor, FontFamily, FontWeight, LifeCycle, PaintCtx, Rect, RenderContext, Target, Widget, WidgetExt, }; use syntect::easy::HighlightLines; use syntect::highlighting::{Style, ThemeSet}; use syntect::parsing::SyntaxSet; use crate::{ app_header::ToolbarButtonController, data::{EditMode, EditorData}, Block, }; const FONT_SIZE: f64 = 16.0; mod keymap { pub const LINE_ABOVE: &str = "O"; pub const LINE_BELOW: &str = "o"; pub const DELETE_LINE: &str = "dd"; pub const EDIT_EOL: &str = "A"; pub const EDIT: &str = "i"; pub const EDIT_BOL: &str = "I"; pub const CURSOR_LEFT: &str = "h"; pub const CURSOR_RIGHT: &str = "l"; pub const CURSOR_UP: &str = "k"; pub const CURSOR_DOWN: &str = "j"; pub const DELETE_CHAR: &str = "x"; pub const DELETE_TO_EOL: &str = "D"; pub const WORD_FORWARD: &str = "w"; pub const WORD_BACK: &str = "b"; pub const SELECT_MODE: &str = "v"; #[derive(Debug, Default)] pub struct KeyList { input_value: String, commands: Vec<&'static str>, } impl KeyList { pub fn new() -> Self { Self { input_value: String::new(), commands: vec![ LINE_ABOVE, LINE_BELOW, DELETE_LINE, EDIT_EOL, EDIT, EDIT_BOL, CURSOR_LEFT, CURSOR_RIGHT, CURSOR_UP, CURSOR_DOWN, DELETE_CHAR, DELETE_TO_EOL, WORD_FORWARD, WORD_BACK, SELECT_MODE, ], } } pub fn push(&mut self, s: &str) { self.input_value.push_str(s); if !self.valid() { self.clear(); } } fn valid(&self) -> bool { self.commands .iter() .any(|i| i.starts_with(&self.input_value)) } pub fn clear(&mut self) { self.input_value.clear(); } pub fn get(&mut self) -> Option<&str> { let v = self .commands .iter() .find(|i| **i == self.input_value) .cloned(); if v.is_some() { self.clear(); } v } } } pub struct AbacusEditor { syntax_set: SyntaxSet, theme_set: ThemeSet, key_list: keymap::KeyList, } impl Default for AbacusEditor { fn default() -> Self { Self { syntax_set: SyntaxSet::load_defaults_newlines(), theme_set: ThemeSet::load_defaults(), key_list: keymap::KeyList::new(), } } } impl AbacusEditor { fn paint_cursor(&self, ctx: &mut PaintCtx, data: &EditorData, layout: &PietTextLayout) { if data.mode == EditMode::Insert { if data.cursor_pos == 0 { let rects = layout.rects_for_range(0..1); let rect = rects.first().unwrap(); let rect = Rect::new( rect.min_x() - 1.0, rect.min_y(), rect.min_x() + 1.0, rect.max_y(), ); ctx.fill( rect, &Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8), ); } else if let Some(char_rect) = layout .rects_for_range((data.cursor_pos.max(1) - 1)..data.cursor_pos) .last() { let cursor_rect = Rect::new( char_rect.max_x() - 1.0, char_rect.min_y(), char_rect.max_x() + 1.0, char_rect.max_y(), ); ctx.fill( cursor_rect, &Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8), ); } } if data.mode == EditMode::Normal { let char_rect = if data.content.len_chars() == 0 { let rects = layout.rects_for_range(0..1); rects.first().cloned() } else if data.current_char() == Some('\n') || data.cursor_pos == data.content.len_chars() { let range = (data.cursor_pos.max(1) - 1)..data.cursor_pos; layout.rects_for_range(range).last().cloned().map(|rect| { Rect::new(rect.max_x(), rect.min_y(), rect.max_x() + 10., rect.max_y()) }) } else { let range = data.cursor_pos..(data.cursor_pos + 1); layout.rects_for_range(range).last().cloned() }; if let Some(char_rect) = char_rect { 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), ); }; } } } pub fn build_highlighted_layout( &self, ctx: &mut PaintCtx, data: &EditorData, ) -> PietTextLayout { let syntax = self.syntax_set.find_syntax_by_extension("rs").unwrap(); let mut h = HighlightLines::new(syntax, &self.theme_set.themes["base16-mocha.dark"]); let mut layout = ctx .text() .new_text_layout(if data.content.len_chars() == 0 { String::from(" ") } else { data.content.to_string() }) .font(FontFamily::MONOSPACE, FONT_SIZE); let mut pos = 0; for line in data.content.lines() { let s = line.to_string(); let ranges: Vec<(Style, &str)> = h.highlight_line(&s, &self.syntax_set).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(); } } layout.build().unwrap() } } impl Widget for AbacusEditor { fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &EditorData, _env: &druid::Env) { let layout = self.build_highlighted_layout(ctx, data); if data.selection_pos.is_some() { let rects = layout.rects_for_range(data.select_range()); for rect in rects.iter() { if rect.width() == 0.0 { ctx.fill( Rect::new( rect.min_x(), rect.min_y(), rect.max_x() + 10.0, rect.max_y(), ), &Color::rgb8(90, 90, 90), ); } else { ctx.fill(rect, &Color::rgb8(90, 90, 90)); } } } if ctx.has_focus() { self.paint_cursor(ctx, data, &layout); } ctx.draw_text(&layout, (0.0, 0.0)); } fn event( &mut self, ctx: &mut druid::EventCtx, event: &druid::Event, data: &mut EditorData, _env: &druid::Env, ) { match event { Event::KeyDown(e) => { match &e.key { druid::keyboard_types::Key::Character(ch) if e.mods.ctrl() => { match ch.as_ref() { "a" => { if e.mods.ctrl() { data.select_all(); } } "c" => { if data.selection_pos.is_some() { let mut cb: clipboard::ClipboardContext = clipboard::ClipboardProvider::new().unwrap(); if let Some(slice) = data.content.get_slice(data.select_range()) { let _ = cb.set_contents(slice.to_string()); } } } "v" => { let mut cb: clipboard::ClipboardContext = clipboard::ClipboardProvider::new().unwrap(); data.push_str(&cb.get_contents().unwrap_or_default()); ctx.request_paint(); ctx.request_layout(); } _ => {} } } 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 => { self.key_list.push(ch); match self.key_list.get() { Some(keymap::DELETE_LINE) => { data.delete_current_line(); data.deselect(); } Some(keymap::DELETE_CHAR) => { data.delete_char_forward(); data.deselect(); } Some(keymap::EDIT) => { data.mode = EditMode::Insert; data.deselect(); } Some(keymap::LINE_ABOVE) => { data.mode = EditMode::Insert; data.cursor_to_start_of_line(); data.content.insert(data.cursor_pos, "\n"); data.deselect(); } Some(keymap::LINE_BELOW) => { data.mode = EditMode::Insert; data.cursor_to_end_of_line(); data.push_str("\n"); data.deselect(); } Some(keymap::EDIT_EOL) => { data.mode = EditMode::Insert; data.cursor_to_end_of_line(); data.deselect(); } Some(keymap::EDIT_BOL) => { data.cursor_to_start_of_line(); data.mode = EditMode::Insert; data.deselect(); } Some(keymap::CURSOR_LEFT) => { data.cursor_left(); } Some(keymap::CURSOR_DOWN) => { data.cursor_down(); } Some(keymap::CURSOR_UP) => { data.cursor_up(); } Some(keymap::CURSOR_RIGHT) => { data.cursor_right(); } Some(keymap::DELETE_TO_EOL) => { data.delete_to_eol(); data.deselect(); } Some(keymap::WORD_FORWARD) => { data.word_scan_forward(); } Some(keymap::WORD_BACK) => { data.word_scan_backward(); } Some(keymap::SELECT_MODE) => { data.selection_pos = Some(data.cursor_pos); } _ => {} } } druid::keyboard_types::Key::Enter if e.mods.ctrl() => { ctx.submit_command(crate::commands::PROCESS_WORKBOOK.to(Target::Global)); } druid::keyboard_types::Key::Enter if data.mode == EditMode::Insert => { data.push('\n'); ctx.request_layout(); data.deselect(); } druid::keyboard_types::Key::Enter if data.mode == EditMode::Normal => { data.cursor_down(); data.deselect(); } druid::keyboard_types::Key::Backspace => { data.delete_char_back(); ctx.request_layout(); data.deselect(); } druid::keyboard_types::Key::Delete => { data.delete_char_forward(); ctx.request_layout(); data.deselect(); } druid::keyboard_types::Key::Escape => { data.normal_mode(); data.deselect(); } druid::keyboard_types::Key::ArrowLeft if e.mods.shift() => { data.select_left(); } druid::keyboard_types::Key::ArrowRight if e.mods.shift() => { data.select_right(); } druid::keyboard_types::Key::ArrowLeft if !e.mods.shift() => { data.cursor_left(); } druid::keyboard_types::Key::ArrowRight if !e.mods.shift() => { data.cursor_right(); data.deselect(); } druid::keyboard_types::Key::ArrowUp if !e.mods.shift() => { data.cursor_up(); data.deselect(); } druid::keyboard_types::Key::ArrowDown if !e.mods.shift() => { data.cursor_down(); data.deselect(); } druid::keyboard_types::Key::ArrowUp if e.mods.shift() => { data.select_up(); data.deselect(); } druid::keyboard_types::Key::ArrowDown if e.mods.shift() => { data.select_down(); } druid::keyboard_types::Key::Tab => { data.push_str(" "); data.deselect(); } druid::keyboard_types::Key::End if e.mods.shift() => { data.select_to_end_of_line(); } druid::keyboard_types::Key::Home if e.mods.shift() => { data.select_to_start_of_line(); } 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); } } ctx.request_paint(); } Event::MouseDown(e) => { if !ctx.is_focused() { ctx.request_focus(); } let layout = ctx .text() .new_text_layout(data.content.to_string()) .font(FontFamily::MONOSPACE, FONT_SIZE) .build() .unwrap(); let pos = layout.hit_test_point(e.pos); if pos.idx != data.cursor_pos { data.cursor_pos = pos.idx; data.deselect(); ctx.request_paint(); } } Event::MouseMove(e) => { let layout = ctx .text() .new_text_layout(data.content.to_string()) .font(FontFamily::MONOSPACE, FONT_SIZE) .text_color(Color::rgb8(255, 255, 255)) .build() .unwrap(); let pos = layout.hit_test_point(e.pos); if e.buttons.has_left() { if data.selection_pos.is_none() { data.selection_pos = Some(data.cursor_pos); } let new_pos = (pos.idx + 1).min(data.content.len_chars()); if new_pos > data.cursor_pos { data.cursor_pos = pos.idx.min(data.content.len_chars()); } else { data.cursor_pos = pos.idx.max(1) - 1; } ctx.request_paint(); } } _ => {} } } fn lifecycle( &mut self, ctx: &mut druid::LifeCycleCtx, event: &druid::LifeCycle, _data: &EditorData, _env: &druid::Env, ) { match event { LifeCycle::FocusChanged(_) => { ctx.request_paint(); } LifeCycle::WidgetAdded => { // ctx.register_text_input(document) } LifeCycle::BuildFocusChain => { ctx.register_for_focus(); } _ => {} } } fn update( &mut self, ctx: &mut druid::UpdateCtx, old_data: &EditorData, data: &EditorData, _env: &druid::Env, ) { if old_data != data { ctx.request_paint(); ctx.request_layout(); } } fn layout( &mut self, ctx: &mut druid::LayoutCtx, bc: &druid::BoxConstraints, data: &EditorData, _env: &druid::Env, ) -> druid::Size { let layout = ctx .text() .new_text_layout(data.content.to_string()) .font(FontFamily::MONOSPACE, FONT_SIZE) .build() .unwrap(); (bc.max().width, layout.size().height).into() } } pub fn editor_header() -> impl Widget { let ban_svg = include_str!("../assets/ban.svg") .parse::() .unwrap_or_default(); let trash_svg = include_str!("../assets/trash.svg") .parse::() .unwrap_or_default(); let run_svg = include_str!("../assets/play.svg") .parse::() .unwrap_or_default(); Container::new( Flex::row() .must_fill_main_axis(true) .with_spacer(20.0) .with_child( Label::dynamic(|data: &Block, _| data.name.clone()) .with_font( FontDescriptor::new(FontFamily::SANS_SERIF) .with_weight(FontWeight::BOLD) .with_size(14.0), ) .padding(5.0) .on_click(|ctx, data, _| { ctx.submit_command(crate::commands::RENAME_BLOCK.with(data.index)); }), ) .with_flex_spacer(1.0) .with_child( Label::dynamic(|data: &Block, _| { match data.editor_data.mode { EditMode::Insert => "EDIT", EditMode::Normal => "", } .to_string() }) .with_font( FontDescriptor::new(FontFamily::SANS_SERIF) .with_size(14.0) .with_weight(FontWeight::BOLD), ) .padding(5.0), ) .with_spacer(10.0) .with_child( Label::dynamic(|data: &Block, _| { format!( "{}:{}", data.editor_data.current_line_index() + 1, data.editor_data.current_column() + 1, ) }) .with_font(FontDescriptor::new(FontFamily::SANS_SERIF).with_size(14.0)) .padding(5.0), ) .with_spacer(20.0) .with_child( Container::new(Padding::new(10.0, Svg::new(run_svg).fix_width(10.0))) .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) .on_click(|ctx, data: &mut Block, _env| { ctx.submit_command( crate::commands::PROCESS_BLOCK .with(data.index) .to(Target::Global), ); }), ) .with_spacer(10.0) .with_child( Container::new(Padding::new(10.0, Svg::new(ban_svg).fix_width(10.0))) .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) .on_click(|_ctx, data: &mut Block, _env| data.output = Output::None), ) .with_spacer(10.0) .with_child( Container::new(Padding::new(10.0, Svg::new(trash_svg).fix_width(10.0))) .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) .on_click(|ctx, data: &mut Block, _env| { ctx.submit_command( crate::commands::DELETE_BLOCK .with(data.index) .to(Target::Global), ); }), ) .with_spacer(20.0), ) .background(Color::rgb8(35, 35, 35)) .fix_height(30.0) }