mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-30 13:16:56 -06:00
Conflict: crates/ui-egui/src/chrome.rs: next's horizontally scrolling app bar kept; its workspace menu and Delete Workspace submenu call #281's menus::menu_button Conflict: crates/ui-egui/src/menus.rs: kept next's activate_native/native_menu_enabled/forward_to_text_field and #281's menu_button; #281's menu_bar keeps #249's top_down_justified rows
This commit is contained in:
@@ -58,52 +58,67 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.add_space(8.0);
|
||||
let current = app.ui.workspace.clone();
|
||||
let shown = crate::i18n::workspace_name(&app.ui.language, ¤t);
|
||||
ui.menu_button(crate::rtl::widget(ui, egui::RichText::new(format!("{shown} ▾")).font(semibold(11.5)).color(t.text)), |ui| {
|
||||
let customs: Vec<String> = app.ui.custom_workspaces.iter().map(|w| w.name.clone()).collect();
|
||||
for w in
|
||||
["Essentials", "Advanced", "Book", "Digital Publishing", "Interactive for PDF", "Printing and Proofing", "Typography"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.chain(customs.iter().cloned())
|
||||
{
|
||||
crate::menus::menu_button(
|
||||
ui,
|
||||
crate::rtl::widget(ui, egui::RichText::new(format!("{shown} ▾")).font(semibold(11.5)).color(t.text)),
|
||||
|ui| {
|
||||
let customs: Vec<String> = app.ui.custom_workspaces.iter().map(|w| w.name.clone()).collect();
|
||||
for w in [
|
||||
"Essentials",
|
||||
"Advanced",
|
||||
"Book",
|
||||
"Digital Publishing",
|
||||
"Interactive for PDF",
|
||||
"Printing and Proofing",
|
||||
"Typography",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.chain(customs.iter().cloned())
|
||||
{
|
||||
if ui
|
||||
.selectable_label(w == current, crate::rtl::widget(ui, crate::i18n::workspace_name(&app.ui.language, &w)))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("window.workspace", json!({"name": w}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.selectable_label(w == current, crate::rtl::widget(ui, crate::i18n::workspace_name(&app.ui.language, &w)))
|
||||
.button(crate::rtl::widget(
|
||||
ui,
|
||||
format!(
|
||||
"{} {}",
|
||||
crate::i18n::tr(&app.ui.language, "Reset"),
|
||||
crate::i18n::workspace_name(&app.ui.language, ¤t)
|
||||
),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("window.workspace", json!({"name": w}));
|
||||
let _ = app.run("window.resetWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.button(crate::rtl::widget(
|
||||
ui,
|
||||
format!(
|
||||
"{} {}",
|
||||
crate::i18n::tr(&app.ui.language, "Reset"),
|
||||
crate::i18n::workspace_name(&app.ui.language, ¤t)
|
||||
),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("window.resetWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Workspace…"))).clicked() {
|
||||
let _ = app.run("window.newWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if !customs.is_empty() {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Workspace")), |ui| {
|
||||
for w in &customs {
|
||||
if ui.button(w).clicked() {
|
||||
let _ = app.run("window.deleteWorkspace", json!({"name": w}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Workspace…"))).clicked() {
|
||||
let _ = app.run("window.newWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if !customs.is_empty() {
|
||||
crate::menus::menu_button(
|
||||
ui,
|
||||
crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Workspace")),
|
||||
|ui| {
|
||||
for w in &customs {
|
||||
if ui.button(w).clicked() {
|
||||
let _ = app.run("window.deleteWorkspace", json!({"name": w}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
if icons::button(ui, "share", 22.0, false, crate::i18n::tr(&app.ui.language, "Share")).clicked() {
|
||||
app.status("Export a PDF, IDML or package to share — no cloud account needed.");
|
||||
@@ -336,7 +351,7 @@ pub fn status_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
return;
|
||||
}
|
||||
let z = app.view().map(|v| v.zoom).unwrap_or(1.0);
|
||||
ui.menu_button(egui::RichText::new(format!("{:.2}% ▾", z * 100.0)).font(small.clone()), |ui| {
|
||||
crate::menus::menu_button(ui, egui::RichText::new(format!("{:.2}% ▾", z * 100.0)).font(small.clone()), |ui| {
|
||||
for p in [0.05, 0.125, 0.25, 0.5, 0.75, 1.0, 2.0, 4.0, 8.0, 16.0] {
|
||||
if ui.button(format!("{}%", p * 100.0)).clicked() {
|
||||
let _ = app.run("view.zoom", json!({"zoom": p}));
|
||||
|
||||
+223
-47
@@ -1884,48 +1884,59 @@ fn forward_to_text_field(app: &mut DesignApp, ctx: &egui::Context, id: &str) ->
|
||||
true
|
||||
}
|
||||
|
||||
/// The menu bar contents (inside the app bar; macOS uses the native menu instead).
|
||||
pub fn menu_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let lang = app.ui.language.clone();
|
||||
let mut menus = menu_tree();
|
||||
if crate::i18n::is_rtl(&lang) {
|
||||
menus.reverse();
|
||||
}
|
||||
for (menu, entries) in menus {
|
||||
egui::containers::menu::MenuButton::new(crate::rtl::widget(ui, crate::i18n::tr(&lang, menu)))
|
||||
.config(egui::containers::menu::MenuConfig::new().close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside))
|
||||
.ui(ui, |ui| {
|
||||
if crate::i18n::is_rtl(&lang) {
|
||||
ui.set_max_width(320.0);
|
||||
}
|
||||
scroll_menu(ui, |ui| {
|
||||
ui.with_layout(
|
||||
egui::Layout::top_down_justified(if crate::i18n::is_rtl(&lang) { egui::Align::Max } else { egui::Align::Min }),
|
||||
|ui| {
|
||||
ui.set_min_width(240.0);
|
||||
let hidden = menu_items(app, ui, &entries, menu);
|
||||
if hidden > 0 && !app.ui.show_full_menus {
|
||||
ui.separator();
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&lang, "Show All Menu Items"))).clicked() {
|
||||
app.ui.show_full_menus = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
/// A menu button whose popup stays within the viewport and scrolls when needed.
|
||||
/// All menu buttons share this wrapper, including workspace, zoom and panel menus.
|
||||
pub fn menu_button<'a, R>(
|
||||
ui: &mut egui::Ui,
|
||||
atoms: impl egui::IntoAtoms<'a>,
|
||||
add_contents: impl FnOnce(&mut egui::Ui) -> R,
|
||||
) -> egui::InnerResponse<Option<R>> {
|
||||
use egui::containers::menu::{MenuButton, MenuConfig};
|
||||
|
||||
// A menu opens from a button already shown in the previous frame. Base its height on
|
||||
// that anchor, not the popup's eventual position, so placement cannot oscillate.
|
||||
let button = ui.ctx().read_response(ui.next_auto_id()).map(|response| response.rect);
|
||||
let max_height = menu_max_height(ui, button);
|
||||
let contents = |ui: &mut egui::Ui| {
|
||||
// Override the popup's initial available height, including when it opens upwards.
|
||||
ui.set_max_height(max_height);
|
||||
let mut scroll = egui::ScrollArea::vertical().max_height(max_height).min_scrolled_height(1.0);
|
||||
if ui.is_sizing_pass() {
|
||||
// Closing and reopening a menu starts at its first command again.
|
||||
scroll = scroll.vertical_scroll_offset(0.0);
|
||||
}
|
||||
scroll.show(ui, add_contents).inner
|
||||
};
|
||||
// Scrollbar clicks must not dismiss the popup; command rows close it explicitly.
|
||||
let config = MenuConfig::find(ui).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside);
|
||||
let (response, inner) = if egui::containers::menu::is_in_menu(ui) {
|
||||
clipped_submenu(ui, atoms, config, contents)
|
||||
} else {
|
||||
MenuButton::new(atoms).config(config).ui(ui, contents)
|
||||
};
|
||||
egui::InnerResponse { response, inner: inner.map(|response| response.inner) }
|
||||
}
|
||||
|
||||
/// Keep every command reachable when the viewport is short or the UI is enlarged.
|
||||
fn scroll_menu<R>(ui: &mut egui::Ui, contents: impl FnOnce(&mut egui::Ui) -> R) -> R {
|
||||
let frame_margin = egui::Frame::popup(ui.style()).total_margin().sum().y;
|
||||
let max_height = (ui.ctx().content_rect().height() - frame_margin).max(1.0);
|
||||
ui.set_max_height(max_height);
|
||||
egui::ScrollArea::vertical().max_height(max_height).show(ui, contents).inner
|
||||
/// The tallest menu that fits on either side of its anchor, including the popup frame.
|
||||
fn menu_max_height(ui: &egui::Ui, button: Option<egui::Rect>) -> f32 {
|
||||
let screen = ui.ctx().content_rect();
|
||||
let space = match button {
|
||||
// Submenus open beside their row, aligned with its top or bottom.
|
||||
Some(rect) if egui::containers::menu::is_in_menu(ui) => (screen.bottom() - rect.top()).max(rect.bottom() - screen.top()),
|
||||
Some(rect) => (screen.bottom() - rect.bottom()).max(rect.top() - screen.top()),
|
||||
None => screen.height(),
|
||||
};
|
||||
let chrome = egui::Frame::menu(ui.style()).total_margin().sum().y + 8.0;
|
||||
(space.min(screen.height()) - chrome).max(1.0)
|
||||
}
|
||||
|
||||
fn clipped_submenu<R>(ui: &mut egui::Ui, label: egui::WidgetText, contents: impl FnOnce(&mut egui::Ui) -> R) -> Option<R> {
|
||||
/// egui's submenu hover test uses the full row rect, even outside a scroll area's clip.
|
||||
fn clipped_submenu<'a, R>(
|
||||
ui: &mut egui::Ui,
|
||||
atoms: impl egui::IntoAtoms<'a>,
|
||||
config: egui::containers::menu::MenuConfig,
|
||||
contents: impl FnOnce(&mut egui::Ui) -> R,
|
||||
) -> (egui::Response, Option<egui::InnerResponse<R>>) {
|
||||
use egui::containers::menu::{MenuState, SubMenu, SubMenuButton};
|
||||
|
||||
let id = SubMenu::id_from_widget_id(ui.next_auto_id());
|
||||
@@ -1934,21 +1945,49 @@ fn clipped_submenu<R>(ui: &mut egui::Ui, label: egui::WidgetText, contents: impl
|
||||
if open {
|
||||
ui.style_mut().visuals.widgets.inactive = ui.style().visuals.widgets.open;
|
||||
}
|
||||
let button = SubMenuButton::new(label);
|
||||
let mut response = ui.add(button.button);
|
||||
let button = SubMenuButton::new(atoms).config(config);
|
||||
let response = ui.add(button.button);
|
||||
ui.style_mut().visuals.widgets.inactive = inactive;
|
||||
let clip = ui.clip_rect();
|
||||
if !response.rect.intersect(clip).is_positive() {
|
||||
if open {
|
||||
MenuState::from_ui(ui, |state, _| state.open_item = None);
|
||||
}
|
||||
return None;
|
||||
return (response, None);
|
||||
}
|
||||
// egui expands an unclipped hover rectangle by half the item spacing.
|
||||
// Undo the half-spacing expansion used by SubMenu::show after clamping to the clip.
|
||||
let hover_margin = ui.spacing().item_spacing / 2.0;
|
||||
response.rect = response.rect.expand2(hover_margin).intersect(clip).shrink2(hover_margin);
|
||||
response.interact_rect = response.interact_rect.intersect(clip);
|
||||
button.sub_menu.show(ui, &response, contents).map(|response| response.inner)
|
||||
let mut clipped = response.clone();
|
||||
clipped.rect = response.rect.expand2(hover_margin).intersect(clip).shrink2(hover_margin);
|
||||
clipped.interact_rect = response.interact_rect.intersect(clip);
|
||||
let inner = button.sub_menu.show(ui, &clipped, contents);
|
||||
(response, inner)
|
||||
}
|
||||
|
||||
/// The menu bar contents (inside the app bar; macOS uses the native menu instead).
|
||||
pub fn menu_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let lang = app.ui.language.clone();
|
||||
let mut menus = menu_tree();
|
||||
if crate::i18n::is_rtl(&lang) {
|
||||
menus.reverse();
|
||||
}
|
||||
for (menu, entries) in menus {
|
||||
menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&lang, menu)), |ui| {
|
||||
if crate::i18n::is_rtl(&lang) {
|
||||
ui.set_max_width(320.0);
|
||||
}
|
||||
ui.with_layout(egui::Layout::top_down_justified(if crate::i18n::is_rtl(&lang) { egui::Align::Max } else { egui::Align::Min }), |ui| {
|
||||
ui.set_min_width(240.0);
|
||||
let hidden = menu_items(app, ui, &entries, menu);
|
||||
if hidden > 0 && !app.ui.show_full_menus {
|
||||
ui.separator();
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&lang, "Show All Menu Items"))).clicked() {
|
||||
app.ui.show_full_menus = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The key a menu item is hidden by (Edit › Menus).
|
||||
@@ -1967,8 +2006,9 @@ fn menu_items(app: &mut DesignApp, ui: &mut egui::Ui, items: &[Item], path: &str
|
||||
Item::Sub(name, children) => {
|
||||
let sub = format!("{path}/{name}");
|
||||
let shown = crate::i18n::tr(&app.ui.language, name).to_owned();
|
||||
hidden +=
|
||||
clipped_submenu(ui, crate::rtl::widget(ui, shown), |ui| scroll_menu(ui, |ui| menu_items(app, ui, children, &sub))).unwrap_or(0);
|
||||
menu_button(ui, crate::rtl::widget(ui, shown), |ui| {
|
||||
hidden += menu_items(app, ui, children, &sub);
|
||||
});
|
||||
}
|
||||
Item::Cmd { label, .. } if !app.ui.show_full_menus && app.ui.hidden_menu_items.contains(&menu_key(path, label)) => hidden += 1,
|
||||
Item::Cmd { label, id, params, shortcut: _ } => {
|
||||
@@ -3127,6 +3167,142 @@ mod tests {
|
||||
assert!(items.len() > 100, "every menu's items are listed: {}", items.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_button_fits_its_anchor_and_resets_scroll_on_reopen() {
|
||||
// Workspace/panel menus may be in the middle; the status-bar zoom menu opens upwards.
|
||||
for anchor_y in [0.0, 130.0, 260.0] {
|
||||
let ctx = egui::Context::default();
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 300.0));
|
||||
let mut tick = 0;
|
||||
let mut frame = |events: Vec<egui::Event>| {
|
||||
tick += 1;
|
||||
let mut button = None;
|
||||
let mut first = None;
|
||||
let mut last = None;
|
||||
let mut output =
|
||||
ctx.run_ui(egui::RawInput { screen_rect: Some(screen), time: Some(tick as f64 / 60.0), events, ..Default::default() }, |ui| {
|
||||
ui.add_space(anchor_y);
|
||||
let response = menu_button(ui, "Long menu", |ui| {
|
||||
for i in 0..60 {
|
||||
let response = ui.button(format!("Item {i}"));
|
||||
if i == 0 {
|
||||
first = Some((response.rect, ui.clip_rect()));
|
||||
}
|
||||
if i == 59 {
|
||||
last = Some((response.rect, ui.clip_rect()));
|
||||
}
|
||||
}
|
||||
});
|
||||
button = Some(response.response);
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
(button.expect("menu button"), first, last)
|
||||
};
|
||||
let click =
|
||||
|pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
|
||||
frame(vec![]);
|
||||
let (button, _, _) = frame(vec![]);
|
||||
let anchor = button.rect;
|
||||
for pressed in [true, false] {
|
||||
frame(vec![egui::Event::PointerMoved(anchor.center()), click(anchor.center(), pressed)]);
|
||||
}
|
||||
for _ in 0..3 {
|
||||
frame(vec![]);
|
||||
}
|
||||
let popup = ctx.memory(|memory| memory.area_rect(egui::Popup::default_response_id(&button))).expect("open menu");
|
||||
assert!(screen.contains_rect(popup), "menu at {anchor_y} fits in the viewport: {popup:?}");
|
||||
assert!(
|
||||
popup.top() >= anchor.bottom() - 1.0 || popup.bottom() <= anchor.top() + 1.0,
|
||||
"menu at {anchor_y} stays on one side of its button: {popup:?}, {anchor:?}"
|
||||
);
|
||||
let (_, first, last) = frame(vec![]);
|
||||
let (first, clip) = first.expect("first row");
|
||||
assert!(clip.contains_rect(first), "newly opened menu starts at the first row");
|
||||
assert!(!clip.intersects(last.expect("last row").0), "last row starts below the scroll area");
|
||||
|
||||
frame(vec![
|
||||
egui::Event::PointerMoved(popup.center()),
|
||||
egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: egui::vec2(0.0, -5000.0),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
},
|
||||
]);
|
||||
for _ in 0..60 {
|
||||
frame(vec![]);
|
||||
}
|
||||
let (_, first, last) = frame(vec![]);
|
||||
let (last, clip) = last.expect("scrolled last row");
|
||||
assert!(popup.contains_rect(last) && clip.contains_rect(last), "last row is reachable: {last:?}, {popup:?}, {clip:?}");
|
||||
assert!(!clip.intersects(first.expect("scrolled first row").0), "menu really scrolled away from its start");
|
||||
|
||||
let outside = egui::pos2(screen.right() - 10.0, screen.center().y);
|
||||
for pressed in [true, false] {
|
||||
frame(vec![egui::Event::PointerMoved(outside), click(outside, pressed)]);
|
||||
}
|
||||
assert!(!egui::Popup::is_any_open(&ctx), "outside click closes the menu");
|
||||
assert!(frame(vec![]).1.is_none(), "closed menu is no longer drawn");
|
||||
for pressed in [true, false] {
|
||||
frame(vec![egui::Event::PointerMoved(anchor.center()), click(anchor.center(), pressed)]);
|
||||
}
|
||||
for _ in 0..3 {
|
||||
frame(vec![]);
|
||||
}
|
||||
let (_, first, last) = frame(vec![]);
|
||||
let (first, clip) = first.expect("reopened first row");
|
||||
assert!(popup.contains_rect(first) && clip.contains_rect(first), "reopening resets to the first row: {first:?}, {clip:?}");
|
||||
assert!(!clip.intersects(last.expect("reopened last row").0), "reopening discards the old scroll offset");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_menu_viewport_does_not_force_the_default_scroll_height() {
|
||||
let ctx = egui::Context::default();
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 100.0));
|
||||
let mut tick = 0;
|
||||
let mut frame = |events: Vec<egui::Event>| {
|
||||
tick += 1;
|
||||
let mut button = None;
|
||||
let mut output =
|
||||
ctx.run_ui(egui::RawInput { screen_rect: Some(screen), time: Some(tick as f64 / 60.0), events, ..Default::default() }, |ui| {
|
||||
ui.add_space(35.0);
|
||||
let response = menu_button(ui, "Long menu", |ui| {
|
||||
for i in 0..20 {
|
||||
let _ = ui.button(format!("Item {i}"));
|
||||
}
|
||||
});
|
||||
button = Some(response.response);
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
button.expect("menu button")
|
||||
};
|
||||
frame(vec![]);
|
||||
let button = frame(vec![]);
|
||||
let pos = button.rect.center();
|
||||
for pressed in [true, false] {
|
||||
frame(vec![
|
||||
egui::Event::PointerMoved(pos),
|
||||
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE },
|
||||
]);
|
||||
}
|
||||
for _ in 0..3 {
|
||||
frame(vec![]);
|
||||
}
|
||||
let popup = ctx.memory(|memory| memory.area_rect(egui::Popup::default_response_id(&button))).expect("open menu");
|
||||
let space = (screen.bottom() - button.rect.bottom()).max(button.rect.top() - screen.top());
|
||||
assert!(space < 64.0, "fixture has less space than egui's default minimum scroll height");
|
||||
// A complete command row may not fit, but the scroll area's minimum must not make
|
||||
// the popup exceed its height budget or cover the button that dismisses it.
|
||||
assert!(popup.height() <= space, "tiny menu respects its available height: {popup:?}, {space}");
|
||||
assert!(screen.contains_rect(popup), "tiny menu remains inside the viewport: {popup:?}");
|
||||
assert!(
|
||||
popup.top() >= button.rect.bottom() - 1.0 || popup.bottom() <= button.rect.top() + 1.0,
|
||||
"tiny menu stays beside its anchor: {popup:?}, {:?}",
|
||||
button.rect
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipped_submenu_does_not_open_when_hovering_outside_menu() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
@@ -256,7 +256,7 @@ fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Docu
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply Parent")), |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply Parent")), |ui| {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "[None]"))).clicked() {
|
||||
let _ = app.run("layout.pages.applyParent", json!({"pages": [abs], "parent": null}));
|
||||
ui.close();
|
||||
|
||||
@@ -1365,7 +1365,7 @@ pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
let features: Vec<String> =
|
||||
c["otfFeatures"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
|
||||
ui.menu_button("OpenType", |ui| open_type_menu(app, ui, &features));
|
||||
crate::menus::menu_button(ui, "OpenType", |ui| open_type_menu(app, ui, &features));
|
||||
});
|
||||
// Underline / Strikethrough Options.
|
||||
for (key, title) in [("underline", "Underline Options"), ("strikethrough", "Strikethrough Options")] {
|
||||
@@ -1420,13 +1420,14 @@ pub fn open_type_menu(app: &mut DesignApp, ui: &mut egui::Ui, features: &[String
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Stylistic Sets")), |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Stylistic Sets")), |ui| {
|
||||
let mask = otf::stylistic_sets(features);
|
||||
for n in 1..=20u32 {
|
||||
let mut on = mask & (1 << (n - 1)) != 0;
|
||||
if ui.checkbox(&mut on, crate::rtl::widget(ui, format!("{} {n}", crate::i18n::tr(&app.ui.language, "Set")))).clicked() {
|
||||
let sets: Vec<u32> = (1..=20).filter(|k| if *k == n { on } else { mask & (1 << (k - 1)) != 0 }).collect();
|
||||
let _ = app.run("type.openType", json!({"stylisticSets": sets}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,7 +121,7 @@ fn style_row(app: &mut DesignApp, ui: &mut egui::Ui, para: bool, n: &str, curren
|
||||
ui.close();
|
||||
}
|
||||
if !n.starts_with('[') {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Group")), |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Group")), |ui| {
|
||||
if n.contains('/') && ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "[No Group]"))).clicked() {
|
||||
let _ = app.run("style.group", json!({"kind": kind, "names": [n], "group": ""}));
|
||||
ui.close();
|
||||
|
||||
@@ -73,12 +73,13 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if icons::button(ui, "plus", 20.0, false, crate::i18n::tr(&app.ui.language, "New Swatch")).clicked() {
|
||||
let _ = app.run("swatch.create", json!({"color": {"c": 0, "m": 50, "y": 100, "k": 0}}));
|
||||
}
|
||||
ui.menu_button("☰", |ui| {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Ink Manager")), |ui| {
|
||||
crate::menus::menu_button(ui, "☰", |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Ink Manager")), |ui| {
|
||||
let Ok(l) = app.session.execute("ink.list", &json!({})) else { return };
|
||||
let mut all = l["allToProcess"].as_bool().unwrap_or(false);
|
||||
if ui.checkbox(&mut all, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "All Spots to Process"))).changed() {
|
||||
let _ = app.run("ink.options", json!({"allToProcess": all}));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
let inks = l["inks"].as_array().cloned().unwrap_or_default();
|
||||
@@ -91,6 +92,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.add_enabled_ui(!all, |ui| {
|
||||
if ui.checkbox(&mut process, format!("{name} → process")).changed() {
|
||||
let _ = app.run("ink.options", json!({"ink": name, "toProcess": process}));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -160,7 +162,7 @@ fn swatch_row(
|
||||
ui.close();
|
||||
}
|
||||
if !sw.locked {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Color Group")), |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Color Group")), |ui| {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "(Top Level)"))).clicked() {
|
||||
let _ = app.run("swatch.moveToGroup", json!({"swatches": [sw.name], "group": null}));
|
||||
ui.close();
|
||||
|
||||
Reference in New Issue
Block a user