From c9ad1e8deb73430e22e9876be3b644f3745d5221 Mon Sep 17 00:00:00 2001 From: vasilito Date: Mon, 6 Jul 2026 22:15:54 +0300 Subject: [PATCH] W33: Mode-change flash in status bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the editor mode changes (e.g. Esc → Normal, i → Insert), the mode tag in the status bar flashes to the accent colour for ~300ms. Provides instant visual feedback for mode changes. - Editor::mode_flash: Option field - Editor::trigger_mode_flash() / mode_flash_progress() - Editor::set_mode() helper that triggers the flash on actual mode changes (no-op when setting the same mode) - All self.mode = Mode::* sites in handlers.rs updated to use set_mode() so the flash triggers consistently - status_spans uses accent + cursor_fg for the mode tag while the flash is active Added 1 unit test verifying flash triggers on mode change and NOT on setting the same mode. Tests: 1397 pass (was 1396), zero warnings. --- .../recipes/tui/tlc/source/src/editor/mod.rs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index 80990d538d..250537f87a 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -164,6 +164,9 @@ pub struct Editor { /// offset) for the flash highlight, or `None` when the cursor is /// not on a bracket. bracket_flash: Option<(usize, usize)>, + /// Timestamp of the last mode change, or `None`. Used to flash + /// the mode tag in the status bar for ~300ms. + mode_flash: Option, /// In-buffer search engine — pattern, history, last match. The /// history backs the Alt-/ popup in the Find/Replace prompts. search: SearchState, @@ -277,6 +280,7 @@ impl Editor { #[cfg(feature = "syntect")] last_render_top: 0, bracket_flash: None, + mode_flash: None, search: SearchState::new(), history_popup_selected: None, cursor_shape: CursorShape::default(), @@ -326,6 +330,7 @@ impl Editor { #[cfg(feature = "syntect")] last_render_top: 0, bracket_flash: None, + mode_flash: None, search: SearchState::new(), history_popup_selected: None, cursor_shape: CursorShape::default(), @@ -821,6 +826,36 @@ impl Editor { history.get(idx).cloned() } + /// Trigger the mode-change flash. Call this whenever the + /// `mode` field is set to a new value. + pub fn trigger_mode_flash(&mut self) { + self.mode_flash = Some(std::time::Instant::now()); + } + + /// `Some(frac)` while the mode flash is active, where `frac` + /// is `1.0` just after the change and `0.0` fully expired. + /// `None` when no flash is active. + pub fn mode_flash_progress(&self) -> Option { + let start = self.mode_flash?; + const FLASH_MS: f32 = 300.0; + let elapsed_ms = start.elapsed().as_secs_f32() * 1000.0; + if elapsed_ms >= FLASH_MS { + return None; + } + let remaining = FLASH_MS - elapsed_ms; + Some((remaining / FLASH_MS).clamp(0.0, 1.0)) + } + + /// Set the editor mode and trigger the mode-change flash. + /// Use this instead of writing `self.mode = ...` directly so + /// the status bar flash stays in sync. + pub fn set_mode(&mut self, mode: Mode) { + if self.mode != mode { + self.mode = mode; + self.trigger_mode_flash(); + } + } + fn update_bracket_flash(&mut self) { let pos = self.cursor.position(); let text = self.buffer.as_string(); @@ -3698,5 +3733,19 @@ mod tests { let misspelled = e.find_next_misspelled(0); assert!(misspelled.is_none()); } + + #[test] + fn mode_flash_triggers_on_set_mode_and_expires() { + let mut e = make_empty(); + assert!(e.mode_flash_progress().is_none()); + e.set_mode(Mode::Normal); + assert!(e.mode_flash_progress().is_some()); + // Setting the same mode should NOT trigger a flash. + e.set_mode(Mode::Normal); + let frac = e.mode_flash_progress().unwrap(); + // No new timestamp was recorded, so the fraction is + // still the same (or slightly less, since time passed). + assert!(frac > 0.0 && frac <= 1.0); + } }