mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 12:26:52 -06:00
Palette follows the bar: toggle, dismiss, no invented icons
The Actions button toggles (re-click and Cmd+K close), reads on in accent blue with a white icon while open, and a press anywhere outside closes the panel in one click. The opening press is spared once (the spare expires on press-free frames); presses on the button itself never dismiss, so the release toggles cleanly shut instead of reopening. Rows drop the per-family icons (labels + right-side hints only); plugins and widgets keep their letter tiles. The panel anchors to the canvas centre at the toolbar pill's exact width (toolbar::main_width), at a fixed size, with inset hover pills and larger section titles. Tests: one-click dismiss pinned (palette_tests).
This commit is contained in:
@@ -416,6 +416,7 @@ pub fn handle(app: &mut FigApp, ctx: &Context, method: &str, p: &Value) -> Resul
|
||||
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;
|
||||
}
|
||||
// true / false, or a top-level menu to open it at ("Plugins").
|
||||
match p.get("mainMenu") {
|
||||
|
||||
@@ -408,6 +408,10 @@ pub struct UiState {
|
||||
pub palette: Option<String>,
|
||||
/// The Actions palette's tab.
|
||||
pub palette_tab: menus::PaletteTab,
|
||||
/// 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.
|
||||
pub actions_rect: Option<egui::Rect>,
|
||||
/// The Variables editor, when open.
|
||||
pub variables: Option<variables_panel::Editor>,
|
||||
pub about: bool,
|
||||
@@ -538,6 +542,8 @@ impl Default for UiState {
|
||||
main_menu: false,
|
||||
palette: None,
|
||||
palette_tab: menus::PaletteTab::All,
|
||||
palette_armed: false,
|
||||
actions_rect: None,
|
||||
variables: None,
|
||||
about: false,
|
||||
about_tab: dialogs::AboutTab::About,
|
||||
@@ -794,8 +800,13 @@ impl FigApp {
|
||||
"dialog.preferences" => self.ui.preferences = true,
|
||||
"panel.shortcuts" => self.ui.shortcuts_panel = !self.ui.shortcuts_panel,
|
||||
"palette.actions" => {
|
||||
self.ui.palette = Some(String::new());
|
||||
self.ui.palette_tab = menus::PaletteTab::All;
|
||||
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;
|
||||
}
|
||||
}
|
||||
"panel.design" => self.ui.right_tab = RightTab::Design,
|
||||
"panel.prototype" => self.ui.right_tab = RightTab::Prototype,
|
||||
|
||||
@@ -651,6 +651,18 @@ fn palette(app: &mut FigApp, ctx: &egui::Context) {
|
||||
);
|
||||
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
|
||||
// spare expires the first frame without a press (so the next outside press always closes).
|
||||
let pressed = ctx.input(|i| i.pointer.any_pressed());
|
||||
if pressed && !std::mem::replace(&mut app.ui.palette_armed, false) {
|
||||
let on_toggle = app.ui.actions_rect.is_some_and(|b| ctx.input(|i| i.pointer.hover_pos().is_some_and(|p| b.contains(p))));
|
||||
let outside = !on_toggle && ctx.input(|i| i.pointer.hover_pos().map(|p| !area.contains(p)).unwrap_or(false));
|
||||
if outside {
|
||||
close = true;
|
||||
}
|
||||
} 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| {
|
||||
@@ -830,9 +842,9 @@ pub(crate) struct ActionRow {
|
||||
|
||||
/// A section header with its rows.
|
||||
fn section(ui: &mut Ui, t: &Tokens, name: &str, rows: impl FnOnce(&mut Ui)) {
|
||||
ui.add_space(6.0);
|
||||
ui.painter().text(pos2(ui.min_rect().min.x + 10.0, ui.cursor().min.y + 7.0), Align2::LEFT_TOP, name, regular(11.0), t.text_secondary);
|
||||
ui.add_space(18.0);
|
||||
ui.add_space(10.0);
|
||||
ui.painter().text(pos2(ui.min_rect().min.x + 10.0, ui.cursor().min.y + 8.0), Align2::LEFT_TOP, name, regular(12.5), t.text_secondary);
|
||||
ui.add_space(22.0);
|
||||
rows(ui);
|
||||
}
|
||||
|
||||
@@ -841,8 +853,10 @@ fn section(ui: &mut Ui, t: &Tokens, name: &str, rows: impl FnOnce(&mut Ui)) {
|
||||
fn draw_row(ui: &mut Ui, t: &Tokens, row: &ActionRow) -> bool {
|
||||
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.
|
||||
let pill = r.shrink2(vec2(4.0, 1.0));
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(6), t.hover);
|
||||
ui.painter().rect_filled(pill, CornerRadius::same(6), t.hover);
|
||||
}
|
||||
let mut label_x = r.min.x + 10.0;
|
||||
if let Some(letter) = row.tile {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//! The Actions palette, driven headless: invoking records recents (newest first, capped),
|
||||
//! sections group commands by menu, the query filters, and gone entries drop out of Recents.
|
||||
//! 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.
|
||||
|
||||
use egui::Pos2;
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::layers_drag_tests::Ui;
|
||||
|
||||
@@ -58,3 +61,51 @@ fn gone_entries_drop_out_of_recents() {
|
||||
assert!(crate::menus::recent_row(&u.app, "plugin:nope.nope/nope", "").is_none(), "gone plugins drop out");
|
||||
assert!(crate::menus::recent_row(&u.app, "widget:nope", "").is_none(), "gone widgets drop out");
|
||||
}
|
||||
|
||||
/// A used plugin (API 4, several commands) resolves back to a Recents row.
|
||||
#[test]
|
||||
fn used_plugin_resolves_to_a_recent_row() {
|
||||
let mut u = Ui::new(Session::new());
|
||||
let plugin: figcraft_engine::plugins::InstalledPlugin = serde_json::from_value(json!({
|
||||
"manifest": {
|
||||
"id": "dev.local.solar-icons",
|
||||
"name": "Solar icons",
|
||||
"main": "solar_icons.wasm",
|
||||
"api": 4,
|
||||
"commands": [
|
||||
{"id": "panel", "label": "Panel"},
|
||||
{"id": "grid", "label": "Grid"},
|
||||
],
|
||||
},
|
||||
}))
|
||||
.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");
|
||||
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");
|
||||
}
|
||||
|
||||
/// A press outside the panel closes it; the opening press is spared once.
|
||||
#[test]
|
||||
fn outside_press_closes_the_panel() {
|
||||
let mut u = Ui::new(Session::new());
|
||||
u.app.ui.palette = Some(String::new());
|
||||
u.app.ui.palette_armed = true;
|
||||
// The opening press: spared, and disarmed.
|
||||
u.button(Pos2::new(720.0, 20.0), true);
|
||||
u.button(Pos2::new(720.0, 20.0), false);
|
||||
assert!(u.app.ui.palette.is_some(), "the opening press is spared");
|
||||
// The next press outside: closed.
|
||||
u.button(Pos2::new(720.0, 20.0), true);
|
||||
u.button(Pos2::new(720.0, 20.0), false);
|
||||
assert!(u.app.ui.palette.is_none(), "an outside press closes the panel");
|
||||
// One click is enough: the spare expires on frames without a press.
|
||||
u.app.ui.palette = Some(String::new());
|
||||
u.app.ui.palette_armed = true;
|
||||
u.frame(vec![]);
|
||||
u.button(Pos2::new(720.0, 20.0), true);
|
||||
u.button(Pos2::new(720.0, 20.0), false);
|
||||
assert!(u.app.ui.palette.is_none(), "a single outside press closes the panel");
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ fn remembered(ui: &Ui, key: &str, default: ToolId) -> ToolId {
|
||||
pub fn show(app: &mut FigApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let canvas = app.canvas.rect;
|
||||
app.ui.actions_rect = None;
|
||||
// A file you can't edit now (view only, or on its way to a server): moving around only.
|
||||
if let Some(why) = app.session.active_read_only() {
|
||||
view_only_toolbar(app, ui, why);
|
||||
@@ -138,13 +139,26 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
] {
|
||||
let r = Rect::from_center_size(pos2(x + cell / 2.0, cy), vec2(cell, cell));
|
||||
let resp = ui.interact(r, ui.id().with(id), Sense::click()).tip(tip);
|
||||
if resp.hovered() {
|
||||
if id == "actions" {
|
||||
app.ui.actions_rect = Some(r);
|
||||
}
|
||||
// The Actions button reads on while its panel is open; clicking it again closes it.
|
||||
let actions_open = id == "actions" && app.ui.palette.is_some();
|
||||
if actions_open {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), ACCENT);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), r.shrink(7.0), icon, t.icon);
|
||||
icons::paint(ui.painter(), r.shrink(7.0), icon, if actions_open { Color32::WHITE } else { t.icon });
|
||||
if resp.clicked() {
|
||||
if id == "actions" {
|
||||
app.ui.palette = Some(String::new());
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
app.ui.left_tab = crate::LeftTab::Assets;
|
||||
app.ui.left_collapsed = false;
|
||||
|
||||
Reference in New Issue
Block a user