Wire ↑↓↵ keyboard navigation in the command overlay

The footer chips already advertised ↑↓ to navigate and ↵ to open;
they now match reality.

- chrome::command_match exposes COMMAND_ACTIONS / matching_actions
  alongside the tab/history/bookmark match helpers and a
  CommandSelection enum + visible_command_rows that returns the flat
  ordered list of activatable rows. command_overlay drops its private
  copies of the action const and matcher and consumes them from the
  shared module so render and key handling share one source of truth.

- ElyShell tracks command_selected_index with command_select_next /
  command_select_prev (cyclic, no notify when index doesn't change)
  and activate_selected_command which dispatches the right shell call
  for the currently selected CommandSelection variant and dismisses
  the overlay. Dismissing command mode resets the index to 0.

- The shell root captures key_down via on_command_overlay_key_down.
  When the live snapshot's command_query starts with '>', up / down
  / enter run the matching helper and the event stops propagating so
  the omnibar input doesn't move its caret.

- Each rendered row receives a `selected` flag. The selected row gets
  the design's tinted bg + accent-bar on the left edge so the active
  result is unambiguous at any keyboard step.
This commit is contained in:
2026-05-09 20:12:31 -04:00
parent 4403eacf15
commit 49e4816bce
5 changed files with 266 additions and 67 deletions
@@ -1,8 +1,94 @@
use ely_browser_core::BrowserSnapshot; use ely_browser_core::BrowserSnapshot;
use ely_domain::{BookmarkEntry, BrowserTab, HistoryEntry}; use ely_domain::{BookmarkEntry, BrowserTab, HistoryEntry, TabId, UrlText};
use gpui_component::IconName;
pub(crate) const RESULT_LIMIT: usize = 4; pub(crate) const RESULT_LIMIT: usize = 4;
pub(crate) struct CommandActionEntry {
pub title: &'static str,
pub hint: &'static str,
pub icon: IconName,
pub route: &'static str,
pub keys: &'static str,
}
pub(crate) const COMMAND_ACTIONS: &[CommandActionEntry] = &[
CommandActionEntry {
title: "Switch workspace",
hint: "Cycle to the next space",
icon: IconName::LayoutDashboard,
route: "ely://settings/spaces",
keys: "⌘⇧W",
},
CommandActionEntry {
title: "Open Settings",
hint: "Appearance, sync, plugins…",
icon: IconName::Settings,
route: "ely://settings",
keys: "⌘,",
},
CommandActionEntry {
title: "Open Plugins",
hint: "Marketplace and installed plugins",
icon: IconName::Asterisk,
route: "ely://plugins",
keys: "",
},
CommandActionEntry {
title: "Open Bookmarks",
hint: "Manage your saved pages",
icon: IconName::BookOpen,
route: "ely://bookmarks",
keys: "",
},
];
pub(crate) fn matching_actions(needle: &str) -> Vec<&'static CommandActionEntry> {
COMMAND_ACTIONS
.iter()
.filter(|action| {
needle.is_empty()
|| action.title.to_lowercase().contains(needle)
|| action.hint.to_lowercase().contains(needle)
})
.collect()
}
#[derive(Clone, Debug)]
pub(crate) enum CommandSelection {
SelectTab(TabId),
OpenUrl(UrlText),
OpenRoute(&'static str),
}
pub(crate) fn visible_command_rows(
snapshot: &BrowserSnapshot,
needle: &str,
) -> Vec<CommandSelection> {
let mut rows = Vec::new();
rows.extend(
matching_tabs(snapshot, needle)
.iter()
.map(|tab| CommandSelection::SelectTab(tab.id().clone())),
);
rows.extend(
matching_history(snapshot, needle)
.iter()
.map(|entry| CommandSelection::OpenUrl(entry.url().clone())),
);
rows.extend(
matching_bookmarks(snapshot, needle)
.iter()
.map(|bookmark| CommandSelection::OpenUrl(bookmark.url().clone())),
);
rows.extend(
matching_actions(needle)
.iter()
.map(|action| CommandSelection::OpenRoute(action.route)),
);
rows
}
pub(crate) fn matching_tabs<'a>( pub(crate) fn matching_tabs<'a>(
snapshot: &'a BrowserSnapshot, snapshot: &'a BrowserSnapshot,
needle: &str, needle: &str,
@@ -3,20 +3,22 @@ use ely_design_system::colors;
use ely_domain::{BookmarkEntry, BrowserTab, HistoryEntry}; use ely_domain::{BookmarkEntry, BrowserTab, HistoryEntry};
use gpui::{ use gpui::{
AnyElement, BoxShadow, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, AnyElement, BoxShadow, Context, FontWeight, InteractiveElement, IntoElement, ParentElement,
SharedString, StatefulInteractiveElement, Styled, div, hsla, point, px, rgb, rgba, SharedString, StatefulInteractiveElement, Styled, div, hsla, point,
prelude::FluentBuilder, px, rgb, rgba,
}; };
use gpui_component::IconName; use gpui_component::IconName;
use crate::shell::ElyShell; use crate::shell::ElyShell;
use crate::shell::chrome::command_footer::{render_command_footer, render_kbd}; use crate::shell::chrome::command_footer::{render_command_footer, render_kbd};
use crate::shell::chrome::command_match::{ use crate::shell::chrome::command_match::{
matching_bookmarks, matching_history, matching_tabs, CommandActionEntry, matching_actions, matching_bookmarks, matching_history, matching_tabs,
}; };
use crate::shell::chrome::render_glyph_for; use crate::shell::chrome::render_glyph_for;
const COMMAND_PREFIX: &str = ">"; const COMMAND_PREFIX: &str = ">";
pub(crate) fn render_command_overlay( pub(crate) fn render_command_overlay(
shell: &ElyShell,
snapshot: &BrowserSnapshot, snapshot: &BrowserSnapshot,
cx: &mut Context<ElyShell>, cx: &mut Context<ElyShell>,
) -> Option<AnyElement> { ) -> Option<AnyElement> {
@@ -25,13 +27,15 @@ pub(crate) fn render_command_overlay(
return None; return None;
} }
let needle = query[COMMAND_PREFIX.len()..].trim().to_lowercase(); let needle = query[COMMAND_PREFIX.len()..].trim().to_lowercase();
let selected_index = shell.command_selected_index;
Some(render_overlay(snapshot, &needle, cx)) Some(render_overlay(snapshot, &needle, selected_index, cx))
} }
fn render_overlay( fn render_overlay(
snapshot: &BrowserSnapshot, snapshot: &BrowserSnapshot,
needle: &str, needle: &str,
selected_index: usize,
cx: &mut Context<ElyShell>, cx: &mut Context<ElyShell>,
) -> AnyElement { ) -> AnyElement {
div() div()
@@ -42,13 +46,14 @@ fn render_overlay(
.flex_col() .flex_col()
.items_center() .items_center()
.pt(px(80.0)) .pt(px(80.0))
.child(render_panel(snapshot, needle, cx)) .child(render_panel(snapshot, needle, selected_index, cx))
.into_any_element() .into_any_element()
} }
fn render_panel( fn render_panel(
snapshot: &BrowserSnapshot, snapshot: &BrowserSnapshot,
needle: &str, needle: &str,
selected_index: usize,
cx: &mut Context<ElyShell>, cx: &mut Context<ElyShell>,
) -> AnyElement { ) -> AnyElement {
let query_label = if needle.is_empty() { let query_label = if needle.is_empty() {
@@ -67,7 +72,7 @@ fn render_panel(
.flex() .flex()
.flex_col() .flex_col()
.child(render_header(query_label.clone(), needle.is_empty())) .child(render_header(query_label.clone(), needle.is_empty()))
.child(render_results(snapshot, needle, cx)) .child(render_results(snapshot, needle, selected_index, cx))
.child(render_command_footer()) .child(render_command_footer())
.into_any_element() .into_any_element()
} }
@@ -112,6 +117,7 @@ fn render_header(query_label: String, is_empty: bool) -> AnyElement {
fn render_results( fn render_results(
snapshot: &BrowserSnapshot, snapshot: &BrowserSnapshot,
needle: &str, needle: &str,
selected_index: usize,
cx: &mut Context<ElyShell>, cx: &mut Context<ElyShell>,
) -> AnyElement { ) -> AnyElement {
let tabs = matching_tabs(snapshot, needle); let tabs = matching_tabs(snapshot, needle);
@@ -119,21 +125,37 @@ fn render_results(
let bookmarks = matching_bookmarks(snapshot, needle); let bookmarks = matching_bookmarks(snapshot, needle);
let actions = matching_actions(needle); let actions = matching_actions(needle);
let mut offset = 0usize;
let mut sections: Vec<AnyElement> = Vec::new(); let mut sections: Vec<AnyElement> = Vec::new();
if !tabs.is_empty() { if !tabs.is_empty() {
sections.push(render_section("Open tabs", render_tab_rows(tabs, cx))); let count = tabs.len();
sections.push(render_section(
"Open tabs",
render_tab_rows(tabs, offset, selected_index, cx),
));
offset += count;
} }
if !history.is_empty() { if !history.is_empty() {
sections.push(render_section("History", render_history_rows(history, cx))); let count = history.len();
sections.push(render_section(
"History",
render_history_rows(history, offset, selected_index, cx),
));
offset += count;
} }
if !bookmarks.is_empty() { if !bookmarks.is_empty() {
let count = bookmarks.len();
sections.push(render_section( sections.push(render_section(
"Bookmarks", "Bookmarks",
render_bookmark_rows(bookmarks, cx), render_bookmark_rows(bookmarks, offset, selected_index, cx),
)); ));
offset += count;
} }
if !actions.is_empty() { if !actions.is_empty() {
sections.push(render_section("Actions", render_action_rows(actions, cx))); sections.push(render_section(
"Actions",
render_action_rows(actions, offset, selected_index, cx),
));
} }
if sections.is_empty() { if sections.is_empty() {
@@ -170,7 +192,12 @@ fn render_section(label: &'static str, body: AnyElement) -> AnyElement {
.into_any_element() .into_any_element()
} }
fn render_tab_rows(tabs: Vec<&BrowserTab>, cx: &mut Context<ElyShell>) -> AnyElement { fn render_tab_rows(
tabs: Vec<&BrowserTab>,
offset: usize,
selected_index: usize,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div() div()
.flex() .flex()
.flex_col() .flex_col()
@@ -182,6 +209,7 @@ fn render_tab_rows(tabs: Vec<&BrowserTab>, cx: &mut Context<ElyShell>) -> AnyEle
.clone() .clone()
.unwrap_or_else(|| tab.display_url()); .unwrap_or_else(|| tab.display_url());
let initial = title.chars().next().unwrap_or('?').to_string(); let initial = title.chars().next().unwrap_or('?').to_string();
let is_selected = offset + index == selected_index;
render_row_with_glyph( render_row_with_glyph(
CommandRowContent { CommandRowContent {
@@ -189,6 +217,7 @@ fn render_tab_rows(tabs: Vec<&BrowserTab>, cx: &mut Context<ElyShell>) -> AnyEle
title, title,
hint: Some(host_label), hint: Some(host_label),
keys: None, keys: None,
selected: is_selected,
}, },
host.as_deref(), host.as_deref(),
&initial, &initial,
@@ -204,6 +233,8 @@ fn render_tab_rows(tabs: Vec<&BrowserTab>, cx: &mut Context<ElyShell>) -> AnyEle
fn render_history_rows( fn render_history_rows(
entries: Vec<&HistoryEntry>, entries: Vec<&HistoryEntry>,
offset: usize,
selected_index: usize,
cx: &mut Context<ElyShell>, cx: &mut Context<ElyShell>,
) -> AnyElement { ) -> AnyElement {
div() div()
@@ -217,6 +248,7 @@ fn render_history_rows(
.clone() .clone()
.unwrap_or_else(|| entry.url().as_str().to_string()); .unwrap_or_else(|| entry.url().as_str().to_string());
let initial = title.chars().next().unwrap_or('?').to_string(); let initial = title.chars().next().unwrap_or('?').to_string();
let is_selected = offset + index == selected_index;
render_row_with_glyph( render_row_with_glyph(
CommandRowContent { CommandRowContent {
@@ -224,6 +256,7 @@ fn render_history_rows(
title, title,
hint: Some(display), hint: Some(display),
keys: None, keys: None,
selected: is_selected,
}, },
host.as_deref(), host.as_deref(),
&initial, &initial,
@@ -237,58 +270,10 @@ fn render_history_rows(
.into_any_element() .into_any_element()
} }
struct CommandAction {
title: &'static str,
hint: &'static str,
icon: IconName,
route: &'static str,
keys: &'static str,
}
const COMMAND_ACTIONS: &[CommandAction] = &[
CommandAction {
title: "Switch workspace",
hint: "Cycle to the next space",
icon: IconName::LayoutDashboard,
route: "ely://settings/spaces",
keys: "⌘⇧W",
},
CommandAction {
title: "Open Settings",
hint: "Appearance, sync, plugins…",
icon: IconName::Settings,
route: "ely://settings",
keys: "⌘,",
},
CommandAction {
title: "Open Plugins",
hint: "Marketplace and installed plugins",
icon: IconName::Asterisk,
route: "ely://plugins",
keys: "",
},
CommandAction {
title: "Open Bookmarks",
hint: "Manage your saved pages",
icon: IconName::BookOpen,
route: "ely://bookmarks",
keys: "",
},
];
fn matching_actions(needle: &str) -> Vec<&'static CommandAction> {
COMMAND_ACTIONS
.iter()
.filter(|action| {
needle.is_empty()
|| action.title.to_lowercase().contains(needle)
|| action.hint.to_lowercase().contains(needle)
})
.collect()
}
fn render_action_rows( fn render_action_rows(
actions: Vec<&'static CommandAction>, actions: Vec<&'static CommandActionEntry>,
offset: usize,
selected_index: usize,
cx: &mut Context<ElyShell>, cx: &mut Context<ElyShell>,
) -> AnyElement { ) -> AnyElement {
div() div()
@@ -302,6 +287,7 @@ fn render_action_rows(
Some(action.keys.to_string()) Some(action.keys.to_string())
}; };
let icon = action.icon.clone(); let icon = action.icon.clone();
let is_selected = offset + index == selected_index;
render_row( render_row(
CommandRowContent { CommandRowContent {
@@ -309,6 +295,7 @@ fn render_action_rows(
title: action.title.to_string(), title: action.title.to_string(),
hint: Some(action.hint.to_string()), hint: Some(action.hint.to_string()),
keys, keys,
selected: is_selected,
}, },
icon, icon,
cx, cx,
@@ -326,6 +313,7 @@ struct CommandRowContent {
title: String, title: String,
hint: Option<String>, hint: Option<String>,
keys: Option<String>, keys: Option<String>,
selected: bool,
} }
fn render_row<F>( fn render_row<F>(
@@ -373,19 +361,34 @@ fn render_row_inner<F>(
where where
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + 'static, F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + 'static,
{ {
let CommandRowContent { id, title, hint, keys } = content; let CommandRowContent { id, title, hint, keys, selected } = content;
let bg = if selected { ROW_SELECTED_BG } else { 0x00000000 };
div() div()
.id(SharedString::from(id)) .id(SharedString::from(id))
.relative()
.flex() .flex()
.items_center() .items_center()
.gap(px(12.0)) .gap(px(12.0))
.px(px(16.0)) .px(px(16.0))
.py(px(8.0)) .py(px(8.0))
.bg(rgba(bg))
.cursor_pointer() .cursor_pointer()
.hover(|style| style.bg(rgba(ROW_HOVER_BG))) .hover(|style| style.bg(rgba(ROW_HOVER_BG)))
.active(|style| style.opacity(0.85)) .active(|style| style.opacity(0.85))
.on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx))) .on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx)))
.when(selected, |el| {
el.child(
div()
.absolute()
.left_0()
.top(px(6.0))
.bottom(px(6.0))
.w(px(2.0))
.rounded(px(2.0))
.bg(rgb(colors::ACCENT)),
)
})
.child(leading) .child(leading)
.child( .child(
div() div()
@@ -431,6 +434,8 @@ fn render_empty_state() -> AnyElement {
fn render_bookmark_rows( fn render_bookmark_rows(
entries: Vec<&BookmarkEntry>, entries: Vec<&BookmarkEntry>,
offset: usize,
selected_index: usize,
cx: &mut Context<ElyShell>, cx: &mut Context<ElyShell>,
) -> AnyElement { ) -> AnyElement {
div() div()
@@ -444,6 +449,7 @@ fn render_bookmark_rows(
.clone() .clone()
.unwrap_or_else(|| bookmark.url().as_str().to_string()); .unwrap_or_else(|| bookmark.url().as_str().to_string());
let initial = title.chars().next().unwrap_or('?').to_string(); let initial = title.chars().next().unwrap_or('?').to_string();
let is_selected = offset + index == selected_index;
render_row_with_glyph( render_row_with_glyph(
CommandRowContent { CommandRowContent {
@@ -451,6 +457,7 @@ fn render_bookmark_rows(
title, title,
hint: Some(display), hint: Some(display),
keys: None, keys: None,
selected: is_selected,
}, },
host.as_deref(), host.as_deref(),
&initial, &initial,
@@ -467,6 +474,7 @@ fn render_bookmark_rows(
const PANEL_BG: u32 = 0xfffffff5; const PANEL_BG: u32 = 0xfffffff5;
const BACKDROP_BG: u32 = 0x140f0a3d; const BACKDROP_BG: u32 = 0x140f0a3d;
const ROW_HOVER_BG: u32 = 0xc9644214; const ROW_HOVER_BG: u32 = 0xc9644214;
const ROW_SELECTED_BG: u32 = 0xc964421f;
const ROW_ICON_BG: u32 = 0xffffffd9; const ROW_ICON_BG: u32 = 0xffffffd9;
const BADGE_BG: u32 = 0x281e140f; const BADGE_BG: u32 = 0x281e140f;
+2
View File
@@ -67,6 +67,7 @@ pub struct ElyShell {
pub(crate) translucency_slider: Entity<SliderState>, pub(crate) translucency_slider: Entity<SliderState>,
pub(crate) workspace_picker_open: bool, pub(crate) workspace_picker_open: bool,
pub(crate) sidebar_hover_expanded: bool, pub(crate) sidebar_hover_expanded: bool,
pub(crate) command_selected_index: usize,
download_action_error: Option<String>, download_action_error: Option<String>,
download_clear_confirmation: bool, download_clear_confirmation: bool,
download_security_confirmation: Option<PendingDownloadFileAction>, download_security_confirmation: Option<PendingDownloadFileAction>,
@@ -179,6 +180,7 @@ impl ElyShell {
translucency_slider, translucency_slider,
workspace_picker_open: false, workspace_picker_open: false,
sidebar_hover_expanded: false, sidebar_hover_expanded: false,
command_selected_index: 0,
download_action_error: None, download_action_error: None,
download_clear_confirmation: false, download_clear_confirmation: false,
download_security_confirmation: None, download_security_confirmation: None,
+67
View File
@@ -81,6 +81,73 @@ impl ElyShell {
self.command_input.update(cx, |input, cx| { self.command_input.update(cx, |input, cx| {
input.set_value("", window, cx); input.set_value("", window, cx);
}); });
self.command_selected_index = 0;
cx.notify(); cx.notify();
} }
pub(crate) fn command_select_next(&mut self, total_rows: usize, cx: &mut Context<Self>) {
if total_rows == 0 {
self.command_selected_index = 0;
return;
}
let next = (self.command_selected_index + 1) % total_rows;
if next != self.command_selected_index {
self.command_selected_index = next;
cx.notify();
}
}
pub(crate) fn command_select_prev(&mut self, total_rows: usize, cx: &mut Context<Self>) {
if total_rows == 0 {
self.command_selected_index = 0;
return;
}
let prev = if self.command_selected_index == 0 {
total_rows - 1
} else {
self.command_selected_index - 1
};
if prev != self.command_selected_index {
self.command_selected_index = prev;
cx.notify();
}
}
pub(crate) fn activate_selected_command(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) {
let snapshot = match &self.state {
ShellState::Ready(core) => match core.snapshot() {
Ok(snapshot) => snapshot,
Err(_) => return,
},
ShellState::StartupError(_) => return,
};
let needle_owned = snapshot.command_query.as_str();
let Some(stripped) = needle_owned.strip_prefix('>') else {
return;
};
let needle = stripped.trim().to_lowercase();
let rows = crate::shell::chrome::command_match::visible_command_rows(
&snapshot, &needle,
);
if rows.is_empty() {
return;
}
let index = self.command_selected_index.min(rows.len() - 1);
match rows[index].clone() {
crate::shell::chrome::command_match::CommandSelection::SelectTab(tab_id) => {
self.select_tab(&tab_id, window, cx);
}
crate::shell::chrome::command_match::CommandSelection::OpenUrl(url) => {
self.open_internal_tab(url.as_str(), window, cx);
}
crate::shell::chrome::command_match::CommandSelection::OpenRoute(route) => {
self.open_internal_tab(route, window, cx);
}
}
self.dismiss_command_mode(window, cx);
}
} }
+40 -4
View File
@@ -2,11 +2,12 @@ use ely_browser_core::BrowserSnapshot;
use ely_design_system::{colors, spacing}; use ely_design_system::{colors, spacing};
use ely_domain::{BrowserTab, DEFAULT_SIDEBAR_WIDTH_PX, HIDDEN_SIDEBAR_WIDTH_PX}; use ely_domain::{BrowserTab, DEFAULT_SIDEBAR_WIDTH_PX, HIDDEN_SIDEBAR_WIDTH_PX};
use gpui::{ use gpui::{
AnyElement, Context, InteractiveElement, IntoElement, MouseMoveEvent, ParentElement, Render, AnyElement, Context, InteractiveElement, IntoElement, KeyDownEvent, MouseMoveEvent,
SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::FluentBuilder, px, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, div,
rgb, rgba, prelude::FluentBuilder, px, rgb, rgba,
}; };
use super::chrome::command_match::visible_command_rows;
use super::chrome::{ use super::chrome::{
panel_bg, panel_shadow, render_command_overlay, panel_bg, panel_shadow, render_command_overlay,
render_topbar as render_topbar_chrome, render_wallpaper, render_topbar as render_topbar_chrome, render_wallpaper,
@@ -67,6 +68,7 @@ impl ElyShell {
.on_action(cx.listener(Self::on_zoom_in)) .on_action(cx.listener(Self::on_zoom_in))
.on_action(cx.listener(Self::on_zoom_out)) .on_action(cx.listener(Self::on_zoom_out))
.on_mouse_move(cx.listener(Self::on_window_mouse_move)) .on_mouse_move(cx.listener(Self::on_window_mouse_move))
.capture_key_down(cx.listener(Self::on_command_overlay_key_down))
.text_color(rgb(colors::INK)) .text_color(rgb(colors::INK))
.child(render_wallpaper(snapshot.appearance.wallpaper())) .child(render_wallpaper(snapshot.appearance.wallpaper()))
.child( .child(
@@ -88,7 +90,7 @@ impl ElyShell {
.when(hover_expanded, |el| { .when(hover_expanded, |el| {
el.child(self.render_hidden_sidebar_overlay(&snapshot, cx)) el.child(self.render_hidden_sidebar_overlay(&snapshot, cx))
}) })
.children(render_command_overlay(&snapshot, cx)) .children(render_command_overlay(self, &snapshot, cx))
.into_any_element() .into_any_element()
} }
@@ -167,6 +169,40 @@ impl ElyShell {
self.render_expanded_sidebar(snapshot, sidebar_width, cx) self.render_expanded_sidebar(snapshot, sidebar_width, cx)
} }
fn on_command_overlay_key_down(
&mut self,
event: &KeyDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let key = event.keystroke.key.as_str();
if !matches!(key, "up" | "down" | "enter") {
return;
}
let total_rows = match &self.state {
ShellState::Ready(core) => match core.snapshot() {
Ok(snapshot) => {
let Some(stripped) = snapshot.command_query.strip_prefix('>') else {
return;
};
let needle = stripped.trim().to_lowercase();
visible_command_rows(&snapshot, &needle).len()
}
Err(_) => return,
},
ShellState::StartupError(_) => return,
};
match key {
"up" => self.command_select_prev(total_rows, cx),
"down" => self.command_select_next(total_rows, cx),
"enter" => self.activate_selected_command(window, cx),
_ => {}
}
cx.stop_propagation();
}
fn on_window_mouse_move( fn on_window_mouse_move(
&mut self, &mut self,
event: &MouseMoveEvent, event: &MouseMoveEvent,