A simple CPU rendered GUI IDE experience.
selections
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | src/bar.rs | 15 | ||||
| -rw-r--r-- | src/main.rs | 105 | ||||
| -rw-r--r-- | src/text.rs | 102 |
4 files changed, 182 insertions, 45 deletions
@@ -22,7 +22,10 @@ lower = "0.2.0" amap = "0.1.2" run_times = "0.1.0" array_chunks = "1.0.0" -rust-fsm = { version = "0.8.0", path = "../rust-fsm/rust-fsm", features = ["diagram"] } +rust-fsm = { version = "0.8.0", path = "../rust-fsm/rust-fsm", features = [ + "diagram", +] } +clipp = "0.1.0" [build-dependencies] cc = "*" @@ -4,6 +4,8 @@ use dsb::Cell; use dsb::cell::Style; use winit::keyboard::{Key, ModifiersState, NamedKey}; +use crate::text::TextArea; + pub struct Bar { pub text: crate::text::TextArea, pub last_action: String, @@ -18,6 +20,7 @@ impl Bar { oy: usize, fname: &str, state: &super::State, + t: &TextArea, ) { let row = &mut into[oy * w..oy * w + w]; row.fill(Cell { @@ -35,8 +38,8 @@ impl Bar { .chain(s("ave, ")) .chain(once(('Q', Style::BOLD))) .chain(s("uit, ")) - .chain(once(('C', Style::BOLD))) - .chain(s("opy }")); + .chain(once(('V', Style::BOLD))) + .chain(s("aste }")); x.zip(row).for_each(|((x, z), y)| { *y = Cell { @@ -75,6 +78,14 @@ impl Bar { } }); } + State::Selection(x) => { + let [(x1, y1), (x2, y2)] = t.position(x.clone()); + format!("selection from ({x1}, {y1}) to ({x2}, {y2})") + .chars() + .rev() + .zip(row.iter_mut().rev()) + .for_each(|(x, y)| y.letter = Some(x)); + } State::Save => unreachable!(), _ => {} } diff --git a/src/main.rs b/src/main.rs index 6899a92..ea3f974 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ use rust_fsm::StateMachineImpl; use swash::{FontRef, Instance}; use winit::event::{ElementState, Event, MouseScrollDelta, WindowEvent}; use winit::event_loop::{ControlFlow, EventLoop}; -use winit::keyboard::{Key, ModifiersState, NamedKey}; +use winit::keyboard::{Key, ModifiersState, NamedKey, SmolStr}; use crate::bar::Bar; use crate::text::TextArea; @@ -155,6 +155,10 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { style: Style { color: BG, bg: BG, flags: 0 }, letter: None, }); + let x = match &state { + State::Selection(x) => Some(x.clone()), + _ => None, + }; let t_ox = text.line_numbers( (c, r - 1), [67, 76, 87], @@ -168,6 +172,7 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { BG, (&mut cells, (c, r)), (t_ox, 0), + x, ); text.c = c - t_ox; text.r = r - 1; @@ -178,6 +183,7 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { r - 1, &origin.as_deref().unwrap_or("new buffer"), &state, + &text, ); println!("cell="); @@ -279,6 +285,16 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { event: WindowEvent::KeyboardInput { event, .. }, .. } if event.state == ElementState::Pressed => { + if matches!( + event.logical_key, + Key::Named( + NamedKey::Shift + | NamedKey::Alt + | NamedKey::Control + ) + ) { + return; + } let o = state .consume(Action::K(event.logical_key.clone())) .unwrap(); @@ -306,7 +322,13 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { let State::Selection(x) = &mut state else { panic!() }; - *x = text.cursor..text.cursor + let Key::Named(y) = event.logical_key else { + panic!() + }; + *x = text.extend_selection( + y, + text.cursor..text.cursor, + ); } Some(Do::UpdateSelection) => { let State::Selection(x) = &mut state else { @@ -317,6 +339,25 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { }; *x = text.extend_selection(y, x.clone()); } + Some(Do::Insert((x, c))) => { + text.rope.remove(x); + text.insert_(c); + } + Some(Do::Delete(x)) => { + text.rope.remove(x); + } + Some(Do::Copy(x)) => { + clipp::copy(text.rope.slice(x).to_string()); + } + Some(Do::Cut(x)) => { + clipp::copy( + text.rope.slice(x.clone()).to_string(), + ); + text.rope.remove(x); + } + Some(Do::Paste) => { + text.insert(&clipp::paste()); + } None => {} } window.request_redraw(); @@ -337,8 +378,17 @@ fn handle2(key: Key, text: &mut TextArea) { Named(Space) => text.insert(" "), Named(Backspace) => text.backspace(), Named(ArrowLeft) => text.left(), + Named(Home) if ctrl() => { + text.cursor = 0; + text.vo = 0; + } + Named(End) if ctrl() => { + text.cursor = text.rope.len_chars(); + text.vo = text.l() - text.r; + } Named(Home) => text.home(), Named(End) => text.end(), + // Named(Tab) Named(ArrowRight) => text.right(), Named(ArrowUp) => text.up(), Named(ArrowDown) => text.down(), @@ -380,36 +430,33 @@ fn shift() -> bool { fn ctrl() -> bool { unsafe { MODIFIERS }.control_key() } -fn arrow(k: &Key) -> bool { - matches!( - k, - Key::Named( - NamedKey::ArrowLeft - | NamedKey::ArrowRight - | NamedKey::ArrowDown - | NamedKey::ArrowUp - ) - ) -} // use NamedKey::Arrow use std::ops::Range; rust_fsm::state_machine! { - #[derive(Clone, Debug)] - pub(crate) State => Action => Do +#[derive(Clone, Debug)] +pub(crate) State => Action => Do - Dead => K(Key => _) => Dead, - Default => { - K(Key => Key::Character(x) if x == "s" && ctrl()) => Save [Save], - K(Key => Key::Character(x) if x == "q" && ctrl()) => Dead [Quit], - K(Key => x if shift() && arrow(&x)) => Selection(Range<usize> => 0..0) [StartSelection], - K(Key => _) => Default [Edit], - }, - Selection(Range<usize> => x) => K(Key => y if arrow(&y) && shift()) => Selection(Range<usize> => x) [UpdateSelection], - Save => { - RequireFilename => InputFname(TextArea => default()), - Saved => Default, - }, - InputFname(TextArea => t) => K(Key => Key::Named(NamedKey::Enter)) => Default [SaveTo(String => t.rope.to_string())], - InputFname(TextArea => t) => K(Key => k) => InputFname(TextArea => handle(k, t)), +Dead => K(Key => _) => Dead, +Default => { + K(Key::Character(x) if x == "s" && ctrl()) => Save [Save], + K(Key::Character(x) if x == "q" && ctrl()) => Dead [Quit], + K(Key::Character(x) if x == "v" && ctrl()) => Default [Paste], + K(Key::Named(NamedKey::ArrowUp | NamedKey::ArrowLeft | NamedKey::ArrowDown | NamedKey::ArrowRight | NamedKey::Home | NamedKey::End) if shift()) => Selection(Range<usize> => 0..0) [StartSelection], + K(_) => Default [Edit], +}, +Selection(x) => { + K(Key::Named(NamedKey::ArrowUp | NamedKey::ArrowLeft | NamedKey::ArrowDown | NamedKey::ArrowRight | NamedKey::Home | NamedKey::End) if shift()) => Selection(x) [UpdateSelection], + K(Key::Named(NamedKey::Backspace)) => Default [Delete(Range<usize> => x)], + K(Key::Character(y) if y == "x" && ctrl()) => Default [Cut(Range<usize> => x)], + K(Key::Character(y) if y == "c" && ctrl()) => Default [Copy(Range<usize> => x)], + K(Key::Character(y)) => Default [Insert((Range<usize>, SmolStr) => (x, y))], + K(_) => Default [Edit], +}, +Save => { + RequireFilename => InputFname(TextArea => default()), + Saved => Default, +}, +InputFname(t) => K(Key::Named(NamedKey::Enter)) => Default [SaveTo(String => t.rope.to_string())], +InputFname(t) => K(k) => InputFname(TextArea => handle(k, t)), } diff --git a/src/text.rs b/src/text.rs index bfb6d1c..373b04f 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,4 +1,5 @@ use std::fmt::Debug; +use std::ops::Range; use std::sync::LazyLock; use atools::Chunked; @@ -73,6 +74,17 @@ impl Clone for TextArea { } impl TextArea { + pub fn position( + &self, + Range { start, end }: Range<usize>, + ) -> [(usize, usize); 2] { + let y1 = self.rope.char_to_line(start); + let y2 = self.rope.char_to_line(end); + let x1 = start - self.rope.line_to_char(y1); + let x2 = end - self.rope.line_to_char(y2); + [(x1, y1), (x2, y2)] + } + pub fn l(&self) -> usize { self.rope.len_lines() } @@ -213,6 +225,7 @@ impl TextArea { bg: [u8; 3], (into, (w, _)): (&mut [Cell], (usize, usize)), (ox, oy): (usize, usize), + selection: Option<Range<usize>>, ) { static HL: LazyLock<HighlightConfiguration> = LazyLock::new(|| { @@ -261,9 +274,11 @@ impl TextArea { // } let y1 = self.rope.byte_to_line(start); let y2 = self.rope.byte_to_line(end); - let x1 = start - self.rope.line_to_char(y1); - let x2 = end - self.rope.line_to_char(y2); - // dbg!((x1, y1), (x2, y2)); + let x1 = self.rope.byte_to_char(start) + - self.rope.line_to_char(y1); + let x2 = self.rope.byte_to_char(end) + - self.rope.line_to_char(y2); + cells.get_mut(y1 * c + x1..y2 * c + x2).map(|x| { x.iter_mut().for_each(|x| { x.style.flags = STYLES[s].unwrap_or_default(); @@ -309,6 +324,32 @@ impl TextArea { } } } + selection.map(|x| self.position(x)).map(|[(x1, y1), (x2, y2)]| { + (y1 * c + x1..y2 * c + x2).for_each(|x| { + cells + .get_mut(x) + .filter( + _.letter.is_some() + || (x % c == 0) + || (self + .rope + .get_line(x / c) + .map(_.len_chars()) + .unwrap_or_default() + .saturating_sub(1) + == x % c), + ) + .map(|x| { + if x.letter == Some(' ') { + x.letter = Some('ยท'); // tabs? what are those + x.style.color = [0x4e, 0x62, 0x79]; + } + x.style.bg = [0x27, 0x43, 0x64]; + // 0x23, 0x34, 0x4B + }); + }); + }); + let cells = &cells[self.vo * c..self.vo * c + r * c]; assert_eq!(cells.len(), c * r); @@ -346,23 +387,58 @@ impl TextArea { } pub fn extend_selection( - &self, + &mut self, key: NamedKey, r: std::ops::Range<usize>, ) -> std::ops::Range<usize> { + macro_rules! left { + () => { + if self.cursor != 0 && self.cursor >= r.start { + // left to right going left (shrink right end) + r.start..self.cursor + } else { + // right to left going left (extend left end) + self.cursor..r.end + } + }; + } + macro_rules! right { + () => { + if self.cursor == self.rope.len_chars() { + r + } else if self.cursor > r.end { + // left to right (extend right end) + r.start..self.cursor + } else { + // right to left (shrink left end) + self.cursor..r.end + } + }; + } match key { - NamedKey::ArrowLeft => r.start.saturating_sub(1)..r.end, - NamedKey::ArrowRight => r.start..r.end + 1, + NamedKey::Home => { + self.home(); + left!() + } + NamedKey::End => { + self.end(); + right!() + } + NamedKey::ArrowLeft => { + self.left(); + left!() + } + NamedKey::ArrowRight => { + self.right(); + right!() + } NamedKey::ArrowUp => { - let l = self.rope.char_to_line(r.start); - self.rope.line_to_char(l - 1) + r.start - - self.rope.line_to_char(l)..r.end + self.up(); + left!() } NamedKey::ArrowDown => { - let l = self.rope.char_to_line(r.end); - r.start - ..self.rope.line_to_char(l + 1) + r.end - - self.rope.line_to_char(l) + self.down(); + right!() } _ => unreachable!(), } |