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:
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user