W33: Mode-change flash in status bar
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<Instant> 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.
This commit is contained in:
@@ -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<std::time::Instant>,
|
||||
/// 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<f32> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user