From 69f6f68ba6814b1d5cf22adf06bbed97bbb8ec90 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 25 Oct 2023 09:08:53 -0700 Subject: [PATCH] Added blinking cursor --- Cargo.lock | 35 ++++++++++++++---- src/widgets/input.rs | 88 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 256ee83..b367de0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,13 +35,14 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -95,7 +96,7 @@ dependencies = [ [[package]] name = "appit" version = "0.1.0" -source = "git+https://github.com/khonsulabs/appit#665107f59b0d1b1cc6780800fa1e172faf615f07" +source = "git+https://github.com/khonsulabs/appit#91c540c2a2db69eb25ea47eccb7aac1eb911933e" dependencies = [ "raw-window-handle 0.5.2", "winit", @@ -1607,9 +1608,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" [[package]] name = "toml_edit" @@ -1881,9 +1882,9 @@ dependencies = [ [[package]] name = "web-time" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8208e3fdbc243c8fd30805721869242a7f6de3e2e9f3b057652ab36e52ae1e87" +checksum = "57099a701fb3a8043f993e8228dc24229c7b942e2b009a1b962e54489ba1d3bf" dependencies = [ "js-sys", "wasm-bindgen", @@ -2313,3 +2314,23 @@ name = "zeno" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" + +[[package]] +name = "zerocopy" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/src/widgets/input.rs b/src/widgets/input.rs index ac6b406..e8e1111 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -1,5 +1,6 @@ use std::cmp::Ordering; use std::fmt::Debug; +use std::time::Duration; use kludgine::app::winit::event::Ime; use kludgine::app::winit::keyboard::Key; @@ -17,10 +18,13 @@ use crate::styles::{HighlightColor, LineHeight, Styles, TextColor, TextSize}; use crate::utils::ModifiersExt; use crate::widget::{EventHandling, IntoValue, Value, Widget, HANDLED, UNHANDLED}; +const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500); + #[must_use] pub struct Input { pub text: Value, editor: Option, + cursor_state: CursorState, } impl Input { @@ -32,6 +36,7 @@ impl Input { Self { text: initial_text.into_value(), editor: None, + cursor_state: CursorState::default(), } } @@ -121,11 +126,14 @@ impl Widget for Input { y: location.y.0, }, ); + self.cursor_state.force_on(); context.set_needs_redraw(); } #[allow(clippy::too_many_lines)] fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) { + self.cursor_state.update(context.elapsed()); + let cursor_state = self.cursor_state; let size = context.graphics.size(); let styles = context.query_style(&[&TextColor, &HighlightColor]); let highlight = styles.get_or_default(&HighlightColor); @@ -246,18 +254,21 @@ impl Widget for Input { (Err(_), Err(_)) => {} } } else if let Ok((location, _)) = cursor_glyph(buffer, &cursor) { - context.graphics.draw_shape( - &Shape::filled_rect( - Rect::new( - location, - Size::new(Px(1), line_height), + if cursor_state.visible { + context.graphics.draw_shape( + &Shape::filled_rect( + Rect::new( + location, + Size::new(Px(1), line_height), + ), + highlight, // TODO cursor should be a bold color, highlight probably not. This should have its own color. ), - highlight, // TODO cursor should be a bold color, highlight probably not. This should have its own color. - ), - Point::default(), - None, - None, - ); + Point::default(), + None, + None, + ); + } + context.redraw_in(cursor_state.remaining_until_blink); } } @@ -309,7 +320,7 @@ impl Widget for Input { "Keyboard input: {:?}. {:?}, {:?}", input.logical_key, input.text, input.physical_key ); - match (input.logical_key, input.text) { + let handled = match (input.logical_key, input.text) { (key @ (Key::Backspace | Key::Delete), _) => { editor.action( context.kludgine.font_system(), @@ -319,7 +330,6 @@ impl Widget for Input { _ => unreachable!("previously matched"), }, ); - context.set_needs_redraw(); HANDLED } (key @ (Key::ArrowLeft | Key::ArrowDown | Key::ArrowUp | Key::ArrowRight), _) => { @@ -346,16 +356,21 @@ impl Widget for Input { _ => unreachable!("previously matched"), }, ); - context.set_needs_redraw(); HANDLED } (_, Some(text)) if !context.modifiers().state().primary() => { editor.insert_string(&text, None); - context.set_needs_redraw(); HANDLED } (_, _) => UNHANDLED, + }; + + if handled.is_break() { + context.set_needs_redraw(); + self.cursor_state.force_on(); } + + handled } fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling { @@ -379,7 +394,6 @@ impl Widget for Input { } fn blur(&mut self, context: &mut EventContext<'_, '_>) { - println!("Blur"); context.set_ime_allowed(false); } } @@ -459,3 +473,45 @@ enum NotVisible { Before, After, } + +#[derive(Clone, Copy)] +struct CursorState { + visible: bool, + remaining_until_blink: Duration, +} + +impl Default for CursorState { + fn default() -> Self { + Self { + visible: true, + remaining_until_blink: CURSOR_BLINK_DURATION, + } + } +} + +impl CursorState { + pub fn update(&mut self, elapsed: Duration) { + let total_cycles = elapsed.as_nanos() / CURSOR_BLINK_DURATION.as_nanos(); + let remaining = Duration::from_nanos( + u64::try_from(elapsed.as_nanos() % CURSOR_BLINK_DURATION.as_nanos()) + .expect("remainder fits in u64"), + ); + // If we have an odd number of totaal cycles, flip the visibility. + if total_cycles & 1 == 1 { + self.visible = !self.visible; + } + + if let Some(remaining) = self.remaining_until_blink.checked_sub(remaining) { + self.remaining_until_blink = remaining; + } else { + self.visible = !self.visible; + self.remaining_until_blink = + CURSOR_BLINK_DURATION - (remaining - self.remaining_until_blink); + } + } + + pub fn force_on(&mut self) { + self.visible = true; + self.remaining_until_blink = CURSOR_BLINK_DURATION; + } +}