mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 03:46:50 -06:00
#707: search every command, ranked; a highlight for Enter; recents from the palette only
- A search covers every command again (the engine's and the UI's own), grouped by menu where one lists it and under Other otherwise, best matches first (prefix, then shortest), at most 60. Without a query the menus' items show as before. - Up and Down move a visible highlight (the pointer moves it too); Enter runs the highlighted row, not an invisible first one. - Only runs from the palette are kept as Recents; menus, shortcuts and context menus no longer write the preferences. Recents match the translated label. - The Plugins menu's items (run last, manage, import) show with the plugins. - The search keeps its hint, so every catalog keeps its translation. - The panel sits above the toolbar pill's own rect (toolbar::pill_rect); one set_palette/toggle_palette opens it from every entry point. - Clippy: needless borrows in the toolbar.
This commit is contained in:
@@ -1133,6 +1133,7 @@ status = "draft — needs native review"
|
||||
"Scroll with parent" = "Posouvat s nadřazeným prvkem"
|
||||
"Search plugins" = "Hledat pluginy"
|
||||
"Search" = "Hledat"
|
||||
"Search actions, layers and tools" = "Hledat akce, vrstvy a nástroje"
|
||||
"Search assets" = "Hledat zdroje"
|
||||
"Search comments" = "Hledat komentáře"
|
||||
"Search fonts" = "Hledat písma"
|
||||
|
||||
@@ -1096,6 +1096,7 @@ status = "draft — needs native review"
|
||||
"Scroll to layer" = "Zur Ebene scrollen"
|
||||
"Scroll with parent" = "Mit übergeordnetem Element scrollen"
|
||||
"Search" = "Suchen"
|
||||
"Search actions, layers and tools" = "Aktionen, Ebenen und Werkzeuge suchen"
|
||||
"Search assets" = "Assets suchen"
|
||||
"Search comments" = "Kommentare suchen"
|
||||
"Search fonts" = "Schriften suchen"
|
||||
|
||||
@@ -913,6 +913,7 @@ name = "English"
|
||||
"Orange" = "Orange"
|
||||
"Order" = "Order"
|
||||
"Other apps may not open every .fig file exported here." = "Other apps may not open every .fig file exported here."
|
||||
"Other" = "Other"
|
||||
"Others lose access and keep their copy; the file stays on the server for you" = "Others lose access and keep their copy; the file stays on the server for you"
|
||||
"Others type this under Share ▸ Join with a code" = "Others type this under Share ▸ Join with a code"
|
||||
"Outline stroke" = "Outline stroke"
|
||||
@@ -1152,7 +1153,7 @@ name = "English"
|
||||
"Scroll with parent" = "Scroll with parent"
|
||||
"Search plugins" = "Search plugins"
|
||||
"Search" = "Search"
|
||||
"Search actions, assets and plugins" = "Search actions, assets and plugins"
|
||||
"Search actions, layers and tools" = "Search actions, layers and tools"
|
||||
"Search assets" = "Search assets"
|
||||
"Search comments" = "Search comments"
|
||||
"Search fonts" = "Search fonts"
|
||||
|
||||
@@ -914,6 +914,7 @@ status = "draft — needs native review"
|
||||
"Orange" = "Naranja"
|
||||
"Order" = "Orden"
|
||||
"Other apps may not open every .fig file exported here." = "Puede que otras aplicaciones no abran todos los archivos .fig exportados aquí."
|
||||
"Other" = "Otros"
|
||||
"Others lose access and keep their copy; the file stays on the server for you" = "Los demás pierden el acceso y conservan su copia; el archivo se queda en el servidor para ti"
|
||||
"Others type this under Share ▸ Join with a code" = "Los demás lo escriben en Compartir ▸ Unirse con un código"
|
||||
"Outline stroke" = "Contornear trazo"
|
||||
@@ -971,7 +972,7 @@ status = "draft — needs native review"
|
||||
"Plugin" = "Plugin"
|
||||
"Plugins run in FigCraft's sandbox, without files, network or clock. Install only plugins you trust." = "Los plugins se ejecutan en el entorno aislado de FigCraft, sin archivos, red ni reloj. Instala solo plugins de confianza."
|
||||
"Plugins" = "Plugins"
|
||||
"Plugins & widgets" = "Plugins & widgets"
|
||||
"Plugins & widgets" = "Plugins y widgets"
|
||||
"Plus darker" = "Más oscuro"
|
||||
"Plus lighter" = "Más claro"
|
||||
"Point" = "Punto"
|
||||
@@ -1153,8 +1154,8 @@ status = "draft — needs native review"
|
||||
"Scroll with parent" = "Desplazar con el padre"
|
||||
"Search plugins" = "Buscar plugins"
|
||||
"Search" = "Buscar"
|
||||
"Search actions, layers and tools" = "Buscar acciones, capas y herramientas"
|
||||
"Search assets" = "Buscar recursos"
|
||||
"Search actions, assets and plugins" = "Buscar acciones, recursos y plugins"
|
||||
"Search comments" = "Buscar comentarios"
|
||||
"Search fonts" = "Buscar fuentes"
|
||||
"Search layers" = "Buscar capas"
|
||||
|
||||
@@ -1090,6 +1090,7 @@ status = "draft — needs native review"
|
||||
"Scroll to layer" = "Défiler jusqu’au calque"
|
||||
"Scroll with parent" = "Défiler avec le parent"
|
||||
"Search" = "Rechercher"
|
||||
"Search actions, layers and tools" = "Rechercher des actions, des calques et des outils"
|
||||
"Search assets" = "Rechercher des ressources"
|
||||
"Search comments" = "Rechercher des commentaires"
|
||||
"Search fonts" = "Rechercher des polices"
|
||||
|
||||
@@ -1137,6 +1137,7 @@ status = "draft — needs native review"
|
||||
"Scroll with parent" = "Gulir bersama induk"
|
||||
"Search plugins" = "Cari plugin"
|
||||
"Search" = "Cari"
|
||||
"Search actions, layers and tools" = "Cari tindakan, layer, dan alat"
|
||||
"Search assets" = "Cari aset"
|
||||
"Search comments" = "Cari komentar"
|
||||
"Search fonts" = "Cari font"
|
||||
|
||||
@@ -1140,6 +1140,7 @@ status = "draft — needs native review"
|
||||
"Scroll to layer" = "レイヤーまでスクロール"
|
||||
"Scroll with parent" = "親と一緒にスクロール"
|
||||
"Search" = "検索"
|
||||
"Search actions, layers and tools" = "アクション、レイヤー、ツールを検索"
|
||||
"Search assets" = "アセットを検索"
|
||||
"Search comments" = "コメントを検索"
|
||||
"Search fonts" = "フォントを検索"
|
||||
|
||||
@@ -1090,6 +1090,7 @@ status = "draft — needs native review"
|
||||
"Scroll to layer" = "레이어로 스크롤"
|
||||
"Scroll with parent" = "부모와 함께 스크롤"
|
||||
"Search" = "검색"
|
||||
"Search actions, layers and tools" = "액션, 레이어, 도구 검색"
|
||||
"Search assets" = "에셋 검색"
|
||||
"Search comments" = "댓글 검색"
|
||||
"Search fonts" = "글꼴 검색"
|
||||
|
||||
@@ -1089,6 +1089,7 @@ status = "draft — needs native review"
|
||||
"Scroll to layer" = "Rolar até a camada"
|
||||
"Scroll with parent" = "Rolar com o pai"
|
||||
"Search" = "Pesquisar"
|
||||
"Search actions, layers and tools" = "Pesquisar ações, camadas e ferramentas"
|
||||
"Search assets" = "Pesquisar recursos"
|
||||
"Search comments" = "Pesquisar comentários"
|
||||
"Search fonts" = "Pesquisar fontes"
|
||||
|
||||
@@ -1134,6 +1134,7 @@ name = "Русский"
|
||||
"Scroll with parent" = "Прокручивать с родителем"
|
||||
"Search plugins" = "Поиск плагинов"
|
||||
"Search" = "Поиск"
|
||||
"Search actions, layers and tools" = "Поиск действий, слоёв и инструментов"
|
||||
"Search assets" = "Поиск ресурсов"
|
||||
"Search comments" = "Поиск комментариев"
|
||||
"Search fonts" = "Поиск шрифтов"
|
||||
|
||||
@@ -1135,6 +1135,7 @@ name = "Svenska"
|
||||
"Scroll with parent" = "Rulla med överordnat element"
|
||||
"Search plugins" = "Sök plugin-program"
|
||||
"Search" = "Sök"
|
||||
"Search actions, layers and tools" = "Sök åtgärder, lager och verktyg"
|
||||
"Search assets" = "Sök tillgångar"
|
||||
"Search comments" = "Sök i kommentarer"
|
||||
"Search fonts" = "Sök typsnitt"
|
||||
|
||||
@@ -1137,6 +1137,7 @@ name = "Türkçe"
|
||||
"Scroll with parent" = "Üst öğeyle birlikte kaydır"
|
||||
"Search plugins" = "Eklentilerde ara"
|
||||
"Search" = "Ara"
|
||||
"Search actions, layers and tools" = "Eylem, katman ve araç ara"
|
||||
"Search assets" = "Varlıklarda ara"
|
||||
"Search comments" = "Yorumlarda ara"
|
||||
"Search fonts" = "Yazı tiplerinde ara"
|
||||
|
||||
@@ -1091,6 +1091,7 @@ status = "draft — needs native review"
|
||||
"Scroll to layer" = "滚动到图层"
|
||||
"Scroll with parent" = "随父级滚动"
|
||||
"Search" = "搜索"
|
||||
"Search actions, layers and tools" = "搜索操作、图层和工具"
|
||||
"Search assets" = "搜索资源"
|
||||
"Search comments" = "搜索评论"
|
||||
"Search fonts" = "搜索字体"
|
||||
|
||||
@@ -414,9 +414,7 @@ pub fn handle(app: &mut FigApp, ctx: &Context, method: &str, p: &Value) -> Resul
|
||||
crate::home_recents::Sort { descending: order == "desc", ..sort }.store(&mut app.session.prefs);
|
||||
}
|
||||
if let Some(h) = p.get("palette").and_then(Value::as_bool) {
|
||||
app.ui.palette = if h { Some(String::new()) } else { None };
|
||||
app.ui.palette_tab = crate::menus::PaletteTab::All;
|
||||
app.ui.palette_armed = true;
|
||||
crate::menus::set_palette(&mut app.ui, h);
|
||||
}
|
||||
// true / false, or a top-level menu to open it at ("Plugins").
|
||||
match p.get("mainMenu") {
|
||||
|
||||
@@ -408,6 +408,8 @@ pub struct UiState {
|
||||
pub palette: Option<String>,
|
||||
/// The Actions palette's tab.
|
||||
pub palette_tab: menus::PaletteTab,
|
||||
/// The palette row Up and Down are on (Enter runs it).
|
||||
pub palette_hi: usize,
|
||||
/// The palette ignores one outside click after opening (the click that opened it).
|
||||
pub palette_armed: bool,
|
||||
/// This frame's Actions toolbar button rect: presses on it toggle, never dismiss.
|
||||
@@ -542,6 +544,7 @@ impl Default for UiState {
|
||||
main_menu: false,
|
||||
palette: None,
|
||||
palette_tab: menus::PaletteTab::All,
|
||||
palette_hi: 0,
|
||||
palette_armed: false,
|
||||
actions_rect: None,
|
||||
variables: None,
|
||||
@@ -799,15 +802,7 @@ impl FigApp {
|
||||
}
|
||||
"dialog.preferences" => self.ui.preferences = true,
|
||||
"panel.shortcuts" => self.ui.shortcuts_panel = !self.ui.shortcuts_panel,
|
||||
"palette.actions" => {
|
||||
if self.ui.palette.is_some() {
|
||||
self.ui.palette = None;
|
||||
} else {
|
||||
self.ui.palette = Some(String::new());
|
||||
self.ui.palette_tab = menus::PaletteTab::All;
|
||||
self.ui.palette_armed = true;
|
||||
}
|
||||
}
|
||||
"palette.actions" => menus::toggle_palette(&mut self.ui),
|
||||
"panel.design" => self.ui.right_tab = RightTab::Design,
|
||||
"panel.prototype" => self.ui.right_tab = RightTab::Prototype,
|
||||
"panel.designPrototype" => {
|
||||
|
||||
+191
-185
@@ -7,8 +7,8 @@ use serde_json::json;
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::a11y;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::i18n::{self, tr, tr_noop, trf};
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::{Tokens, regular};
|
||||
use crate::toolbar::pretty_shortcut;
|
||||
|
||||
@@ -82,7 +82,7 @@ pub enum PaletteTab {
|
||||
Plugins,
|
||||
}
|
||||
|
||||
/// Remember a ran action for the palette's Recents (newest first, capped).
|
||||
/// Remember an action run from the palette for its Recents (newest first, capped).
|
||||
pub fn record_recent(app: &mut FigApp, key: &str) {
|
||||
let r = &mut app.session.prefs.recent_actions;
|
||||
if r.first().is_some_and(|f| f == key) {
|
||||
@@ -94,7 +94,6 @@ pub fn record_recent(app: &mut FigApp, key: &str) {
|
||||
}
|
||||
|
||||
pub fn invoke(app: &mut FigApp, id: &str) {
|
||||
record_recent(app, &format!("command:{id}"));
|
||||
match id {
|
||||
"file.open" => app.open_dialog(),
|
||||
"plugin.import" => crate::plugins_ui::import_dialog(app),
|
||||
@@ -434,7 +433,7 @@ pub fn popups(app: &mut FigApp, ctx: &egui::Context) {
|
||||
t.menu_dim,
|
||||
);
|
||||
if resp.clicked() {
|
||||
app.ui.palette = Some(String::new());
|
||||
set_palette(&mut app.ui, true);
|
||||
close = true;
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
@@ -657,19 +656,31 @@ fn select_layer_menu(app: &mut FigApp, ui: &mut Ui, pos: egui::Pos2, t: &Tokens)
|
||||
/// The Actions palette's content height (the frame adds its own 8 px margins).
|
||||
const PALETTE_H: f32 = 450.0;
|
||||
|
||||
/// The Actions palette: search over commands, assets and plugins, in Figma's shape (tabs,
|
||||
/// recents, grouped sections). Rows run on click; Enter runs the first visible row.
|
||||
/// Open the Actions palette afresh (empty search, All tab, first row highlighted), or close it.
|
||||
pub fn set_palette(ui: &mut crate::UiState, open: bool) {
|
||||
ui.palette = open.then(String::new);
|
||||
ui.palette_tab = PaletteTab::All;
|
||||
ui.palette_hi = 0;
|
||||
ui.palette_armed = open;
|
||||
}
|
||||
|
||||
/// Open the Actions palette afresh, or close it when it's open (⌘K, the toolbar's Actions button).
|
||||
pub fn toggle_palette(ui: &mut crate::UiState) {
|
||||
let open = ui.palette.is_none();
|
||||
set_palette(ui, open);
|
||||
}
|
||||
|
||||
/// The Actions palette: search over commands, assets and plugins, with tabs, recents and grouped
|
||||
/// sections, floating above the toolbar. Rows run on click; Up and Down move the highlight and
|
||||
/// Enter runs the highlighted row.
|
||||
fn palette(app: &mut FigApp, ctx: &egui::Context) {
|
||||
let Some(mut q) = app.ui.palette.clone() else { return };
|
||||
let t = Tokens::get(ctx);
|
||||
// Same centre and width as the toolbar pill (it centres on the canvas, not the window),
|
||||
// floating 8 px above it. Fixed size: switching tabs never resizes the panel.
|
||||
let bar_w = crate::toolbar::main_width();
|
||||
let canvas = app.canvas.rect;
|
||||
let area = Rect::from_min_size(
|
||||
pos2(canvas.center().x - bar_w / 2.0, canvas.max.y - 68.0 - (PALETTE_H + 16.0)),
|
||||
vec2(bar_w, PALETTE_H + 16.0),
|
||||
);
|
||||
let bar = crate::toolbar::pill_rect(app.canvas.rect, bar_w);
|
||||
let area = Rect::from_min_max(pos2(bar.min.x, bar.min.y - 8.0 - (PALETTE_H + 16.0)), pos2(bar.max.x, bar.min.y - 8.0));
|
||||
let mut run: Option<String> = None;
|
||||
let mut close = false;
|
||||
// A press anywhere outside closes the panel. The opening press is spared once, and the
|
||||
@@ -684,135 +695,93 @@ fn palette(app: &mut FigApp, ctx: &egui::Context) {
|
||||
} else if !pressed {
|
||||
app.ui.palette_armed = false;
|
||||
}
|
||||
egui::Area::new(Id::new("palette")).fixed_pos(area.min).order(egui::Order::Foreground).show(
|
||||
ctx,
|
||||
|ui| {
|
||||
egui::Frame::NONE
|
||||
.fill(t.panel)
|
||||
.corner_radius(CornerRadius::same(13))
|
||||
.stroke(Stroke::new(1.0, t.border))
|
||||
.shadow(egui::Shadow { offset: [0, 8], blur: 32, spread: 0, color: t.shadow })
|
||||
.inner_margin(egui::Margin::same(8))
|
||||
.show(ui, |ui| {
|
||||
// Same outer width as the toolbar pill, and a fixed height: switching tabs
|
||||
// never resizes the panel.
|
||||
let inner = crate::toolbar::main_width() - 16.0;
|
||||
ui.set_width(inner);
|
||||
ui.set_height(PALETTE_H);
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(&mut q)
|
||||
.hint_text(tr("Search actions, assets and plugins"))
|
||||
.desired_width(inner - 16.0)
|
||||
.font(regular(14.0))
|
||||
.frame(egui::Frame::NONE.inner_margin(egui::Margin::same(8))),
|
||||
);
|
||||
r.request_focus();
|
||||
crate::widgets::select_all_on_focus(ui, &r, &q);
|
||||
ui.add_space(4.0);
|
||||
// Tabs: All, Assets, Plugins & widgets.
|
||||
let tabs = [(PaletteTab::All, tr("All")), (PaletteTab::Assets, tr("Assets")), (PaletteTab::Plugins, tr("Plugins & widgets"))];
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(4.0, 0.0);
|
||||
for (tab, label) in tabs {
|
||||
let active = app.ui.palette_tab == tab;
|
||||
// The frame stays on for every tab (transparent when idle) so the
|
||||
// active pill never moves its neighbours.
|
||||
let resp = ui.add(
|
||||
egui::Button::new(egui::RichText::new(label).size(12.0).color(if active { t.text } else { t.text_secondary }))
|
||||
.frame(true)
|
||||
.fill(if active { t.hover } else { Color32::TRANSPARENT })
|
||||
.corner_radius(CornerRadius::same(6)),
|
||||
);
|
||||
if resp.clicked() {
|
||||
app.ui.palette_tab = tab;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let enter = ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
let mut first: Option<String> = None;
|
||||
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| {
|
||||
let tab = app.ui.palette_tab;
|
||||
if tab == PaletteTab::All {
|
||||
let recents: Vec<ActionRow> = app
|
||||
.session
|
||||
.prefs
|
||||
.recent_actions
|
||||
.clone()
|
||||
.iter()
|
||||
.filter_map(|key| recent_row(app, key, &q))
|
||||
.collect();
|
||||
if !recents.is_empty() {
|
||||
section(ui, &t, tr("Recents"), |ui| {
|
||||
for row in &recents {
|
||||
first.get_or_insert_with(|| row.key.clone());
|
||||
if draw_row(ui, &t, row) {
|
||||
run = Some(row.key.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if tab != PaletteTab::Plugins {
|
||||
for (group, rows) in command_sections(&q) {
|
||||
if tab == PaletteTab::Assets {
|
||||
continue;
|
||||
}
|
||||
section(ui, &t, &group, |ui| {
|
||||
for row in rows {
|
||||
first.get_or_insert_with(|| row.key.clone());
|
||||
if draw_row(ui, &t, &row) {
|
||||
run = Some(row.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if tab != PaletteTab::Assets {
|
||||
let plugins = plugin_rows(app, &q);
|
||||
let widgets = widget_rows(app, &q);
|
||||
if !plugins.is_empty() {
|
||||
section(ui, &t, tr("Plugins"), |ui| {
|
||||
for row in plugins {
|
||||
first.get_or_insert_with(|| row.key.clone());
|
||||
if draw_row(ui, &t, &row) {
|
||||
run = Some(row.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if !widgets.is_empty() {
|
||||
section(ui, &t, tr("Widgets"), |ui| {
|
||||
for row in widgets {
|
||||
first.get_or_insert_with(|| row.key.clone());
|
||||
if draw_row(ui, &t, &row) {
|
||||
run = Some(row.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if tab == PaletteTab::Assets {
|
||||
for (group, rows) in asset_sections(app, &q) {
|
||||
section(ui, &t, &group, |ui| {
|
||||
for row in rows {
|
||||
first.get_or_insert_with(|| row.key.clone());
|
||||
if draw_row(ui, &t, &row) {
|
||||
run = Some(row.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if enter {
|
||||
if let Some(key) = first {
|
||||
run = Some(key);
|
||||
// Taken before the search field sees them (it would move its cursor).
|
||||
let step = ctx.input_mut(|i| {
|
||||
i.count_and_consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown) as i64
|
||||
- i.count_and_consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp) as i64
|
||||
});
|
||||
egui::Area::new(Id::new("palette")).fixed_pos(area.min).order(egui::Order::Foreground).show(ctx, |ui| {
|
||||
egui::Frame::NONE
|
||||
.fill(t.panel)
|
||||
.corner_radius(CornerRadius::same(13))
|
||||
.stroke(Stroke::new(1.0, t.border))
|
||||
.shadow(egui::Shadow { offset: [0, 8], blur: 32, spread: 0, color: t.shadow })
|
||||
.inner_margin(egui::Margin::same(8))
|
||||
.show(ui, |ui| {
|
||||
// Same outer width as the toolbar pill, and a fixed height: switching tabs
|
||||
// never resizes the panel.
|
||||
let inner = bar_w - 16.0;
|
||||
ui.set_width(inner);
|
||||
ui.set_height(PALETTE_H);
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(&mut q)
|
||||
.hint_text(tr("Search actions, layers and tools"))
|
||||
.desired_width(inner - 16.0)
|
||||
.font(regular(14.0))
|
||||
.frame(egui::Frame::NONE.inner_margin(egui::Margin::same(8))),
|
||||
);
|
||||
r.request_focus();
|
||||
crate::widgets::select_all_on_focus(ui, &r, &q);
|
||||
// A new search starts again at the first row.
|
||||
if r.changed() {
|
||||
app.ui.palette_hi = 0;
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
// Tabs: All, Assets, Plugins & widgets.
|
||||
let tabs = [(PaletteTab::All, tr("All")), (PaletteTab::Assets, tr("Assets")), (PaletteTab::Plugins, tr("Plugins & widgets"))];
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(4.0, 0.0);
|
||||
for (tab, label) in tabs {
|
||||
let active = app.ui.palette_tab == tab;
|
||||
// The frame stays on for every tab (transparent when idle) so the
|
||||
// active pill never moves its neighbours.
|
||||
let resp = ui.add(
|
||||
egui::Button::new(egui::RichText::new(label).size(12.0).color(if active { t.text } else { t.text_secondary }))
|
||||
.frame(true)
|
||||
.fill(if active { t.hover } else { Color32::TRANSPARENT })
|
||||
.corner_radius(CornerRadius::same(6)),
|
||||
);
|
||||
if resp.clicked() {
|
||||
app.ui.palette_tab = tab;
|
||||
app.ui.palette_hi = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
let sections = palette_sections(app, &q, app.ui.palette_tab);
|
||||
let count = sections.iter().map(|(_, rows)| rows.len()).sum::<usize>();
|
||||
// At either end it stays where it was.
|
||||
let hi = (app.ui.palette_hi as i64 + step).clamp(0, count.saturating_sub(1) as i64) as usize;
|
||||
app.ui.palette_hi = hi;
|
||||
// The pointer moving over a row moves the highlight there.
|
||||
let pointer_moved = ui.input(|i| i.pointer.delta() != egui::Vec2::ZERO);
|
||||
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| {
|
||||
let mut i = 0;
|
||||
for (name, rows) in §ions {
|
||||
section(ui, &t, name, |ui| {
|
||||
for row in rows {
|
||||
let resp = draw_row(ui, &t, row, i == hi);
|
||||
if resp.clicked() {
|
||||
run = Some(row.key.clone());
|
||||
}
|
||||
if pointer_moved && resp.hovered() && i != hi {
|
||||
app.ui.palette_hi = i;
|
||||
// Drawn there on the next frame, even if the pointer stops now.
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
if step != 0 && i == hi {
|
||||
resp.scroll_to_me(None);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if run.is_none() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
run = sections.iter().flat_map(|(_, rows)| rows).nth(hi).map(|row| row.key.clone());
|
||||
}
|
||||
});
|
||||
});
|
||||
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
close = true;
|
||||
}
|
||||
@@ -825,10 +794,32 @@ fn palette(app: &mut FigApp, ctx: &egui::Context) {
|
||||
let _ = StrokeKind::Inside;
|
||||
}
|
||||
|
||||
/// The rows a palette tab shows for the query, by section: All has Recents, the commands by menu,
|
||||
/// then plugins and widgets; Assets this file's components and styles; Plugins & widgets those.
|
||||
pub(crate) fn palette_sections(app: &FigApp, q: &str, tab: PaletteTab) -> Vec<(String, Vec<ActionRow>)> {
|
||||
let mut out = match tab {
|
||||
PaletteTab::All => {
|
||||
let recents = app.session.prefs.recent_actions.iter().filter_map(|key| recent_row(app, key, q)).collect();
|
||||
let mut out = vec![(tr("Recents").to_string(), recents)];
|
||||
out.extend(command_sections(q));
|
||||
out
|
||||
}
|
||||
PaletteTab::Assets => asset_sections(app, q),
|
||||
PaletteTab::Plugins => Vec::new(),
|
||||
};
|
||||
if tab != PaletteTab::Assets {
|
||||
out.push((tr("Plugins").to_string(), plugin_rows(app, q)));
|
||||
out.push((tr("Widgets").to_string(), widget_rows(app, q)));
|
||||
}
|
||||
out.retain(|(_, rows)| !rows.is_empty());
|
||||
out
|
||||
}
|
||||
|
||||
/// Run a palette row: `command:<id>`, `plugin:<plugin>/<command>`, `widget:<plugin>`,
|
||||
/// `component:<node>` or `style:<id>`.
|
||||
fn run_action(app: &mut FigApp, key: &str) {
|
||||
pub(crate) fn run_action(app: &mut FigApp, key: &str) {
|
||||
if let Some(id) = key.strip_prefix("command:") {
|
||||
record_recent(app, key);
|
||||
invoke(app, id);
|
||||
} else if let Some(rest) = key.strip_prefix("plugin:") {
|
||||
if let Some((plugin, command)) = rest.split_once('/') {
|
||||
@@ -869,14 +860,14 @@ fn section(ui: &mut Ui, t: &Tokens, name: &str, rows: impl FnOnce(&mut Ui)) {
|
||||
rows(ui);
|
||||
}
|
||||
|
||||
/// Draw one row (a letter tile for plugins, else just the label) with its right-side hint.
|
||||
/// True when clicked.
|
||||
fn draw_row(ui: &mut Ui, t: &Tokens, row: &ActionRow) -> bool {
|
||||
/// Draw one row (a letter tile for plugins, else just the label) with its right-side hint,
|
||||
/// `highlighted` when Enter runs it.
|
||||
fn draw_row(ui: &mut Ui, t: &Tokens, row: &ActionRow, highlighted: bool) -> egui::Response {
|
||||
let w = ui.available_width();
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 34.0), Sense::click());
|
||||
// The hover pill stays inside the row: it never touches the panel's edges.
|
||||
// The highlight pill stays inside the row: it never touches the panel's edges.
|
||||
let pill = r.shrink2(vec2(4.0, 1.0));
|
||||
if resp.hovered() {
|
||||
if highlighted {
|
||||
ui.painter().rect_filled(pill, CornerRadius::same(6), t.hover);
|
||||
}
|
||||
let mut label_x = r.min.x + 10.0;
|
||||
@@ -888,7 +879,7 @@ fn draw_row(ui: &mut Ui, t: &Tokens, row: &ActionRow) -> bool {
|
||||
}
|
||||
ui.painter().text(pos2(label_x, r.center().y), Align2::LEFT_CENTER, &row.label, regular(13.0), t.text);
|
||||
ui.painter().text(pos2(r.max.x - 10.0, r.center().y), Align2::RIGHT_CENTER, &row.hint, regular(11.0), t.text_secondary);
|
||||
resp.clicked()
|
||||
resp
|
||||
}
|
||||
|
||||
/// Whether `label` (or `extra`) matches the query, in any case.
|
||||
@@ -896,28 +887,55 @@ fn matches(q: &str, label: &str, extra: &str) -> bool {
|
||||
q.is_empty() || label.to_lowercase().contains(q) || extra.to_lowercase().contains(q)
|
||||
}
|
||||
|
||||
/// Commands grouped by top-level menu, in catalog order, filtered by the query.
|
||||
/// A command's row: its label, and its shortcut as the hint.
|
||||
fn command_row(id: &str, label: String) -> ActionRow {
|
||||
ActionRow { key: format!("command:{id}"), label, hint: crate::shortcuts::shortcut(id).map(pretty_shortcut).unwrap_or_default(), tile: None }
|
||||
}
|
||||
|
||||
/// Most rows a search shows (the best matches).
|
||||
const MAX_HITS: usize = 60;
|
||||
|
||||
/// Commands grouped by top-level menu, filtered by the query. Without one, the menus' items in
|
||||
/// catalog order; a search covers every command (the engine's and the UI's own, "Other" for those
|
||||
/// no menu lists), best matches first (labels starting with the query, then the shortest), each
|
||||
/// group placed by its best row. Labels match in the UI language and in English. The Plugins
|
||||
/// menu's items come with the plugins ([`plugin_rows`]).
|
||||
pub(crate) fn command_sections(q: &str) -> Vec<(String, Vec<ActionRow>)> {
|
||||
let ql = q.to_lowercase();
|
||||
let mut groups: Vec<(String, Vec<ActionRow>)> = Vec::new();
|
||||
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||
for m in figcraft_engine::catalog::MENU.iter() {
|
||||
if m.label == figcraft_engine::catalog::SEP || m.menu.starts_with("Plugins") || !available(m.id) || !seen.insert(m.id) {
|
||||
let ql = q.trim().to_lowercase();
|
||||
let mut menu_of = std::collections::HashMap::new();
|
||||
for m in MENU.iter().filter(|m| m.label != SEP) {
|
||||
menu_of.entry(m.id).or_insert(m);
|
||||
}
|
||||
let menu_items = MENU.iter().filter(|m| m.label != SEP).map(|m| (m.id, m.label));
|
||||
let engine = figcraft_engine::all_commands().iter().map(|c| (c.id, c.label));
|
||||
let candidates: Box<dyn Iterator<Item = (&str, &str)>> = if ql.is_empty() { Box::new(menu_items) } else { Box::new(menu_items.chain(engine)) };
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
// By top-level menu; `None`: no menu lists it.
|
||||
let mut hits: Vec<(Option<&str>, ActionRow)> = Vec::new();
|
||||
for (id, english) in candidates {
|
||||
if !available(id) || !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
let english = m.label;
|
||||
let label = i18n::menu_item(m.id, english).into_owned();
|
||||
if !matches(&ql, &label, &format!("{english} {}", m.id)) {
|
||||
let menu = menu_of.get(id);
|
||||
let group = menu.map(|m| m.menu.split('/').next().unwrap_or(m.menu));
|
||||
if group == Some("Plugins") {
|
||||
continue;
|
||||
}
|
||||
let group = m.menu.split('/').next().unwrap_or(&m.menu);
|
||||
let group = tr(group).to_string();
|
||||
let row = ActionRow {
|
||||
key: format!("command:{}", m.id),
|
||||
label,
|
||||
hint: crate::shortcuts::shortcut(m.id).map(pretty_shortcut).unwrap_or_default(),
|
||||
tile: None,
|
||||
let label = match menu {
|
||||
Some(m) => i18n::menu_item(id, m.label),
|
||||
None => i18n::command(id, english),
|
||||
};
|
||||
if matches(&ql, &label, &format!("{english} {id}")) {
|
||||
hits.push((group, command_row(id, label.into_owned())));
|
||||
}
|
||||
}
|
||||
if !ql.is_empty() {
|
||||
hits.sort_by_cached_key(|(_, row)| (!row.label.to_lowercase().starts_with(&ql), row.label.chars().count()));
|
||||
hits.truncate(MAX_HITS);
|
||||
}
|
||||
let mut groups: Vec<(String, Vec<ActionRow>)> = Vec::new();
|
||||
for (group, row) in hits {
|
||||
let group = group.map_or_else(|| tr("Other"), tr).to_string();
|
||||
match groups.iter_mut().find(|(g, _)| *g == group) {
|
||||
Some((_, rows)) => rows.push(row),
|
||||
None => groups.push((group, vec![row])),
|
||||
@@ -945,6 +963,13 @@ fn plugin_rows(app: &FigApp, q: &str) -> Vec<ActionRow> {
|
||||
});
|
||||
}
|
||||
}
|
||||
// The Plugins menu's own items (run last, manage, import).
|
||||
for m in MENU.iter().filter(|m| m.label != SEP && m.menu.split('/').next() == Some("Plugins") && available(m.id)) {
|
||||
let label = i18n::menu_item(m.id, m.label);
|
||||
if matches(&ql, &label, &format!("{} {}", m.label, m.id)) {
|
||||
rows.push(command_row(m.id, label.into_owned()));
|
||||
}
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
@@ -974,21 +999,11 @@ fn asset_sections(app: &FigApp, q: &str) -> Vec<(String, Vec<ActionRow>)> {
|
||||
.iter()
|
||||
.filter_map(|id| doc.node(*id))
|
||||
.filter(|n| matches(&ql, &n.name, ""))
|
||||
.map(|n| ActionRow {
|
||||
key: format!("component:{}", n.id.0),
|
||||
label: n.name.clone(),
|
||||
hint: tr("Component").to_string(),
|
||||
tile: None,
|
||||
})
|
||||
.map(|n| ActionRow { key: format!("component:{}", n.id.0), label: n.name.clone(), hint: tr("Component").to_string(), tile: None })
|
||||
.collect();
|
||||
let styles: Vec<ActionRow> = figcraft_engine::library::local_styles(doc)
|
||||
.filter(|s| matches(&ql, &s.name, ""))
|
||||
.map(|s| ActionRow {
|
||||
key: format!("style:{}", s.id),
|
||||
label: s.name.clone(),
|
||||
hint: tr("Style").to_string(),
|
||||
tile: None,
|
||||
})
|
||||
.map(|s| ActionRow { key: format!("style:{}", s.id), label: s.name.clone(), hint: tr("Style").to_string(), tile: None })
|
||||
.collect();
|
||||
let mut out = Vec::new();
|
||||
if !components.is_empty() {
|
||||
@@ -1007,15 +1022,11 @@ pub(crate) fn recent_row(app: &FigApp, key: &str, q: &str) -> Option<ActionRow>
|
||||
let spec = figcraft_engine::find_command(id);
|
||||
let menu_label = figcraft_engine::catalog::MENU.iter().find(|m| m.id == id);
|
||||
let english = menu_label.map(|m| m.label).or_else(|| spec.map(|s| s.label)).unwrap_or(id);
|
||||
if !available(id) || !matches(&ql, english, id) {
|
||||
let label = i18n::command(id, english);
|
||||
if !available(id) || !matches(&ql, &label, &format!("{english} {id}")) {
|
||||
return None;
|
||||
}
|
||||
return Some(ActionRow {
|
||||
key: key.to_string(),
|
||||
label: i18n::command(id, english).into_owned(),
|
||||
hint: crate::shortcuts::shortcut(id).map(pretty_shortcut).unwrap_or_default(),
|
||||
tile: None,
|
||||
});
|
||||
return Some(command_row(id, label.into_owned()));
|
||||
}
|
||||
if let Some(rest) = key.strip_prefix("plugin:") {
|
||||
let (pid, cid) = rest.split_once('/')?;
|
||||
@@ -1025,12 +1036,7 @@ pub(crate) fn recent_row(app: &FigApp, key: &str, q: &str) -> Option<ActionRow>
|
||||
if !matches(&ql, &label, pid) {
|
||||
return None;
|
||||
}
|
||||
return Some(ActionRow {
|
||||
key: key.to_string(),
|
||||
label,
|
||||
hint: tr("Plugin").to_string(),
|
||||
tile: p.manifest.name.chars().next(),
|
||||
});
|
||||
return Some(ActionRow { key: key.to_string(), label, hint: tr("Plugin").to_string(), tile: p.manifest.name.chars().next() });
|
||||
}
|
||||
if let Some(pid) = key.strip_prefix("widget:") {
|
||||
let p = app.session.prefs.plugins.iter().find(|p| p.manifest.id == pid && p.manifest.widget)?;
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
//! The Actions palette, driven headless: invoking records recents (newest first, capped),
|
||||
//! sections group commands by menu, the query filters, gone entries drop out of Recents, a used
|
||||
//! plugin resolves back to a row, and a press outside the panel closes it.
|
||||
//! The Actions palette, driven headless: runs from it record recents (newest first, capped),
|
||||
//! sections group commands by menu, a search covers every command with the best matches first,
|
||||
//! Up/Down move the highlight and Enter runs it, gone entries drop out of Recents, a used plugin
|
||||
//! resolves back to a row, and a press outside the panel closes it.
|
||||
|
||||
use egui::Pos2;
|
||||
use egui::{Event, Key, Modifiers, Pos2};
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::layers_drag_tests::Ui;
|
||||
|
||||
#[test]
|
||||
fn invoke_records_recents_newest_first_and_capped() {
|
||||
fn palette_runs_record_recents_newest_first_and_capped() {
|
||||
let mut u = Ui::new(Session::new());
|
||||
for id in [
|
||||
"view.zoomIn",
|
||||
"view.zoomOut",
|
||||
"edit.copyAsText",
|
||||
"style.list",
|
||||
"edit.findReplace",
|
||||
"edit.copyAsCss",
|
||||
"view.zoomIn",
|
||||
] {
|
||||
crate::menus::invoke(&mut u.app, id);
|
||||
// Menus, shortcuts and context menus don't touch the palette's Recents.
|
||||
crate::menus::invoke(&mut u.app, "view.zoomIn");
|
||||
assert!(u.app.session.prefs.recent_actions.is_empty(), "only the palette records recents");
|
||||
for id in ["view.zoomIn", "view.zoomOut", "edit.copyAsText", "style.list", "edit.findReplace", "edit.copyAsCss", "view.zoomIn"] {
|
||||
crate::menus::run_action(&mut u.app, &format!("command:{id}"));
|
||||
}
|
||||
let r = &u.app.session.prefs.recent_actions;
|
||||
assert_eq!(r.len(), figcraft_engine::MAX_RECENT_ACTIONS, "recents are capped: {r:?}");
|
||||
@@ -81,8 +77,7 @@ fn used_plugin_resolves_to_a_recent_row() {
|
||||
.unwrap();
|
||||
u.app.session.prefs.plugins.push(plugin);
|
||||
crate::menus::record_recent(&mut u.app, "plugin:dev.local.solar-icons/panel");
|
||||
let row = crate::menus::recent_row(&u.app, "plugin:dev.local.solar-icons/panel", "")
|
||||
.expect("a used plugin stays in Recents");
|
||||
let row = crate::menus::recent_row(&u.app, "plugin:dev.local.solar-icons/panel", "").expect("a used plugin stays in Recents");
|
||||
assert_eq!(row.label, "Solar icons — Panel");
|
||||
assert!(crate::menus::recent_row(&u.app, "plugin:dev.local.solar-icons/panel", "zzz").is_none(), "the query filters recents");
|
||||
}
|
||||
@@ -109,3 +104,60 @@ fn outside_press_closes_the_panel() {
|
||||
u.button(Pos2::new(720.0, 20.0), false);
|
||||
assert!(u.app.ui.palette.is_none(), "a single outside press closes the panel");
|
||||
}
|
||||
|
||||
fn rows(groups: &[(String, Vec<crate::menus::ActionRow>)]) -> Vec<&crate::menus::ActionRow> {
|
||||
groups.iter().flat_map(|(_, rows)| rows).collect()
|
||||
}
|
||||
|
||||
/// A search reaches commands no menu lists (in an "Other" group), not just menu items.
|
||||
#[test]
|
||||
fn search_covers_every_command() {
|
||||
let menu = figcraft_engine::catalog::MENU;
|
||||
let spec = figcraft_engine::all_commands().iter().find(|c| !menu.iter().any(|m| m.id == c.id)).expect("a command no menu lists");
|
||||
let groups = crate::menus::command_sections(spec.id);
|
||||
let key = format!("command:{}", spec.id);
|
||||
let group = groups.iter().find(|(_, rows)| rows.iter().any(|r| r.key == key)).map(|(g, _)| g.as_str());
|
||||
assert_eq!(group, Some("Other"), "{} is found: {:?}", spec.id, groups.iter().map(|(g, _)| g).collect::<Vec<_>>());
|
||||
// Without a query the palette lists the menus' items only.
|
||||
assert!(rows(&crate::menus::command_sections("")).iter().all(|r| r.key != key));
|
||||
}
|
||||
|
||||
/// Labels starting with the query come first, the shortest first; the rest are capped.
|
||||
#[test]
|
||||
fn search_ranks_prefix_matches_first() {
|
||||
let groups = crate::menus::command_sections("zoom");
|
||||
let hits = rows(&groups);
|
||||
let first = hits.first().expect("something matches 'zoom'");
|
||||
assert!(first.label.to_lowercase().starts_with("zoom"), "a prefix match leads: {}", first.label);
|
||||
let shortest = hits.iter().filter(|r| r.label.to_lowercase().starts_with("zoom")).map(|r| r.label.chars().count()).min();
|
||||
assert_eq!(Some(first.label.chars().count()), shortest, "the shortest prefix match leads");
|
||||
assert!(rows(&crate::menus::command_sections("e")).len() <= 60, "a broad search is capped");
|
||||
}
|
||||
|
||||
fn key(u: &mut Ui, k: Key) {
|
||||
u.frame(vec![
|
||||
Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE },
|
||||
Event::Key { key: k, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE },
|
||||
]);
|
||||
}
|
||||
|
||||
/// Up and Down move the highlight (it stops at the ends); Enter runs the highlighted row.
|
||||
#[test]
|
||||
fn arrows_move_the_highlight_and_enter_runs_it() {
|
||||
let mut u = Ui::new(Session::new());
|
||||
crate::menus::set_palette(&mut u.app.ui, true);
|
||||
u.frame(vec![]);
|
||||
u.frame(vec![Event::Text("zoom".into())]);
|
||||
assert_eq!(u.app.ui.palette.as_deref(), Some("zoom"));
|
||||
assert_eq!(u.app.ui.palette_hi, 0, "a search starts at the first row");
|
||||
key(&mut u, Key::ArrowUp);
|
||||
assert_eq!(u.app.ui.palette_hi, 0, "it stops at the top");
|
||||
key(&mut u, Key::ArrowDown);
|
||||
key(&mut u, Key::ArrowDown);
|
||||
key(&mut u, Key::ArrowUp);
|
||||
assert_eq!(u.app.ui.palette_hi, 1);
|
||||
let want = rows(&crate::menus::palette_sections(&u.app, "zoom", crate::menus::PaletteTab::All)).get(1).map(|r| r.key.clone()).expect("two rows");
|
||||
key(&mut u, Key::Enter);
|
||||
assert!(u.app.ui.palette.is_none(), "Enter runs a row and closes the palette");
|
||||
assert_eq!(u.app.session.prefs.recent_actions.first(), Some(&want), "the highlighted row ran");
|
||||
}
|
||||
|
||||
@@ -171,12 +171,10 @@ pub fn menu(app: &mut FigApp, ui: &mut Ui, t: &Tokens) -> bool {
|
||||
.flatten();
|
||||
crate::menus::separator(ui);
|
||||
if let Some(plugin) = widget {
|
||||
crate::menus::record_recent(app, &format!("widget:{plugin}"));
|
||||
app.run("widget.insert", json!({"plugin": plugin}));
|
||||
return true;
|
||||
}
|
||||
let Some((id, command)) = chosen else { return false };
|
||||
crate::menus::record_recent(app, &format!("plugin:{id}/{command}"));
|
||||
start(app, &id, &command);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ pub(crate) fn main_width() -> f32 {
|
||||
fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
let (cell, gap) = (32.0, 4.0);
|
||||
let width = main_width();
|
||||
let rect = pill(ui, &mut app.ui.nav, canvas, width, &t);
|
||||
let rect = pill(ui, &mut app.ui.nav, canvas, width, t);
|
||||
let current = app.session.tool.tool;
|
||||
let mut x = rect.min.x + 8.0;
|
||||
let cy = rect.center().y;
|
||||
@@ -94,7 +94,7 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
let shown = if in_group { current } else { remembered(ui, g.key, g.tools.first().map(|x| x.0).unwrap_or(ToolId::Move)) };
|
||||
let icon = g.tools.iter().find(|(tid, _)| *tid == shown).map(|(_, i)| *i).unwrap_or(Icon::Move);
|
||||
let r = Rect::from_center_size(pos2(x + cell / 2.0, cy), vec2(cell, cell));
|
||||
let resp = tool_cell(ui, &t, r, ("tool", g.key), icon, in_group, &format!("{} {}", tr(shown.label()), pretty_shortcut(shown.shortcut())));
|
||||
let resp = tool_cell(ui, t, r, ("tool", g.key), icon, in_group, &format!("{} {}", tr(shown.label()), pretty_shortcut(shown.shortcut())));
|
||||
if in_group {
|
||||
a11y::prefer(&mut app.ui.nav, Region::Toolbar, resp.id);
|
||||
}
|
||||
@@ -152,13 +152,7 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
icons::paint(ui.painter(), r.shrink(7.0), icon, if actions_open { Color32::WHITE } else { t.icon });
|
||||
if resp.clicked() {
|
||||
if id == "actions" {
|
||||
if app.ui.palette.is_some() {
|
||||
app.ui.palette = None;
|
||||
} else {
|
||||
app.ui.palette = Some(String::new());
|
||||
app.ui.palette_tab = crate::menus::PaletteTab::All;
|
||||
app.ui.palette_armed = true;
|
||||
}
|
||||
crate::menus::toggle_palette(&mut app.ui);
|
||||
} else {
|
||||
app.ui.left_tab = crate::LeftTab::Assets;
|
||||
app.ui.left_collapsed = false;
|
||||
@@ -166,7 +160,7 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
}
|
||||
x += cell + 4.0;
|
||||
}
|
||||
divider(ui, &t, x, cy);
|
||||
divider(ui, t, x, cy);
|
||||
x += 8.0;
|
||||
// Mode switcher: Draw · Design · Prototype · Inspect (code).
|
||||
let modes = [(Icon::Pencil, tr("Draw")), (Icon::Frame, tr("Design")), (Icon::Prototype, tr("Prototype")), (Icon::Code, tr("Inspect"))];
|
||||
@@ -348,10 +342,15 @@ fn builder_toolbar(app: &mut FigApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a toolbar pill `width` wide sits: centred 12 px above the canvas's bottom edge.
|
||||
pub(crate) fn pill_rect(canvas: Rect, width: f32) -> Rect {
|
||||
Rect::from_center_size(pos2(canvas.center().x, canvas.max.y - 12.0 - 24.0), vec2(width, 48.0))
|
||||
}
|
||||
|
||||
/// The floating pill (shadow, panel, border) `width` wide at the bottom centre of the canvas: the
|
||||
/// toolbar region keyboard navigation moves through.
|
||||
fn pill(ui: &Ui, nav: &mut Nav, canvas: Rect, width: f32, t: &Tokens) -> Rect {
|
||||
let rect = Rect::from_center_size(pos2(canvas.center().x, canvas.max.y - 12.0 - 24.0), vec2(width, 48.0));
|
||||
let rect = pill_rect(canvas, width);
|
||||
// The pill takes the press and the drag over it: its buttons sense clicks only, and egui gives
|
||||
// a drag the canvas under them would sense to the canvas, so the pen placed an anchor there.
|
||||
// Not focusable: the keyboard walks the buttons, not the pill.
|
||||
|
||||
Reference in New Issue
Block a user