diff --git a/crates/edit/src/bin/edit/main.rs b/crates/edit/src/bin/edit/main.rs index 30ad149b20b..96914f6c440 100644 --- a/crates/edit/src/bin/edit/main.rs +++ b/crates/edit/src/bin/edit/main.rs @@ -554,7 +554,7 @@ impl Drop for RestoreModes { // Same as in the beginning but in the reverse order. // It also includes DECSCUSR 0 to reset the cursor style and DECTCEM to show the cursor. // We specifically don't reset mode 1036, because most applications expect it to be set nowadays. - sys::write_stdout("\x1b[0 q\x1b[?25h\x1b]0;\x07\x1b[?1002;1006;2004l\x1b[?1049l"); + sys::write_stdout("\x1b[0 q\x1b[?25h\x1b]0;\x07\x1b[?1003;1006;2004l\x1b[?1049l"); } } @@ -563,11 +563,11 @@ fn setup_terminal(tui: &mut Tui, state: &mut State, vt_parser: &mut vt::Parser) // 1049: Alternative Screen Buffer // I put the ASB switch in the beginning, just in case the terminal performs // some additional state tracking beyond the modes we enable/disable. - // 1002: Cell Motion Mouse Tracking + // 1003: Any Event Mouse Tracking // 1006: SGR Mouse Mode // 2004: Bracketed Paste Mode // 1036: Xterm: "meta sends escape" (Alt keypresses should be encoded with ESC + char) - "\x1b[?1049h\x1b[?1002;1006;2004h\x1b[?1036h", + "\x1b[?1049h\x1b[?1003;1006;2004h\x1b[?1036h", // OSC 4 color table requests for indices 0 through 15 (base colors). "\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?\x07", "\x1b]4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07", diff --git a/crates/edit/src/tui.rs b/crates/edit/src/tui.rs index 314cdc30cc6..3b7cabfe317 100644 --- a/crates/edit/src/tui.rs +++ b/crates/edit/src/tui.rs @@ -348,6 +348,10 @@ pub struct Tui { mouse_click_counter: CoordType, /// The path to the node that was clicked on. mouse_down_node_path: Vec, + /// Path to the node currently under the mouse cursor. + hovered_node_path: Vec, + /// This is check mouse hover enabled + mouse_hover_enabled: bool, /// The position of the first click in a double/triple click series. first_click_position: Point, /// The node ID of the node that was first clicked on @@ -407,6 +411,8 @@ impl Tui { mouse_is_drag: false, mouse_click_counter: 0, mouse_down_node_path: Vec::with_capacity(16), + hovered_node_path: Vec::with_capacity(16), + mouse_hover_enabled: true, first_click_position: Point::MIN, first_click_target: 0, @@ -422,10 +428,16 @@ impl Tui { read_timeout: time::Duration::MAX, }; Self::clean_node_path(&mut tui.mouse_down_node_path); + Self::clean_node_path(&mut tui.hovered_node_path); Self::clean_node_path(&mut tui.focused_node_path); Ok(tui) } + // Helper to check if a node is in the hover path + fn is_subtree_hovered(&self, node: &Node) -> bool { + self.hovered_node_path.get(node.depth) == Some(&node.id) + } + /// Sets up the framebuffer's color palette. pub fn setup_indexed_colors(&mut self, colors: [StraightRgba; INDEXED_COLORS_COUNT]) { self.framebuffer.set_indexed_colors(colors); @@ -548,6 +560,7 @@ impl Tui { self.size = resize; } Some(Input::Text(text)) => { + self.mouse_hover_enabled = false; input_text = Some(text); // TODO: the .len()==1 check causes us to ignore keyboard inputs that are faster than we process them. // For instance, imagine the user presses "A" twice and we happen to read it in a single chunk. @@ -560,15 +573,18 @@ impl Tui { } } Some(Input::Paste(paste)) => { + self.mouse_hover_enabled = false; let clipboard = self.clipboard_mut(); clipboard.write(paste); clipboard.mark_as_synchronized(); input_keyboard = Some(kbmod::CTRL | vk::V); } Some(Input::Keyboard(keyboard)) => { + self.mouse_hover_enabled = false; input_keyboard = Some(keyboard); } Some(Input::Mouse(mouse)) => { + self.mouse_hover_enabled = true; let mut next_state = mouse.state; let next_position = mouse.position; let next_scroll = mouse.scroll; @@ -583,36 +599,35 @@ impl Tui { let mut hovered_node = None; // Needed for `mouse_down` let mut focused_node = None; // Needed for `mouse_down` and `is_click` - if mouse_down || mouse_up { - // Roots (aka windows) are ordered in Z order, so we iterate - // them in reverse order, from topmost to bottommost. - for root in self.prev_tree.iterate_roots_rev() { - // Find the node that contains the cursor. - Tree::visit_all(root, root, true, |node| { - let n = node.borrow(); - if !n.outer_clipped.contains(next_position) { - // Skip the entire sub-tree, because it doesn't contain the cursor. - return VisitControl::SkipChildren; - } - hovered_node = Some(node); - if n.attributes.focusable { - focused_node = Some(node); - } - VisitControl::Continue - }); - // This root/window contains the cursor. - // We don't care about any lower roots. - if hovered_node.is_some() { - break; + // Calculate the hovered node for ALL mouse events, not just clicks. + // Roots (aka windows) are ordered in Z order... + for root in self.prev_tree.iterate_roots_rev() { + Tree::visit_all(root, root, true, |node| { + let n = node.borrow(); + if !n.outer_clipped.contains(next_position) { + return VisitControl::SkipChildren; } - - // This root is modal and swallows all clicks, - // no matter whether the click was inside it or not. - if matches!(root.borrow().content, NodeContent::Modal(_)) { - break; + hovered_node = Some(node); + if n.attributes.focusable { + focused_node = Some(node); } + VisitControl::Continue + }); + if hovered_node.is_some() { + break; } + if matches!(root.borrow().content, NodeContent::Modal(_)) { + break; + } + } + + // Update the hovered path and trigger redraw if it changed + let mut next_hovered_path = Vec::with_capacity(16); + Self::build_node_path(hovered_node, &mut next_hovered_path); + if self.hovered_node_path != next_hovered_path { + self.hovered_node_path = next_hovered_path; + self.needs_more_settling(); } if is_scroll { @@ -3165,15 +3180,25 @@ impl<'a> Context<'a, '_> { && !contains_focus && self.consume_shortcut(kbmod::ALT | InputKey::new(accelerator as u32)); - if contains_focus || keyboard_focus { + // If the menubar is already active (another menu is open) and we are hovered, + // steal the focus to open this menu automatically. + if !contains_focus && self.is_hovered() { + let menubar_active = self.tui.is_subtree_focused(&self.tree.current_node.borrow()); + if menubar_active { + self.steal_focus(); + } + } + + let is_highlighted = self.is_focused() || self.is_hovered(); + if is_highlighted { + self.attr_background_rgba(self.indexed(IndexedColor::Green)); + self.attr_foreground_rgba(self.contrasted(self.indexed(IndexedColor::Green))); + } else if contains_focus || keyboard_focus { self.attr_background_rgba(self.tui.floater_default_bg); self.attr_foreground_rgba(self.tui.floater_default_fg); + } - if self.is_focused() { - self.attr_background_rgba(self.indexed(IndexedColor::Green)); - self.attr_foreground_rgba(self.contrasted(self.indexed(IndexedColor::Green))); - } - + if contains_focus || keyboard_focus { self.next_block_id_mixin(mixin); self.table_begin("flyout"); self.attr_float(FloatSpec { @@ -3186,6 +3211,9 @@ impl<'a> Context<'a, '_> { self.attr_border(); self.attr_focus_well(); + self.attr_background_rgba(self.tui.floater_default_bg); + self.attr_foreground_rgba(self.tui.floater_default_fg); + if keyboard_focus { self.steal_focus(); } @@ -3223,7 +3251,7 @@ impl<'a> Context<'a, '_> { self.inherit_focus(); } - if self.is_focused() { + if self.is_focused() || self.is_hovered() { self.attr_background_rgba(self.indexed(IndexedColor::Green)); self.attr_foreground_rgba(self.contrasted(self.indexed(IndexedColor::Green))); } @@ -3280,6 +3308,15 @@ impl<'a> Context<'a, '_> { self.table_end(); } + /// Returns whether the current node is hovered. + pub fn is_hovered(&mut self) -> bool { + if !self.tui.mouse_hover_enabled { + return false; + } + let last_node = self.tree.last_node.borrow(); + self.tui.is_subtree_hovered(&last_node) + } + /// Renders a button label with an optional accelerator character /// May also renders a checkbox or square brackets for inline buttons fn button_label(&mut self, classname: &'static str, text: &str, style: ButtonStyle) {