mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 04:46:50 -06:00
M7: popup menus sized to their content (#83)
egui measures a popup in an invisible first pass as wide as the width it is given, else its 600 px default area width, and menu rows filled the available width, so every menu opened from the right panel came out about 600 px wide. - right::menu_popup / context_popup: menus measured at the opening control's width, text kept on one line; every Popup::menu and context menu uses them. - menu_row takes the larger of the menu's width and its own content (label, shortcut, art width), so a menu is as wide as its widest row and at least as wide as its dropdown; explicit .width(w) is now a minimum. Menus at the panel edge stay inside the window. - Headless tests: a menu fits its widest row without wrapping, and the Inspect panel's dropdown menu fits the panel. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -185,7 +185,7 @@ fn model_fields(ui: &mut Ui, c: Color) -> Option<Color> {
|
||||
ui.add_space(16.0);
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
let resp = widgets::dropdown(ui, MODELS.get(model).copied().unwrap_or("Hex"), 52.0, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
crate::right::menu_popup(&resp).show(|ui| {
|
||||
for (i, m) in MODELS.iter().enumerate() {
|
||||
if crate::right::menu_item(ui, m, "") {
|
||||
ui.data_mut(|d| d.insert_temp(model_id, i));
|
||||
@@ -542,7 +542,7 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(16.0);
|
||||
let resp = widgets::dropdown(ui, &format!("{:?}", img.scale_mode), 100.0, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
crate::right::menu_popup(&resp).show(|ui| {
|
||||
for (l, m) in [
|
||||
("Fill", ImageScaleMode::Fill),
|
||||
("Fit", ImageScaleMode::Fit),
|
||||
@@ -759,7 +759,7 @@ fn image_presets(app: &mut FigApp, ui: &mut Ui, target: &PickerTarget, paint: &m
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(16.0);
|
||||
let resp = widgets::dropdown(ui, ¤t, 152.0, None).on_hover_text("Adjustment presets");
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
crate::right::menu_popup(&resp).show(|ui| {
|
||||
for (id, label, _) in built_in {
|
||||
if crate::right::menu_item(ui, label, "") {
|
||||
action = Some(PresetAction::Apply(id.to_string()));
|
||||
|
||||
@@ -684,7 +684,7 @@ fn message(
|
||||
let menu = Id::new(("comment_msg_more_menu", id, i));
|
||||
let more =
|
||||
hover_button(ui, Id::new(("comment_msg_more", id, i)), Icon::Dots, hovered || egui::Popup::is_id_open(&ctx, menu), "More");
|
||||
egui::Popup::menu(&more).id(menu).show(|ui| {
|
||||
crate::right::menu_popup(&more).id(menu).show(|ui| {
|
||||
if crate::right::menu_item(ui, "Edit", "") {
|
||||
*act = Some(Act::Edit(i, m.text.clone()));
|
||||
ui.close();
|
||||
@@ -703,7 +703,7 @@ fn message(
|
||||
hovered || egui::Popup::is_id_open(&ctx, menu),
|
||||
"Add reaction",
|
||||
);
|
||||
egui::Popup::menu(&react).id(menu).show(|ui| {
|
||||
crate::right::menu_popup(&react).id(menu).show(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
for e in REACTIONS {
|
||||
let on = m.reacted(e, cx.me);
|
||||
@@ -901,7 +901,7 @@ pub fn thread(app: &mut FigApp, ctx: &egui::Context, id: u64, anchor: Pos2, boun
|
||||
act = Some(Act::Close);
|
||||
}
|
||||
let more = icons::button(ui, Icon::Dots, 24.0, false, "More");
|
||||
egui::Popup::menu(&more).show(|ui| {
|
||||
crate::right::menu_popup(&more).show(|ui| {
|
||||
if crate::right::menu_item(ui, "Mark as unread", "") {
|
||||
act = Some(Act::MarkUnread);
|
||||
ui.close();
|
||||
@@ -1033,7 +1033,7 @@ pub fn panel(app: &mut FigApp, ui: &mut Ui) {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
let menu = icons::button(ui, Icon::Adjust, 24.0, app.ui.comments.filter != Filter::Open, "Filter and sort");
|
||||
egui::Popup::menu(&menu).width(200.0).show(|ui| {
|
||||
crate::right::menu_popup(&menu).width(200.0).show(|ui| {
|
||||
for f in Filter::ALL {
|
||||
if crate::right::menu_check(ui, app.ui.comments.filter == f, f.label(), "") {
|
||||
app.ui.comments.filter = f;
|
||||
|
||||
@@ -18,7 +18,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::right::{menu_item, row};
|
||||
use crate::right::{menu_item, menu_popup, row};
|
||||
use crate::theme::{COMPONENT, Tokens, semibold};
|
||||
use crate::widgets;
|
||||
|
||||
@@ -136,7 +136,7 @@ fn slot_fill(app: &mut FigApp, ui: &mut Ui, doc: &Document, slot: NodeId, prefer
|
||||
(true, k) => format!("{k} layers"),
|
||||
};
|
||||
let resp = widgets::dropdown(ui, &label, w, Some(Icon::Slot)).on_hover_text("Add content to this slot");
|
||||
egui::Popup::menu(&resp).width(w.max(200.0)).show(|ui| {
|
||||
menu_popup(&resp).width(w.max(200.0)).show(|ui| {
|
||||
let insert = |app: &mut FigApp, c: NodeId| app.run("instance.slotInsert", json!({"id": slot, "component": c.0}));
|
||||
let heading = |ui: &mut Ui, text: &str| ui.label(egui::RichText::new(text).font(semibold(11.0)).color(t.text_secondary));
|
||||
if !preferred.is_empty() {
|
||||
@@ -194,7 +194,7 @@ fn properties_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, owner: NodeId) {
|
||||
let existing: Vec<String> = defs.keys().cloned().chain(variants.iter().map(|(k, _)| k.clone())).collect();
|
||||
let fresh = (1..).map(|k| format!("Property {k}")).find(|n| !existing.contains(n)).unwrap_or_else(|| "Property".into());
|
||||
let plus = header_with_plus(ui, "Properties", "Create component property");
|
||||
egui::Popup::menu(&plus).width(180.0).show(|ui| {
|
||||
menu_popup(&plus).width(180.0).show(|ui| {
|
||||
let swap_default = comps.iter().map(|(c, _)| *c).find(|c| *c != owner && first != Some(*c));
|
||||
let items: [(&str, &str, Value); 4] = [
|
||||
("Variant", "variant", json!("Default")),
|
||||
@@ -226,7 +226,7 @@ fn properties_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, owner: NodeId) {
|
||||
}
|
||||
let def = vals.first().cloned().unwrap_or_default();
|
||||
let resp = widgets::dropdown(ui, &def, 70.0, None).on_hover_text("Default value (the first variant)");
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for v in vals {
|
||||
if menu_item(ui, v, "") {
|
||||
app.run("component.editProperty", json!({"id": owner, "name": k, "default": v}));
|
||||
@@ -285,7 +285,7 @@ fn properties_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, owner: NodeId) {
|
||||
PropDef::InstanceSwap { default } => {
|
||||
let label = doc.node(*default).map(|n| n.name.clone()).unwrap_or_else(|| "Missing".into());
|
||||
let resp = widgets::dropdown(ui, &label, 84.0, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (c, name) in &comps {
|
||||
if menu_item(ui, name, "") {
|
||||
app.run("component.editProperty", json!({"id": owner, "name": k, "default": c.0}));
|
||||
@@ -313,7 +313,7 @@ fn properties_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, owner: NodeId) {
|
||||
};
|
||||
let w = (ui.available_width() - 8.0).max(60.0);
|
||||
let resp = widgets::dropdown(ui, &label, w, None).on_hover_text("Offered first when filling the slot (any content is allowed)");
|
||||
egui::Popup::menu(&resp).width(w.max(200.0)).show(|ui| {
|
||||
menu_popup(&resp).width(w.max(200.0)).show(|ui| {
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
for (c, name) in comps.iter().filter(|(c, _)| *c != owner && first != Some(*c)) {
|
||||
let on = preferred.contains(c);
|
||||
@@ -382,7 +382,7 @@ fn bind_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, n: &Node) {
|
||||
widgets::label(ui, label, t.text_secondary);
|
||||
let shown = bound.clone().unwrap_or_else(|| "Not bound".into());
|
||||
let resp = widgets::dropdown(ui, &shown, 120.0, None).on_hover_text("Apply a component property");
|
||||
egui::Popup::menu(&resp).width(180.0).show(|ui| {
|
||||
menu_popup(&resp).width(180.0).show(|ui| {
|
||||
for k in &matching {
|
||||
if menu_item(ui, k, "") {
|
||||
app.run("component.bindProperty", json!({"layer": n.id, "property": k, "field": field}));
|
||||
@@ -482,7 +482,7 @@ fn instance_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, n: &Node) {
|
||||
ui.label(egui::RichText::new(if nested { "Nested instance" } else { "Instance" }).font(semibold(11.0)).color(t.text));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let more = icons::button(ui, Icon::Dots, 24.0, false, "More instance actions");
|
||||
egui::Popup::menu(&more).width(220.0).show(|ui| {
|
||||
menu_popup(&more).width(220.0).show(|ui| {
|
||||
if !missing && menu_item(ui, "Go to main component", "") {
|
||||
app.run("component.goToMain", json!({"id": id}));
|
||||
ui.close();
|
||||
@@ -516,7 +516,7 @@ fn instance_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, n: &Node) {
|
||||
row(ui, 32.0, |ui| {
|
||||
let w = (ui.available_width() - 32.0).max(80.0);
|
||||
let resp = widgets::dropdown(ui, &comp_name, w, Some(Icon::Instance)).on_hover_text("Swap instance");
|
||||
egui::Popup::menu(&resp).width(w.max(200.0)).show(|ui| {
|
||||
menu_popup(&resp).width(w.max(200.0)).show(|ui| {
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
let mut page = None;
|
||||
for (c, name) in &comps {
|
||||
@@ -601,7 +601,7 @@ fn instance_props(app: &mut FigApp, ui: &mut Ui, doc: &Document, n: &Node, comps
|
||||
prop_label(ui, &k, Icon::ComponentSet);
|
||||
let w = ui.available_width().max(60.0);
|
||||
let resp = widgets::dropdown(ui, &cur, w, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for v in &vals {
|
||||
if menu_item(ui, v, "") {
|
||||
app.run("instance.setProperties", json!({"ids": [id], "properties": {k.clone(): v}}));
|
||||
@@ -643,7 +643,7 @@ fn instance_props(app: &mut FigApp, ui: &mut Ui, doc: &Document, n: &Node, comps
|
||||
let cur = figcraft_engine::params::id_of(&v);
|
||||
let label = cur.and_then(|c| doc.node(c)).map(|c| c.name.clone()).unwrap_or_else(|| "None".into());
|
||||
let resp = widgets::dropdown(ui, &label, w, Some(Icon::Instance));
|
||||
egui::Popup::menu(&resp).width(200.0).show(|ui| {
|
||||
menu_popup(&resp).width(200.0).show(|ui| {
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
for (c, name) in comps {
|
||||
if menu_item(ui, name, "") {
|
||||
|
||||
@@ -10,7 +10,7 @@ use figcraft_engine::devinspect::{self, DevItem, Prop};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::right::{menu_check, prop_row, row};
|
||||
use crate::right::{menu_check, menu_popup, prop_row, row};
|
||||
use crate::theme::{ACCENT, Tokens, regular, semibold};
|
||||
use crate::widgets::{self, PAD};
|
||||
use crate::{FigApp, RightTab};
|
||||
@@ -230,7 +230,7 @@ fn pin_menu(ui: &mut Ui, id: Id, sections: &[devinspect::Section], pinned: &[Str
|
||||
let w = ui.available_width() - PAD * 2.0;
|
||||
row(ui, 30.0, |ui| {
|
||||
let resp = ui.push_id(id, |ui| widgets::dropdown(ui, "Pin a property", w, Some(Icon::Plus))).inner;
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for p in PINNABLE {
|
||||
let on = pinned.iter().any(|q| q == p.id);
|
||||
if (on || annotations::applies(sections, p)) && menu_check(ui, on, p.label, "") {
|
||||
@@ -260,7 +260,7 @@ pub fn ready_list(app: &mut FigApp, ui: &mut Ui) {
|
||||
let w = ui.available_width() - PAD * 2.0;
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, FILTERS.get(filter).map_or("", |f| f.1), w, Some(Icon::Code));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (i, (_, label)) in FILTERS.iter().enumerate() {
|
||||
if menu_check(ui, i == filter, label, "") {
|
||||
app.ui.dev.filter = i;
|
||||
|
||||
@@ -11,7 +11,7 @@ use serde_json::json;
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::right::{common, menu_item, row};
|
||||
use crate::right::{common, menu_item, menu_popup, row};
|
||||
use crate::theme::{Tokens, regular};
|
||||
use crate::widgets::{self, FIELD_H};
|
||||
|
||||
@@ -76,7 +76,7 @@ fn setting_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, e: &Expo
|
||||
app.run("export.update", json!({"ids": ids, "index": i, "suffix": sfx}));
|
||||
}
|
||||
let resp = widgets::dropdown(ui, e.format.label(), format_w, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for f in ExportFormat::ALL {
|
||||
if menu_item(ui, f.label(), "") {
|
||||
app.run("export.update", json!({"ids": ids, "index": i, "format": f.ext()}));
|
||||
|
||||
@@ -227,7 +227,7 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
|
||||
app.ui.rename_page = Some((id, name.clone()));
|
||||
ui.memory_mut(|m| m.request_focus(Id::new(("pagename", id)).with("te")));
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
crate::right::context_popup(&resp).show(|ui| {
|
||||
if ui.button("Rename page").clicked() {
|
||||
app.ui.rename_page = Some((id, name.clone()));
|
||||
ui.close();
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde_json::json;
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::right::{block, common, menu_item, row};
|
||||
use crate::right::{block, common, menu_item, menu_popup, row};
|
||||
use crate::theme::{ACCENT, Tokens, regular};
|
||||
use crate::widgets::{self, FIELD_H, Lead};
|
||||
|
||||
@@ -155,7 +155,7 @@ fn dimensions(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId], has
|
||||
}
|
||||
let current = first.map(|n| if horizontal { n.width } else { n.height }).unwrap_or(0.0);
|
||||
let ids2 = ids.clone();
|
||||
egui::Popup::menu(&resp).width(200.0).show(|ui| {
|
||||
menu_popup(&resp).width(200.0).show(|ui| {
|
||||
let mut opts = vec![("Fixed", "fixed")];
|
||||
if has_al || is_text {
|
||||
opts.push(("Hug contents", "hug"));
|
||||
@@ -312,7 +312,7 @@ fn alignment(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], l: &AutoLayout) {
|
||||
app.field(ids, "gap", e, |v| json!(v));
|
||||
ui.add_space(8.0);
|
||||
let resp = widgets::dropdown(ui, if auto { "Auto gap" } else { "Fixed gap" }, 96.0, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
if menu_item(ui, "Fixed", "") {
|
||||
app.set_props(ids, json!({"mainAlign": "min"}));
|
||||
ui.close();
|
||||
@@ -434,7 +434,7 @@ fn tracks(app: &mut FigApp, ui: &mut Ui, doc: &Document, ids: &[NodeId]) {
|
||||
Sizing::Fill => ("Fill", Icon::FillContainer),
|
||||
};
|
||||
let resp = widgets::dropdown(ui, label, col, Some(icon));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (l, v) in [("Fixed", "fixed"), ("Hug contents", "hug"), ("Fill (fr)", "fill")] {
|
||||
if menu_item(ui, l, "") {
|
||||
app.run("layout.setTrack", with(v, None));
|
||||
|
||||
@@ -58,6 +58,8 @@ mod panel_reorder_tests;
|
||||
#[cfg(test)]
|
||||
mod perf_tests;
|
||||
#[cfg(test)]
|
||||
mod popup_width_tests;
|
||||
#[cfg(test)]
|
||||
mod present_tests;
|
||||
#[cfg(test)]
|
||||
mod present_view_tests;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Popup menus are as wide as their widest row (at least as wide as what opened them), not egui's
|
||||
//! default area width, and stay inside the window: drawn headless.
|
||||
|
||||
use egui::{Color32, Event, Order, PointerButton, Pos2, RawInput, Rect, pos2, vec2};
|
||||
use figcraft_engine::Session;
|
||||
|
||||
use crate::right::{menu_item, menu_popup, menu_row_width};
|
||||
use crate::theme::regular;
|
||||
use crate::{FigApp, NoServices, RightTab};
|
||||
|
||||
const SCREEN: egui::Vec2 = vec2(1440.0, 900.0);
|
||||
|
||||
fn input(t: &mut f64, events: Vec<Event>) -> RawInput {
|
||||
*t += 1.0 / 60.0;
|
||||
RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, SCREEN)), time: Some(*t), events, ..Default::default() }
|
||||
}
|
||||
|
||||
fn press(p: Pos2, pressed: bool) -> Vec<Event> {
|
||||
vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() }]
|
||||
}
|
||||
|
||||
/// The rects of the foreground areas (menus) showing.
|
||||
fn menus(ctx: &egui::Context) -> Vec<Rect> {
|
||||
ctx.memory(|m| m.areas().visible_layer_ids().into_iter().filter(|l| l.order == Order::Foreground).filter_map(|l| m.area_rect(l.id)).collect())
|
||||
}
|
||||
|
||||
/// The popup frame's margins, left plus right.
|
||||
fn frame_margins(ctx: &egui::Context) -> f32 {
|
||||
egui::Frame::popup(&ctx.global_style()).total_margin().sum().x
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_menu_is_as_wide_as_its_widest_row_and_does_not_wrap() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut t = 0.0;
|
||||
let items = [("Copy as code", "Cmd+Shift+C"), ("Paste", "Cmd+V"), ("A much longer menu item label", "")];
|
||||
let mut frame = |events: Vec<Event>| {
|
||||
let mut out = ctx.run_ui(input(&mut t, events), |ui| {
|
||||
// A narrow icon button opens it.
|
||||
let (_, resp) = ui.allocate_exact_size(vec2(24.0, 24.0), egui::Sense::click());
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (label, keys) in items {
|
||||
menu_item(ui, label, keys);
|
||||
}
|
||||
});
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let at = pos2(12.0, 12.0);
|
||||
frame(vec![]);
|
||||
frame(press(at, true));
|
||||
frame(press(at, false));
|
||||
for _ in 0..3 {
|
||||
frame(vec![]);
|
||||
}
|
||||
let shown = menus(&ctx);
|
||||
let [menu] = shown.as_slice() else { panic!("one menu open: {shown:?}") };
|
||||
let width = |s: &str, size: f32| ctx.fonts_mut(|f| f.layout_no_wrap(crate::toolbar::pretty_shortcut(s), regular(size), Color32::WHITE).size().x);
|
||||
let content =
|
||||
items.iter().map(|(l, k)| menu_row_width(width(l, 11.5), if k.is_empty() { 0.0 } else { width(k, 11.0) + 8.0 }).ceil()).fold(0.0, f32::max);
|
||||
let expected = content + frame_margins(&ctx);
|
||||
assert!((menu.width() - expected).abs() <= 2.0, "menu {} wide, content needs {expected}", menu.width());
|
||||
// One line per row.
|
||||
let rows = 26.0 * items.len() as f32;
|
||||
assert!(menu.height() < rows + 40.0, "menu {} tall for {rows} of rows", menu.height());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_panel_dropdown_menus_fit_the_panel() {
|
||||
let mut app = FigApp::new(Session::new(), Box::new(NoServices));
|
||||
app.ui.right_tab = RightTab::Inspect;
|
||||
let ctx = egui::Context::default();
|
||||
let mut t = 0.0;
|
||||
let mut frame = |app: &mut FigApp, events: Vec<Event>| {
|
||||
let mut out = ctx.run_ui(input(&mut t, events), |ui| app.ui(ui));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
for _ in 0..3 {
|
||||
frame(&mut app, vec![]);
|
||||
}
|
||||
// With nothing selected, Inspect lists the frames ready for dev under a filter dropdown, the
|
||||
// only control on that panel: click down the panel until it opens.
|
||||
let panel_left = SCREEN.x - app.ui.right_width;
|
||||
let x = panel_left + app.ui.right_width / 2.0;
|
||||
let mut menu = None;
|
||||
for y in (140..600).step_by(6) {
|
||||
let at = pos2(x, y as f32);
|
||||
frame(&mut app, press(at, true));
|
||||
frame(&mut app, press(at, false));
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![]);
|
||||
if let [m] = menus(&ctx).as_slice() {
|
||||
menu = Some(*m);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let menu = menu.expect("the Ready for dev filter menu opens");
|
||||
// The dropdown spans the panel less its padding; its three short options fit in that.
|
||||
let dropdown = app.ui.right_width - 2.0 * crate::widgets::PAD;
|
||||
assert!(menu.width() >= dropdown - 16.0, "menu {menu:?} at least as wide as its dropdown ({dropdown})");
|
||||
assert!(menu.width() <= app.ui.right_width, "menu {menu:?} fits the {} px panel", app.ui.right_width);
|
||||
assert!(menu.max.x <= SCREEN.x && menu.min.x >= panel_left - 8.0, "menu {menu:?} inside the window, under the panel");
|
||||
}
|
||||
@@ -1912,7 +1912,7 @@ fn top_bar(p: &mut Presenter, ui: &mut Ui, bar: Rect, now: f64, chrome: Chrome)
|
||||
ui.painter().text(br.center(), Align2::CENTER_CENTER, *label, regular(11.5), text);
|
||||
}
|
||||
if *act == BarAction::Options {
|
||||
let menu = egui::Popup::menu(&resp).width(220.0).show(|ui| options_menu(p, ui, device, settings.responsive, now));
|
||||
let menu = crate::right::menu_popup(&resp).width(220.0).show(|ui| options_menu(p, ui, device, settings.responsive, now));
|
||||
action = action.or(menu.and_then(|m| m.inner));
|
||||
} else if resp.clicked() {
|
||||
action = Some(*act);
|
||||
@@ -1934,7 +1934,7 @@ fn top_bar(p: &mut Presenter, ui: &mut Ui, bar: Rect, now: f64, chrome: Chrome)
|
||||
left.rect_filled(fr, CornerRadius::same(5), hover);
|
||||
}
|
||||
left.galley(pos2(fr.min.x + 8.0, fr.center().y - galley.size().y / 2.0), galley, text);
|
||||
egui::Popup::menu(&resp).width(240.0).show(|ui| {
|
||||
crate::right::menu_popup(&resp).width(240.0).show(|ui| {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
for f in &flows {
|
||||
if crate::right::menu_check(ui, f.start == p.start, &f.name, "") {
|
||||
|
||||
@@ -13,7 +13,7 @@ use serde_json::{Value, json};
|
||||
use crate::FigApp;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::proto_panel::{action_fields, action_of, action_summary, targets_for, value_json};
|
||||
use crate::right::{menu_check, menu_item, row};
|
||||
use crate::right::{menu_check, menu_item, menu_popup, row};
|
||||
use crate::theme::{Tokens, regular, semibold};
|
||||
use crate::widgets::{self, BindEvent, FIELD_H, FieldEdit, Lead, PAD};
|
||||
|
||||
@@ -177,7 +177,7 @@ fn link_button(ui: &mut Ui, icon: Icon, label: &str) -> egui::Response {
|
||||
fn add_action_row(app: &mut FigApp, ui: &mut Ui, cx: &Cx, path: Vec<usize>) {
|
||||
row(ui, 28.0, |ui| {
|
||||
let resp = link_button(ui, Icon::Plus, "Add action");
|
||||
egui::Popup::menu(&resp).width(200.0).show(|ui| {
|
||||
menu_popup(&resp).width(200.0).show(|ui| {
|
||||
if let Some(l) = action_menu(ui) {
|
||||
match action_of(l, cx.doc, cx.page, cx.node, None) {
|
||||
Some(a) => {
|
||||
@@ -220,7 +220,7 @@ fn action_item(app: &mut FigApp, ui: &mut Ui, cx: &Cx, a: &Action, path: Vec<usi
|
||||
let w = ui.available_width() - if single { 0.0 } else { 32.0 };
|
||||
if open {
|
||||
let resp = widgets::dropdown(ui, a.label(), w, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
if let Some(l) = action_menu(ui) {
|
||||
match action_of(l, cx.doc, cx.page, cx.node, Some(a)) {
|
||||
Some(new) => set_action(app, &slot, new),
|
||||
@@ -244,7 +244,7 @@ fn action_item(app: &mut FigApp, ui: &mut Ui, cx: &Cx, a: &Action, path: Vec<usi
|
||||
}
|
||||
if !single {
|
||||
let resp = icons::button(ui, Icon::Dots, 24.0, false, "More");
|
||||
egui::Popup::menu(&resp).width(180.0).show(|ui| {
|
||||
menu_popup(&resp).width(180.0).show(|ui| {
|
||||
let mut moved = None;
|
||||
if i > 0 && menu_item(ui, "Move up", "") {
|
||||
moved = Some(i - 1);
|
||||
@@ -445,7 +445,7 @@ fn builder_row(app: &mut FigApp, ui: &mut Ui, cx: &Cx, slot: &Slot, b: usize, s:
|
||||
crate::variable_ui::open_proto(ui, VarSlot { slot: slot.clone(), part: VarPart::Lhs(b) }, None, resp.rect.left_bottom());
|
||||
}
|
||||
let resp = widgets::dropdown(ui, s.op.symbol(), op_w, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for op in ops_for(kind) {
|
||||
if menu_check(ui, *op == s.op, op.symbol(), "") {
|
||||
set_condition(app, slot, b, &with_op(s, *op, vars).expr());
|
||||
@@ -492,7 +492,7 @@ fn literal_field(ui: &mut Ui, id: Id, cur: &VariableValue, w: f32) -> Option<Var
|
||||
VariableValue::Boolean(on) => {
|
||||
let resp = widgets::dropdown(ui, if *on { "true" } else { "false" }, w, None);
|
||||
let mut out = None;
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for v in [true, false] {
|
||||
if menu_check(ui, v == *on, if v { "true" } else { "false" }, "") {
|
||||
out = (v != *on).then_some(VariableValue::Boolean(v));
|
||||
|
||||
@@ -11,7 +11,7 @@ use serde_json::{Value, json};
|
||||
use crate::FigApp;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::proto_editor::{Cx, Slot, set_action};
|
||||
use crate::right::{menu_item, row};
|
||||
use crate::right::{menu_item, menu_popup, row};
|
||||
use crate::theme::{ACCENT, Tokens, regular, semibold};
|
||||
use crate::widgets::{self, FIELD_H, FieldEdit, Lead, PAD};
|
||||
|
||||
@@ -380,7 +380,7 @@ fn settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, page: NodeId) {
|
||||
row(ui, 32.0, |ui| {
|
||||
let w = ui.available_width() - if rotates { 32.0 } else { 0.0 };
|
||||
let resp = widgets::dropdown(ui, &settings.device, w, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
let mut pick = None;
|
||||
if crate::right::menu_check(ui, settings.device == "None", "None", "") {
|
||||
pick = Some("None");
|
||||
@@ -448,12 +448,12 @@ fn device_colors(app: &mut FigApp, ui: &mut Ui, colors: &[proto::DeviceColor], c
|
||||
}
|
||||
let w = ui.available_width();
|
||||
let resp = widgets::dropdown(ui, current.map_or("", |c| c.name), w, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for c in colors {
|
||||
let on = current.is_some_and(|x| x.name == c.name);
|
||||
let label = format!("{}{}", if on { "✓ " } else { " " }, c.name);
|
||||
let paint = color_paint(*c);
|
||||
let picked = crate::right::menu_item_with(ui, &label, "", |p, r, _| {
|
||||
let picked = crate::right::menu_item_with(ui, &label, "", 22.0, |p, r, _| {
|
||||
widgets::paint_swatch(p, Rect::from_center_size(pos2(r.max.x - 16.0, r.center().y), vec2(12.0, 12.0)), &paint);
|
||||
});
|
||||
if picked {
|
||||
@@ -645,7 +645,7 @@ fn trigger_rows(app: &mut FigApp, ui: &mut Ui, node: NodeId, k: usize, it: &Inte
|
||||
widgets::sublabels(ui, "Trigger", None);
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, trigger_kind(&it.trigger), ui.available_width(), None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (i, l) in TRIGGERS.iter().enumerate() {
|
||||
if i == 4 || i == 5 || i == 9 {
|
||||
ui.separator();
|
||||
@@ -741,7 +741,7 @@ fn dest_dropdown(
|
||||
row(ui, 32.0, |ui| {
|
||||
let text = cur.map(|d| name_of(doc, d)).unwrap_or_else(|| "None".into());
|
||||
let resp = widgets::dropdown(ui, &text, ui.available_width(), Some(Icon::Frame));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
for d in options {
|
||||
let n = name_of(doc, *d);
|
||||
@@ -798,7 +798,7 @@ pub(crate) fn action_fields(app: &mut FigApp, ui: &mut Ui, doc: &Document, page:
|
||||
widgets::sublabels(ui, "Overlay position", None);
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, position.label(), ui.available_width(), None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for p in OverlayPosition::ALL {
|
||||
if menu_item(ui, p.label(), "") {
|
||||
set_action(app, slot, Action::OpenOverlay { dest, position: p, close_on_outside, background });
|
||||
@@ -845,7 +845,7 @@ pub(crate) fn action_fields(app: &mut FigApp, ui: &mut Ui, doc: &Document, page:
|
||||
widgets::sublabels(ui, "Collection", None);
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, current.map_or("Choose collection", |c| c.name.as_str()), ui.available_width(), None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for c in colls {
|
||||
if menu_item(ui, &c.name, "")
|
||||
&& let Some(m) = c.modes.first()
|
||||
@@ -861,7 +861,7 @@ pub(crate) fn action_fields(app: &mut FigApp, ui: &mut Ui, doc: &Document, page:
|
||||
let modes = current.map(|c| c.modes.as_slice()).unwrap_or_default();
|
||||
let name = modes.iter().find(|m| &m.id == mode).map_or("Choose mode", |m| m.name.as_str());
|
||||
let resp = widgets::dropdown(ui, name, ui.available_width(), None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for m in modes {
|
||||
if menu_item(ui, &m.name, "") {
|
||||
set_action(app, slot, Action::SetVariableMode { collection: collection.clone(), mode: m.id.clone() });
|
||||
@@ -881,7 +881,7 @@ fn animation_rows(app: &mut FigApp, ui: &mut Ui, node: NodeId, k: usize, it: &In
|
||||
widgets::sublabels(ui, "Animation", None);
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, it.transition.label(), ui.available_width(), None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (i, l) in TRANSITIONS.iter().enumerate() {
|
||||
if i == 3 {
|
||||
ui.separator();
|
||||
@@ -908,7 +908,7 @@ fn animation_rows(app: &mut FigApp, ui: &mut Ui, node: NodeId, k: usize, it: &In
|
||||
if !matches!(it.transition, Transition::Instant) {
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, it.easing.label(), ui.available_width() - 80.0, None);
|
||||
egui::Popup::menu(&resp).width(220.0).show(|ui| {
|
||||
menu_popup(&resp).width(220.0).show(|ui| {
|
||||
let custom = [Easing::CUSTOM_BEZIER, Easing::CUSTOM_SPRING];
|
||||
for (i, e) in Easing::ALL.iter().chain(custom.iter()).enumerate() {
|
||||
if i == 7 || i == Easing::ALL.len() {
|
||||
@@ -1050,7 +1050,7 @@ fn scroll_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId],
|
||||
ScrollPosition::Sticky => "Sticky",
|
||||
};
|
||||
let resp = widgets::dropdown(ui, cur, ui.available_width(), None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (l, v) in [("Scroll with parent", "scrolls"), ("Fixed", "fixed"), ("Sticky", "sticky")] {
|
||||
if menu_item(ui, l, "") {
|
||||
app.set_props(sel, json!({"scrollPosition": v}));
|
||||
@@ -1070,7 +1070,7 @@ fn scroll_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId],
|
||||
Overflow::Both => "Both directions",
|
||||
};
|
||||
let resp = widgets::dropdown(ui, cur, ui.available_width(), None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (l, v) in [("No scrolling", "none"), ("Horizontal", "horizontal"), ("Vertical", "vertical"), ("Both directions", "both")] {
|
||||
if menu_item(ui, l, "") {
|
||||
app.set_props(sel, json!({"overflow": v}));
|
||||
|
||||
+79
-42
@@ -87,7 +87,7 @@ pub fn right_panel(app: &mut FigApp, ui: &mut Ui) {
|
||||
if presp.clicked() {
|
||||
app.run("proto.present", json!({}));
|
||||
}
|
||||
egui::Popup::menu(&mresp).width(200.0).show(|ui| {
|
||||
menu_popup(&mresp).width(200.0).show(|ui| {
|
||||
if menu_item(ui, "Present", &present_keys) {
|
||||
app.run("proto.present", json!({}));
|
||||
ui.close();
|
||||
@@ -125,7 +125,7 @@ pub fn right_panel(app: &mut FigApp, ui: &mut Ui) {
|
||||
}
|
||||
ui.painter().text(pos2(zr.min.x + 6.0, zr.center().y), Align2::LEFT_CENTER, format!("{}%", (zoom * 100.0).round()), regular(11.0), t.text);
|
||||
icons::paint(ui.painter(), Rect::from_center_size(pos2(zr.max.x - 9.0, zr.center().y), vec2(10.0, 10.0)), Icon::Chevron, t.icon_secondary);
|
||||
egui::Popup::menu(&zresp).width(200.0).show(|ui| {
|
||||
menu_popup(&zresp).width(200.0).show(|ui| {
|
||||
for (label, cmd) in [
|
||||
("Zoom in", "view.zoomIn"),
|
||||
("Zoom out", "view.zoomOut"),
|
||||
@@ -182,33 +182,67 @@ pub fn right_panel(app: &mut FigApp, ui: &mut Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The smallest width a popup menu is measured in (see [`menu_popup`]).
|
||||
const MENU_MIN_W: f32 = 48.0;
|
||||
/// A menu row's inset either side, and the gap between its label and its shortcut or art.
|
||||
const MENU_PAD: f32 = 8.0;
|
||||
const MENU_GAP: f32 = 24.0;
|
||||
|
||||
/// The style of popup menus: egui's, with text kept on one line (a menu grows to its widest item).
|
||||
fn menu_style(s: &mut egui::Style) {
|
||||
egui::containers::menu::menu_style(s);
|
||||
s.wrap_mode = Some(egui::TextWrapMode::Extend);
|
||||
}
|
||||
|
||||
/// The menu a dropdown or button opens: as wide as its widest row and at least as wide as
|
||||
/// `anchor`. egui measures a popup in a first, invisible pass as wide as the width it is given
|
||||
/// (else its 600 px default area width) and keeps the size measured; rows ([`menu_row`]) take the
|
||||
/// larger of that width and their content, so the menu fits its content. Chain `.width(w)` for a
|
||||
/// menu at least `w` wide.
|
||||
pub fn menu_popup(anchor: &egui::Response) -> egui::Popup<'static> {
|
||||
egui::Popup::menu(anchor).width(anchor.rect.width().max(MENU_MIN_W)).style(menu_style)
|
||||
}
|
||||
|
||||
/// A context menu (secondary click on `resp`) sized like [`menu_popup`].
|
||||
pub fn context_popup(resp: &egui::Response) -> egui::Popup<'static> {
|
||||
egui::Popup::context_menu(resp).width(MENU_MIN_W).style(menu_style)
|
||||
}
|
||||
|
||||
pub fn menu_item(ui: &mut Ui, label: &str, shortcut: &str) -> bool {
|
||||
menu_row(ui, label, shortcut, |_, _, _| {}).clicked()
|
||||
menu_row(ui, label, shortcut, 0.0, |_, _, _| {}).clicked()
|
||||
}
|
||||
|
||||
/// A menu row with art drawn into it after the label (`art(painter, row rect, text colour)`).
|
||||
pub fn menu_item_with(ui: &mut Ui, label: &str, shortcut: &str, art: impl FnOnce(&egui::Painter, Rect, Color32)) -> bool {
|
||||
menu_row(ui, label, shortcut, art).clicked()
|
||||
/// A menu row with art drawn into it after the label (`art(painter, row rect, text colour)`),
|
||||
/// taking `art_w` from the row's right edge.
|
||||
pub fn menu_item_with(ui: &mut Ui, label: &str, shortcut: &str, art_w: f32, art: impl FnOnce(&egui::Painter, Rect, Color32)) -> bool {
|
||||
menu_row(ui, label, shortcut, art_w, art).clicked()
|
||||
}
|
||||
|
||||
/// A menu row (see [`menu_item_with`]); its response tells hovering too.
|
||||
pub fn menu_row(ui: &mut Ui, label: &str, shortcut: &str, art: impl FnOnce(&egui::Painter, Rect, Color32)) -> egui::Response {
|
||||
/// The width a menu row needs for a label `label_w` wide and, at its right end, a shortcut or
|
||||
/// art `trail` wide (0 for neither).
|
||||
pub(crate) fn menu_row_width(label_w: f32, trail: f32) -> f32 {
|
||||
MENU_PAD + label_w + if trail > 0.0 { MENU_GAP + trail } else { MENU_PAD }
|
||||
}
|
||||
|
||||
/// A menu row (see [`menu_item_with`]); its response tells hovering too. It spans the menu and
|
||||
/// is at least as wide as its label, shortcut and art.
|
||||
pub fn menu_row(ui: &mut Ui, label: &str, shortcut: &str, art_w: f32, art: impl FnOnce(&egui::Painter, Rect, Color32)) -> egui::Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let w = ui.available_width().max(180.0);
|
||||
let text = ui.painter().layout_no_wrap(label.to_owned(), regular(11.5), Color32::PLACEHOLDER);
|
||||
let keys =
|
||||
(!shortcut.is_empty()).then(|| ui.painter().layout_no_wrap(crate::toolbar::pretty_shortcut(shortcut), regular(11.0), Color32::PLACEHOLDER));
|
||||
let trail = keys.as_ref().map_or(0.0, |k| k.size().x + MENU_PAD).max(art_w);
|
||||
let w = ui.available_width().max(menu_row_width(text.size().x, trail).ceil());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 26.0), Sense::click());
|
||||
if resp.hovered() {
|
||||
let hovered = resp.hovered();
|
||||
if hovered {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), ACCENT);
|
||||
}
|
||||
let col = if resp.hovered() { Color32::WHITE } else { t.text };
|
||||
ui.painter().text(pos2(r.min.x + 8.0, r.center().y), Align2::LEFT_CENTER, label, regular(11.5), col);
|
||||
if !shortcut.is_empty() {
|
||||
ui.painter().text(
|
||||
pos2(r.max.x - 8.0, r.center().y),
|
||||
Align2::RIGHT_CENTER,
|
||||
crate::toolbar::pretty_shortcut(shortcut),
|
||||
regular(11.0),
|
||||
if resp.hovered() { Color32::WHITE } else { t.text_secondary },
|
||||
);
|
||||
let col = if hovered { Color32::WHITE } else { t.text };
|
||||
ui.painter().galley(pos2(r.min.x + MENU_PAD, r.center().y - text.size().y / 2.0), text, col);
|
||||
if let Some(k) = keys {
|
||||
let at = pos2(r.max.x - MENU_PAD - k.size().x, r.center().y - k.size().y / 2.0);
|
||||
ui.painter().galley(at, k, if hovered { Color32::WHITE } else { t.text_secondary });
|
||||
}
|
||||
art(ui.painter(), r, col);
|
||||
resp
|
||||
@@ -325,7 +359,7 @@ fn page_section(app: &mut FigApp, ui: &mut Ui, doc: &Document) {
|
||||
widgets::section_header(ui, "Color profile", &[], false);
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, doc.color_profile.label(), 140.0, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for p in figcraft_color::ColorProfile::ALL {
|
||||
if menu_item(ui, p.label(), "") {
|
||||
if p != doc.color_profile {
|
||||
@@ -402,7 +436,7 @@ fn header(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
app.run("frame.swapOrientation", json!({"ids": sel}));
|
||||
}
|
||||
let ids: Vec<NodeId> = sel.to_vec();
|
||||
egui::Popup::menu(&resp).width(260.0).show(|ui| {
|
||||
menu_popup(&resp).width(260.0).show(|ui| {
|
||||
egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| {
|
||||
let mut cat = "";
|
||||
for (c, name, pw, ph) in figcraft_engine::cmd::create::FRAME_PRESETS {
|
||||
@@ -433,7 +467,7 @@ fn header(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
}
|
||||
icons::paint(ui.painter(), br.shrink(5.0), icon, t.icon);
|
||||
if cmd == "bool.union" {
|
||||
egui::Popup::menu(&bresp).width(180.0).show(|ui| {
|
||||
menu_popup(&bresp).width(180.0).show(|ui| {
|
||||
for (l, c) in [
|
||||
("Union", "bool.union"),
|
||||
("Subtract", "bool.subtract"),
|
||||
@@ -479,7 +513,7 @@ fn position(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
}
|
||||
if sel.len() > 2 {
|
||||
let more = icons::button(ui, Icon::DistributeH, 24.0, false, "Distribute and tidy");
|
||||
egui::Popup::menu(&more).show(|ui| {
|
||||
menu_popup(&more).show(|ui| {
|
||||
for (l, c) in [
|
||||
("Distribute horizontal spacing", "distribute.horizontal"),
|
||||
("Distribute vertical spacing", "distribute.vertical"),
|
||||
@@ -535,7 +569,7 @@ fn position(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
row(ui, 32.0, |ui| {
|
||||
let h = c.map(|c| format!("{:?}", c.horizontal)).unwrap_or_else(|| "Mixed".into());
|
||||
let resp = widgets::dropdown(ui, &pretty(&h), col, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (l, v) in [("Left", "left"), ("Right", "right"), ("Left & right", "leftRight"), ("Center", "center"), ("Scale", "scale")] {
|
||||
if menu_item(ui, l, "") {
|
||||
app.set_props(&ids, json!({"constraints": {"horizontal": v}}));
|
||||
@@ -545,7 +579,7 @@ fn position(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
});
|
||||
let v = c.map(|c| format!("{:?}", c.vertical)).unwrap_or_else(|| "Mixed".into());
|
||||
let resp = widgets::dropdown(ui, &pretty(&v), col, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (l, val) in [("Top", "top"), ("Bottom", "bottom"), ("Top & bottom", "topBottom"), ("Center", "center"), ("Scale", "scale")] {
|
||||
if menu_item(ui, l, "") {
|
||||
app.set_props(&ids, json!({"constraints": {"vertical": val}}));
|
||||
@@ -596,7 +630,7 @@ fn appearance(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
widgets::sublabels(ui, "Mask", None);
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, mt.map(MaskType::label).unwrap_or("Mixed"), col * 2.0 + 8.0, Some(Icon::Mask));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for m in MaskType::ALL {
|
||||
if menu_item(ui, m.label(), "") {
|
||||
app.set_props(&ids, json!({"maskType": m.id()}));
|
||||
@@ -683,7 +717,7 @@ fn appearance(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
row(ui, 32.0, |ui| {
|
||||
let label = blend.map(|b| b.label()).unwrap_or("Mixed");
|
||||
let resp = widgets::dropdown(ui, label, col * 2.0 + 8.0, Some(Icon::Blend));
|
||||
egui::Popup::menu(&resp).width(200.0).show(|ui| {
|
||||
menu_popup(&resp).width(200.0).show(|ui| {
|
||||
for b in BlendMode::ALL {
|
||||
if menu_item(ui, b.label(), "") {
|
||||
app.set_props(&ids, json!({"blend": b.css()}));
|
||||
@@ -754,7 +788,7 @@ fn variable_modes(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId])
|
||||
ui.interact(rect, Id::new(("var_mode_name", &c.id)), Sense::hover()).on_hover_text(&hover);
|
||||
}
|
||||
let resp = widgets::elided_dropdown(ui, &label, pick_w);
|
||||
egui::Popup::menu(&resp).width(180.0).show(|ui| {
|
||||
menu_popup(&resp).width(180.0).show(|ui| {
|
||||
let mut pick = None;
|
||||
if menu_check(ui, own == Some(None), &auto, "") {
|
||||
pick = Some(Value::Null);
|
||||
@@ -900,7 +934,7 @@ fn paints(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId], fills:
|
||||
half,
|
||||
None,
|
||||
);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (l, v) in [("Inside", "inside"), ("Center", "center"), ("Outside", "outside")] {
|
||||
if menu_item(ui, l, "") {
|
||||
app.set_props(&ids, json!({"strokeAlign": v}));
|
||||
@@ -915,7 +949,7 @@ fn paints(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId], fills:
|
||||
app.field(&ids, "strokeWeight", e, |v| json!(v.max(0.0)));
|
||||
if sided {
|
||||
let sides_btn = icons::button(ui, Icon::IndependentCorners, 24.0, false, "Individual strokes");
|
||||
egui::Popup::menu(&sides_btn).show(|ui| {
|
||||
menu_popup(&sides_btn).show(|ui| {
|
||||
let w = weight.unwrap_or(1.0);
|
||||
for (l, v) in [
|
||||
("All", Value::Null),
|
||||
@@ -1008,14 +1042,14 @@ fn width_profile_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], profile: Opt
|
||||
let art = Rect::from_min_max(pos2(r.max.x - 22.0 - 56.0, r.center().y - 5.0), pos2(r.max.x - 22.0, r.center().y + 5.0));
|
||||
profile_art(ui.painter(), art, p.as_ref(), t.icon_secondary);
|
||||
}
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for preset in WidthPreset::ALL {
|
||||
let prof = preset.profile();
|
||||
let art = |pt: &egui::Painter, r: Rect, col: Color32| {
|
||||
let a = Rect::from_min_max(pos2(r.max.x - 8.0 - 64.0, r.center().y - 5.0), pos2(r.max.x - 8.0, r.center().y + 5.0));
|
||||
profile_art(pt, a, prof.as_ref(), col);
|
||||
};
|
||||
if menu_item_with(ui, preset.label(), "", art) {
|
||||
if menu_item_with(ui, preset.label(), "", 8.0 + 64.0, art) {
|
||||
app.run("stroke.widthProfile", json!({"ids": ids, "preset": preset.id()}));
|
||||
ui.close();
|
||||
}
|
||||
@@ -1044,7 +1078,7 @@ fn stroke_settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]
|
||||
row(ui, 32.0, |ui| {
|
||||
let dashed = dashes.as_ref().is_some_and(|d| !d.is_empty());
|
||||
let resp = widgets::dropdown(ui, if dashed { "Dash" } else { "Solid" }, half, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
if menu_item(ui, "Solid", "") {
|
||||
app.set_props(&ids, json!({"dashes": []}));
|
||||
ui.close();
|
||||
@@ -1076,7 +1110,7 @@ fn stroke_settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]
|
||||
None => "Mixed",
|
||||
};
|
||||
let resp = widgets::dropdown(ui, label, half, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (l, v) in [("Miter", "miter"), ("Bevel", "bevel"), ("Round", "round")] {
|
||||
if menu_item(ui, l, "") {
|
||||
app.set_props(&ids, json!({"strokeJoin": v}));
|
||||
@@ -1097,9 +1131,9 @@ fn stroke_settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]
|
||||
row(ui, 32.0, |ui| {
|
||||
for (key, cap) in [("startCap", st.as_ref().map(|s| s.cap_start)), ("endCap", st.as_ref().map(|s| s.cap_end))] {
|
||||
let resp = widgets::dropdown(ui, cap.map(StrokeCap::label).unwrap_or("Mixed"), half, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for c in StrokeCap::ALL {
|
||||
if menu_item_with(ui, c.label(), "", |p, r, col| cap_art(p, r, col, c, key == "startCap")) {
|
||||
if menu_item_with(ui, c.label(), "", CAP_ART_W, |p, r, col| cap_art(p, r, col, c, key == "startCap")) {
|
||||
app.set_props(&ids, json!({key: format!("{c:?}")}));
|
||||
ui.close();
|
||||
}
|
||||
@@ -1111,6 +1145,9 @@ fn stroke_settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
|
||||
/// The width [`cap_art`] takes at the right of a menu row (its arrows reach past the line's ends).
|
||||
const CAP_ART_W: f32 = 64.0;
|
||||
|
||||
/// A short stroke ending in `cap` (on its left end for a start point), drawn at the right of a menu
|
||||
/// row.
|
||||
fn cap_art(p: &egui::Painter, r: Rect, col: Color32, cap: StrokeCap, start: bool) {
|
||||
@@ -1270,7 +1307,7 @@ fn effects(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
}
|
||||
let w = ui.available_width() - 56.0;
|
||||
let resp = widgets::dropdown(ui, e.label(), w, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (l, ty) in Effect::TYPES {
|
||||
if menu_item(ui, l, "")
|
||||
&& let Some(ne) = Effect::of_type(ty)
|
||||
@@ -1389,7 +1426,7 @@ fn effect_settings(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, e: &
|
||||
};
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, nz.mode.label(), half * 2.0 + 8.0, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for m in NoiseMode::ALL {
|
||||
if menu_item(ui, m.label(), "") {
|
||||
set(app, with(&|n| n.mode = m), None);
|
||||
@@ -1538,7 +1575,7 @@ fn grids(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
}
|
||||
let w = ui.available_width() - 56.0;
|
||||
let resp = widgets::dropdown(ui, &grid_label(g), w, None);
|
||||
egui::Popup::menu(&resp).width(220.0).show(|ui| {
|
||||
menu_popup(&resp).width(220.0).show(|ui| {
|
||||
for (l, kind) in [("Grid", GridKind::Grid), ("Columns", GridKind::Columns), ("Rows", GridKind::Rows)] {
|
||||
if menu_item(ui, l, "") {
|
||||
let fresh = match kind {
|
||||
@@ -1614,7 +1651,7 @@ fn grid_settings(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], list: &[LayoutGr
|
||||
let aligns = [GridAlign::Stretch, GridAlign::Min, GridAlign::Center, GridAlign::Max];
|
||||
let cur = aligns.iter().position(|a| *a == g.align).unwrap_or(0);
|
||||
let resp = widgets::dropdown(ui, names.get(cur).copied().unwrap_or_default(), col, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (k, (name, a)) in names.iter().zip(aligns).enumerate() {
|
||||
if menu_check(ui, k == cur, name, "") {
|
||||
// Fixed-size columns need a size of their own.
|
||||
@@ -1792,7 +1829,7 @@ fn inspect(app: &mut FigApp, ui: &mut Ui) {
|
||||
let copy = widgets::section_header(ui, "Code", &[(Icon::Export, "Copy")], false).is_some();
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::dropdown(ui, lang.label(), 120.0, Some(Icon::Code));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (i, l) in CodeLang::ALL.iter().enumerate() {
|
||||
if menu_item(ui, l.label(), "") {
|
||||
ui.data_mut(|d| d.insert_temp(lang_id, i));
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde_json::json;
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::icons::Icon;
|
||||
use crate::right::{common, menu_item, row};
|
||||
use crate::right::{common, menu_item, menu_popup, row};
|
||||
use crate::widgets;
|
||||
|
||||
/// Section fill presets: light tints that keep dark text readable.
|
||||
@@ -45,7 +45,7 @@ pub fn status_dropdown(app: &mut FigApp, ui: &mut Ui, doc: &Document, ids: &[Nod
|
||||
None => "Mixed",
|
||||
};
|
||||
let resp = widgets::dropdown(ui, label, width, Some(Icon::Code));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for d in DevStatus::ALL {
|
||||
if menu_item(ui, d.label(), "") {
|
||||
app.set_props(ids, json!({"devStatus": d.id()}));
|
||||
|
||||
@@ -695,7 +695,7 @@ fn text_properties(ui: &mut Ui, id: &str, def: &TextStyle, out: &mut Frame) {
|
||||
});
|
||||
(family_resp, style_resp)
|
||||
});
|
||||
egui::Popup::menu(&family_resp).width(260.0).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
crate::right::menu_popup(&family_resp).width(260.0).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
if let Some(f) = crate::typography::font_picker(ui, Some(&def.font_family), family_resp.clicked()) {
|
||||
set(out, &|d| {
|
||||
d.font_family = f.clone();
|
||||
@@ -708,7 +708,7 @@ fn text_properties(ui: &mut Ui, id: &str, def: &TextStyle, out: &mut Frame) {
|
||||
}
|
||||
});
|
||||
if let Some(sr) = style_resp {
|
||||
egui::Popup::menu(&sr).width(200.0).show(|ui| {
|
||||
crate::right::menu_popup(&sr).width(200.0).show(|ui| {
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
for s in figcraft_text::family_styles(&def.font_family) {
|
||||
let mark = if def.font_style == s { "✓ " } else { " " };
|
||||
|
||||
@@ -148,7 +148,7 @@ pub fn show(app: &mut FigApp, ui: &mut Ui) {
|
||||
ui.painter().circle_filled(hr.center() + vec2(0.0, 1.0), 17.0, Color32::from_black_alpha(20));
|
||||
ui.painter().circle_filled(hr.center(), 16.0, t.panel);
|
||||
ui.painter().text(hr.center(), Align2::CENTER_CENTER, "?", crate::theme::regular(15.0), t.text);
|
||||
egui::Popup::menu(&hresp).show(|ui| {
|
||||
crate::right::menu_popup(&hresp).show(|ui| {
|
||||
for (l, c) in [("Keyboard shortcuts", "help.shortcuts"), ("Help page", "help.page"), ("About FigCraft", "help.about")] {
|
||||
if crate::right::menu_item(ui, l, figcraft_engine::catalog::shortcut(c).unwrap_or("")) {
|
||||
app.run(c, json!({}));
|
||||
|
||||
@@ -13,7 +13,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::right::{common, menu_item, row};
|
||||
use crate::right::{common, menu_item, menu_popup, row};
|
||||
use crate::theme::{Tokens, regular, semibold};
|
||||
use crate::widgets::{self, FIELD_H, FieldEdit, Lead, PAD};
|
||||
|
||||
@@ -137,7 +137,7 @@ fn font_rows(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], styles: &[TextStyle]
|
||||
row(ui, 32.0, |ui| {
|
||||
let w = ui.available_width();
|
||||
let resp = widgets::elided_dropdown(ui, family.as_deref().unwrap_or(MIXED), w);
|
||||
egui::Popup::menu(&resp).width(w.max(240.0)).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
menu_popup(&resp).width(w.max(240.0)).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
if let Some(f) = font_picker(ui, family.as_deref(), resp.clicked()) {
|
||||
remember_family(ui.ctx(), &f);
|
||||
apply_style(app, "fontFamily", json!(f));
|
||||
@@ -151,7 +151,7 @@ fn font_rows(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], styles: &[TextStyle]
|
||||
row(ui, 32.0, |ui| {
|
||||
let resp = widgets::elided_dropdown(ui, fstyle.as_deref().unwrap_or(MIXED), half);
|
||||
let fam = family.clone();
|
||||
egui::Popup::menu(&resp).width(200.0).show(|ui| match &fam {
|
||||
menu_popup(&resp).width(200.0).show(|ui| match &fam {
|
||||
Some(fam) => {
|
||||
let list = figcraft_text::family_styles(fam);
|
||||
if list.is_empty() {
|
||||
@@ -191,7 +191,7 @@ fn font_rows(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], styles: &[TextStyle]
|
||||
None => {}
|
||||
}
|
||||
let more = icons::button(ui, Icon::Chevron, 20.0, false, "Font size");
|
||||
egui::Popup::menu(&more).width(90.0).show(|ui| {
|
||||
menu_popup(&more).width(90.0).show(|ui| {
|
||||
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| {
|
||||
for v in SIZES {
|
||||
let mark = if size == Some(v) { "✓ " } else { " " };
|
||||
@@ -300,7 +300,7 @@ fn paragraph_rows(
|
||||
|
||||
/// A menu item that also reports the pointer being over it: (clicked, hovered).
|
||||
fn menu_item_hover(ui: &mut Ui, label: &str) -> (bool, bool) {
|
||||
let r = crate::right::menu_row(ui, label, "", |_, _, _| {});
|
||||
let r = crate::right::menu_row(ui, label, "", 0.0, |_, _, _| {});
|
||||
(r.clicked(), r.hovered())
|
||||
}
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ fn collection_row(app: &mut FigApp, ui: &mut Ui, view: &View, c: &Collection, st
|
||||
if resp.double_clicked() {
|
||||
start_rename(ui, st, target.clone());
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
crate::right::context_popup(&resp).show(|ui| {
|
||||
if menu_item(ui, "Rename", "") {
|
||||
start_rename(ui, st, target.clone());
|
||||
ui.close();
|
||||
@@ -368,7 +368,7 @@ fn group_rows(app: &mut FigApp, ui: &mut Ui, view: &View, coll: &Collection, st:
|
||||
if resp.double_clicked() {
|
||||
start_rename(ui, st, target.clone());
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
crate::right::context_popup(&resp).show(|ui| {
|
||||
if menu_item(ui, "Rename group", "") {
|
||||
start_rename(ui, st, target.clone());
|
||||
ui.close();
|
||||
@@ -532,7 +532,7 @@ fn header_row(app: &mut FigApp, ui: &mut Ui, view: &View, coll: &Collection, st:
|
||||
if resp.double_clicked() {
|
||||
start_rename(ui, st, target.clone());
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
crate::right::context_popup(&resp).show(|ui| {
|
||||
if menu_item(ui, "Rename mode", "") {
|
||||
start_rename(ui, st, target.clone());
|
||||
ui.close();
|
||||
@@ -584,7 +584,7 @@ fn var_row(app: &mut FigApp, ui: &mut Ui, view: &View, coll: &Collection, v: &Va
|
||||
if resp.double_clicked() && ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| p.x < name_r.max.x) {
|
||||
start_rename(ui, st, target.clone());
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
crate::right::context_popup(&resp).show(|ui| {
|
||||
if menu_item(ui, "Edit variable", "") {
|
||||
st.details = Some(v.id.clone());
|
||||
ui.close();
|
||||
@@ -766,7 +766,7 @@ fn create_row(app: &mut FigApp, ui: &mut Ui, view: &View, coll: &Collection, st:
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(12.0);
|
||||
let resp = widgets::button(ui, "+ Create variable", false, true);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
crate::right::menu_popup(&resp).show(|ui| {
|
||||
for kind in TYPES {
|
||||
let label = match kind {
|
||||
VariableType::Color => "Color",
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::icons::Icon;
|
||||
use crate::right::{menu_item, row};
|
||||
use crate::right::{menu_item, menu_popup, row};
|
||||
use crate::widgets::{self, FieldEdit, Lead, PAD};
|
||||
|
||||
/// Run a point command from a field: typed values are one step, scrubs one step when released.
|
||||
@@ -116,7 +116,7 @@ pub fn points(app: &mut FigApp, ui: &mut Ui, doc: &Document, ve: &VectorEdit) {
|
||||
HandleMirroring::AngleAndLength => "Mirror angle and length",
|
||||
};
|
||||
let resp = widgets::dropdown(ui, mirror.map(label).unwrap_or("Mixed"), col * 2.0 + 8.0, None);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for m in [HandleMirroring::None, HandleMirroring::Angle, HandleMirroring::AngleAndLength] {
|
||||
if menu_item(ui, label(m), "") {
|
||||
app.run("vector.edit.mirroring", json!({"mode": m.id()}));
|
||||
|
||||
@@ -375,8 +375,8 @@ fn version_row(ui: &mut Ui, row: &Row, last: bool, previewing: bool) -> Option<A
|
||||
}
|
||||
}
|
||||
};
|
||||
egui::Popup::menu(&more).show(&mut menu);
|
||||
egui::Popup::context_menu(&resp).show(&mut menu);
|
||||
crate::right::menu_popup(&more).show(&mut menu);
|
||||
crate::right::context_popup(&resp).show(&mut menu);
|
||||
if resp.double_clicked() {
|
||||
out = Some(Action::Edit(row.id.clone()));
|
||||
} else if resp.clicked() && !previewing && out.is_none() {
|
||||
|
||||
@@ -351,7 +351,7 @@ pub fn combo_field(ui: &mut Ui, id: Id, value: &str, width: f32, options: &[Stri
|
||||
ui.painter().rect_filled(chev.shrink(2.0), CornerRadius::same(4), t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), Rect::from_center_size(chev.center(), vec2(12.0, 12.0)), Icon::Chevron, t.icon_secondary);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
crate::right::menu_popup(&resp).show(|ui| {
|
||||
for o in options {
|
||||
if crate::right::menu_item(ui, o, "") {
|
||||
out = Some(o.clone());
|
||||
|
||||
Reference in New Issue
Block a user