diff --git a/ROADMAP.md b/ROADMAP.md index 2c685c6e..818c0497 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -140,6 +140,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md Newest first: the date, what moved, and the new overall estimate. +- **2026-10-11 (UI, checkboxes and radio buttons):** Checkboxes no longer look like radio buttons, and on the light theme neither is invisible. With the theme's 6 px widget radius egui drew a checkbox's 14 px box as a circle, so View ▸ Page display's Continuous scrolling and Show cover page read as two more layout choices; on the light theme an unticked box or an unselected radio button was white on white with no edge. Every checkbox (81) and radio button (79) now goes through `widgets::checkbox` / `widgets::radio` / `widgets::radio_value`: a 1 px outline (darker on hover), the plain fill on the light theme as dialogs already had, and for checkboxes a rounded-square box (3 px), keeping what the caller set (dialogs, the search panel). `clippy.toml` disallows `ui.checkbox`, `ui.radio`, `ui.radio_value`, `egui::Checkbox` and `egui::RadioButton` so new ones use them too. Estimates unchanged. - **2026-10-11 (M13, #971):** `text_find` now bounds each page's temporary match-range list to the remaining result limit plus one, which is enough to detect and return a continuation without allocating every match on a dense page. Added limited-search coverage; the existing end-to-end continuation test passes. The text cache byte budget and per-page extraction warnings remain open. - **2026-10-11 (M7, #594):** Ordinary paragraph body drags now use the existing source-preserving object translation path; edge drags still rewrap. An in-progress drag is discarded if the document generation changes, and added-content gestures keep their own edit handler. Added real-shell source regressions for overlapping and identical labels, history, save/reopen, partial overlap, kerning, rotation/crop/UserUnit and atomic refusal. Linux validation passed: focused UI/engine regressions, exact-base source-loss failures, native drag/undo/redo/save/reopen with independent Poppler renders, and all eight CI gates. Text replacement and resizing over unrelated overlapping content remain outside this repair. Estimates unchanged. diff --git a/clippy.toml b/clippy.toml index 4b6c79f5..7f2fd530 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,3 +2,14 @@ allow-unwrap-in-tests = true allow-expect-in-tests = true allow-panic-in-tests = true allow-indexing-slicing-in-tests = true + +# With the theme's widget visuals egui draws a checkbox's box as a circle, like a radio button, +# and on the light theme neither has an outline or a fill that shows on a white panel. +disallowed-methods = [ + { path = "egui::Ui::checkbox", reason = "use checkbox from crates/ui-egui/src/widgets.rs, whose box is a square" }, + { path = "egui::Checkbox::new", reason = "use checkbox from crates/ui-egui/src/widgets.rs, whose box is a square" }, + { path = "egui::Checkbox::without_text", reason = "use checkbox_without_text from crates/ui-egui/src/widgets.rs, whose box is a square" }, + { path = "egui::Ui::radio", reason = "use radio from crates/ui-egui/src/widgets.rs, which is outlined and filled" }, + { path = "egui::Ui::radio_value", reason = "use radio_value from crates/ui-egui/src/widgets.rs, which is outlined and filled" }, + { path = "egui::RadioButton::new", reason = "use radio from crates/ui-egui/src/widgets.rs, which is outlined and filled" }, +] diff --git a/crates/ui-egui/src/a11y_ui.rs b/crates/ui-egui/src/a11y_ui.rs index 57a7ce86..40505cc4 100644 --- a/crates/ui-egui/src/a11y_ui.rs +++ b/crates/ui-egui/src/a11y_ui.rs @@ -83,12 +83,12 @@ pub(crate) fn options_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) ui.add_space(8.0); }; group(ui, "Report Options", &mut |ui| { - ui.checkbox(&mut o.create_report, tl!("Create accessibility report")); + ui.add(widgets::checkbox(&mut o.create_report, tl!("Create accessibility report"))); }); group(ui, "Page Range", &mut |ui| { ui.horizontal(|ui| { - ui.radio_value(&mut o.all_pages, true, tl!("All pages in document")); - ui.radio_value(&mut o.all_pages, false, tl!("Pages from")); + widgets::radio_value(ui, &mut o.all_pages, true, tl!("All pages in document")); + widgets::radio_value(ui, &mut o.all_pages, false, tl!("Pages from")); ui.add_enabled(!o.all_pages, egui::DragValue::new(&mut o.from).range(1..=pages)); ui.label(tl!("to")); ui.add_enabled(!o.all_pages, egui::DragValue::new(&mut o.to).range(1..=pages)); @@ -107,7 +107,7 @@ pub(crate) fn options_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) ui.add_space(4.0); for r in Rule::ALL.into_iter().filter(|r| r.category() == o.category) { let mut checked = o.rules.contains(&r); - if ui.checkbox(&mut checked, tl!(r.description())).changed() { + if ui.add(widgets::checkbox(&mut checked, tl!(r.description()))).changed() { if checked { o.rules.insert(r); } else { @@ -126,7 +126,7 @@ pub(crate) fn options_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) } }); }); - ui.checkbox(&mut o.show_dialog, tl!("Show this dialog when the Checker starts")); + ui.add(widgets::checkbox(&mut o.show_dialog, tl!("Show this dialog when the Checker starts"))); ui.add_space(10.0); let (mut start, mut cancel) = (false, false); ui.horizontal(|ui| { @@ -507,7 +507,7 @@ pub(crate) fn alt_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> let l = ui.label(tl!("Alternate text")); ui.add(crate::widgets::block(&mut d.texts[i]).desired_rows(3).desired_width(380.0).hint_text(tl!("Describe the figure"))).labelled_by(l.id); }); - ui.checkbox(&mut d.decorative[i], tl!("Decorative figure")); + ui.add(widgets::checkbox(&mut d.decorative[i], tl!("Decorative figure"))); ui.add_space(10.0); let (mut save, mut cancel) = (false, false); ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/appearance.rs b/crates/ui-egui/src/appearance.rs index bb804314..5782b3d5 100644 --- a/crates/ui-egui/src/appearance.rs +++ b/crates/ui-egui/src/appearance.rs @@ -5,6 +5,7 @@ use egui::{Rect, RichText, pos2, vec2}; use crate::PdfCraftApp; use crate::theme::{self, ThemeKind, ThemePreference, Tokens}; +use crate::widgets; /// The themes each card offers. One of each today; a new theme is one more row here. pub const LIGHT_THEMES: &[(ThemeKind, &str)] = &[(ThemeKind::Light, "Light gray")]; @@ -103,7 +104,7 @@ fn card(ui: &mut egui::Ui, t: &Tokens, title: &str, active: bool, themes: &[(The ui.add_space(4.0); for (kind, label) in themes { // One theme per card today, so it is always the chosen one. - let _ = ui.radio(*kind == shown, tl!(label)); + let _ = ui.add(widgets::radio(*kind == shown, tl!(label))); } }); } diff --git a/crates/ui-egui/src/bulk_fields.rs b/crates/ui-egui/src/bulk_fields.rs index 480ace70..3101724c 100644 --- a/crates/ui-egui/src/bulk_fields.rs +++ b/crates/ui-egui/src/bulk_fields.rs @@ -203,7 +203,7 @@ impl Draft { } fn row(ui: &mut egui::Ui, label: &str, c: &mut Change, edit: impl FnOnce(&mut egui::Ui, &mut T)) { - ui.checkbox(&mut c.apply, tl!(label)); + ui.add(widgets::checkbox(&mut c.apply, tl!(label))); // Colour swatches contain their own grid; give every property's editor a separate // namespace so border and fill controls do not share widget identities. ui.push_id(label, |ui| ui.add_enabled_ui(c.apply, |ui| edit(ui, &mut c.value))); @@ -248,7 +248,7 @@ fn bool_row(ui: &mut egui::Ui, label: &str, c: &mut Change) { fn color(ui: &mut egui::Ui, value: &mut Option<[f64; 3]>) { ui.horizontal(|ui| { let mut none = value.is_none(); - if ui.checkbox(&mut none, tl!("No color")).changed() { + if ui.add(widgets::checkbox(&mut none, tl!("No color"))).changed() { *value = if none { None } else { Some([0.0; 3]) }; } if let Some(c) = crate::comments::swatch_grid(ui, *value) { diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index adb67fb5..c8bf4a87 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -3423,8 +3423,8 @@ fn find_bar(view: &mut DocView, pages: usize, area: Rect, ui: &mut egui::Ui, t: } let opts = icons::button(ui, "settings-2", 26.0, find.case_sensitive || find.whole_words, tl!("Find options")); egui::Popup::menu(&opts).show(|ui| { - let a = ui.checkbox(&mut find.whole_words, tl!("Whole words only")).changed(); - let b = ui.checkbox(&mut find.case_sensitive, tl!("Case-sensitive")).changed(); + let a = ui.add(widgets::checkbox(&mut find.whole_words, tl!("Whole words only"))).changed(); + let b = ui.add(widgets::checkbox(&mut find.case_sensitive, tl!("Case-sensitive"))).changed(); if a || b { // Search again with the new options. find.case_query.clear(); @@ -4050,7 +4050,7 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool, dirty: bool // outside the page image, including on very tall or wide pages. let mut checked = view.selected.contains(&i); let check_rect = Rect::from_min_size(c.min + vec2(8.0, 8.0), Vec2::splat(20.0)); - let check = ui.push_id(("org-check", i), |ui| ui.put(check_rect, egui::Checkbox::without_text(&mut checked))).inner; + let check = ui.push_id(("org-check", i), |ui| ui.put(check_rect, widgets::checkbox_without_text(&mut checked))).inner; let label = crate::i18n::fmt(tl!("Select page {label}"), &[("label", &p.label)]); check.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, checked, label.clone())); if check.on_hover_text(label).changed() { diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 58cf8ccd..8036e0cf 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -206,7 +206,7 @@ fn theme_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) { (ThemePreference::Light, "view.theme.light", "Light gray"), (ThemePreference::Dark, "view.theme.dark", "Dark gray"), ] { - if ui.radio(app.theme_preference == preference, tl!(label)).clicked() { + if ui.add(widgets::radio(app.theme_preference == preference, tl!(label))).clicked() { app.execute(command); ui.close(); } @@ -467,7 +467,7 @@ fn main_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) { (RightPanel::Search, tl!("Search")), (RightPanel::Compare, tl!("Compare")), ] { - if ui.radio(app.right == Some(p), tl!(label)).clicked() { + if ui.add(widgets::radio(app.right == Some(p), tl!(label))).clicked() { app.choose_right_panel(Some(p)); } } @@ -595,18 +595,18 @@ fn page_display_menu(ui: &mut egui::Ui, view: &DocView) -> Option<&'static str> ui.label(egui::RichText::new(tl!("Page display")).color(t.text_faint).small()); let mut picked = None; for l in PageLayout::ORDER { - if ui.radio(view.layout == l, tl!(l.label())).clicked() { + if ui.add(widgets::radio(view.layout == l, tl!(l.label()))).clicked() { picked = Some(l.command()); } } ui.separator(); // Continuous scrolling is an independent checkbox over the base layout, as in Acrobat (#752). let mut continuous = view.continuous(); - if ui.add(egui::Checkbox::new(&mut continuous, tl!("Continuous scrolling"))).changed() { + if ui.add(widgets::checkbox(&mut continuous, tl!("Continuous scrolling"))).changed() { picked = Some("view.continuous"); } let mut cover = view.cover; - if ui.add_enabled(view.cover_applies(), egui::Checkbox::new(&mut cover, tl!("Show cover page in two-page view"))).changed() { + if ui.add_enabled(view.cover_applies(), widgets::checkbox(&mut cover, tl!("Show cover page in two-page view"))).changed() { picked = Some("view.layout.cover"); } if picked.is_some() { diff --git a/crates/ui-egui/src/comment_props.rs b/crates/ui-egui/src/comment_props.rs index a01bb94a..e6ac0ba1 100644 --- a/crates/ui-egui/src/comment_props.rs +++ b/crates/ui-egui/src/comment_props.rs @@ -260,7 +260,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo } } ui.add_space(12.0); - ui.checkbox(&mut d.edited.locked, tl!("Locked")); + ui.add(widgets::checkbox(&mut d.edited.locked, tl!("Locked"))); let (mut apply, mut cancel) = (false, false); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { if widgets::pill_button(ui, tl!("OK"), true).clicked() { diff --git a/crates/ui-egui/src/comments_panel.rs b/crates/ui-egui/src/comments_panel.rs index 5182fadc..e89c7de6 100644 --- a/crates/ui-egui/src/comments_panel.rs +++ b/crates/ui-egui/src/comments_panel.rs @@ -13,6 +13,7 @@ use crate::comments::{CommentPrefs, CommentTool, SortBy, color32, status_badge}; use crate::icons; use crate::panels::Nav; use crate::theme::{self, Tokens}; +use crate::widgets; /// Accent bar of the selected card (11-visual-spec §3: `#0165DD`). const CARD_ACCENT: Color32 = Color32::from_rgb(0x01, 0x65, 0xDD); @@ -510,7 +511,7 @@ pub(crate) fn header_controls(ui: &mut egui::Ui, info: &DocInfo, view: &mut DocV (SortBy::Type, tl!("Type")), (SortBy::Color, tl!("Colour")), ] { - if ui.radio(cv.sort == s, label).clicked() { + if ui.add(widgets::radio(cv.sort == s, label)).clicked() { cv.sort = s; ui.close(); } @@ -533,7 +534,7 @@ pub(crate) fn header_controls(ui: &mut egui::Ui, info: &DocInfo, view: &mut DocV ui.set_min_width(200.0); let toggle = |ui: &mut egui::Ui, list: &mut Vec, value: &str, label: &str| { let mut shown = !list.iter().any(|x| x == value); - if ui.checkbox(&mut shown, label).changed() { + if ui.add(widgets::checkbox(&mut shown, label)).changed() { if shown { list.retain(|x| x != value); } else { diff --git a/crates/ui-egui/src/create_ui.rs b/crates/ui-egui/src/create_ui.rs index fedd2d64..a12cec8b 100644 --- a/crates/ui-egui/src/create_ui.rs +++ b/crates/ui-egui/src/create_ui.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use crate::PdfCraftApp; +use crate::widgets; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum ResolutionChoice { @@ -24,10 +25,10 @@ pub(crate) fn image_import_body(ui: &mut egui::Ui, app: &mut PdfCraftApp) -> (bo let Some(draft) = app.image_import.as_mut() else { return (false, true) }; ui.label(crate::i18n::fmt(tl!("Selected image files: {count}"), &[("count", &draft.images.len().to_string())])); ui.add_space(8.0); - ui.radio_value(&mut draft.choice, ResolutionChoice::Image, tl!("Use image resolution")); + widgets::radio_value(ui, &mut draft.choice, ResolutionChoice::Image, tl!("Use image resolution")); ui.label(tl!("Use each image's embedded DPI; use 72 DPI when it is absent.")); - ui.radio_value(&mut draft.choice, ResolutionChoice::Default72, tl!("Use 72 DPI (one point per pixel)")); - ui.radio_value(&mut draft.choice, ResolutionChoice::Custom, tl!("Use custom DPI")); + widgets::radio_value(ui, &mut draft.choice, ResolutionChoice::Default72, tl!("Use 72 DPI (one point per pixel)")); + widgets::radio_value(ui, &mut draft.choice, ResolutionChoice::Custom, tl!("Use custom DPI")); ui.add_enabled(draft.choice == ResolutionChoice::Custom, egui::DragValue::new(&mut draft.dpi).range(1.0..=1200.0).suffix(" DPI")); ui.label(tl!("DPI sets the printed page size without resampling the image.")); ui.add_space(12.0); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index bab565fd..89040f82 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -223,13 +223,13 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { ui.label(""); ui.add_enabled_ui(editable, |ui| { ui.vertical(|ui| { - ui.checkbox(&mut v.fit_window, tl!("Resize window to initial page")); - ui.checkbox(&mut v.center_window, tl!("Center window on screen")); - ui.checkbox(&mut v.full_screen, tl!("Open in Full Screen mode")); + ui.add(widgets::checkbox(&mut v.fit_window, tl!("Resize window to initial page"))); + ui.add(widgets::checkbox(&mut v.center_window, tl!("Center window on screen"))); + ui.add(widgets::checkbox(&mut v.full_screen, tl!("Open in Full Screen mode"))); ui.horizontal(|ui| { ui.label(tl!("Show:")); - ui.radio_value(&mut v.display_title, false, tl!("File Name")); - ui.radio_value(&mut v.display_title, true, tl!("Document Title")); + widgets::radio_value(ui, &mut v.display_title, false, tl!("File Name")); + widgets::radio_value(ui, &mut v.display_title, true, tl!("Document Title")); }); }); }); @@ -239,9 +239,9 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { ui.label(""); ui.add_enabled_ui(editable, |ui| { ui.vertical(|ui| { - ui.checkbox(&mut v.hide_menubar, tl!("Hide menu bar")); - ui.checkbox(&mut v.hide_toolbar, tl!("Hide toolbars")); - ui.checkbox(&mut v.hide_window_ui, tl!("Hide window controls")); + ui.add(widgets::checkbox(&mut v.hide_menubar, tl!("Hide menu bar"))); + ui.add(widgets::checkbox(&mut v.hide_toolbar, tl!("Hide toolbars"))); + ui.add(widgets::checkbox(&mut v.hide_window_ui, tl!("Hide window controls"))); }); }); ui.end_row(); @@ -359,8 +359,8 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { ui.label(egui::RichText::new(tl!("Binding")).color(t.text_muted)); ui.add_enabled_ui(editable, |ui| { ui.horizontal(|ui| { - ui.radio_value(&mut v.right_to_left, false, tl!("Left Edge")); - ui.radio_value(&mut v.right_to_left, true, tl!("Right Edge")); + widgets::radio_value(ui, &mut v.right_to_left, false, tl!("Left Edge")); + widgets::radio_value(ui, &mut v.right_to_left, true, tl!("Right Edge")); }); }); ui.end_row(); @@ -403,7 +403,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { if selected.is_empty() && draft.mode == M::Selection { draft.mode = M::Pages; } - ui.radio_value(&mut draft.mode, M::Pages, tl!("Number of pages")); + widgets::radio_value(ui, &mut draft.mode, M::Pages, tl!("Number of pages")); ui.add_enabled_ui(draft.mode == M::Pages, |ui| { ui.horizontal(|ui| { ui.add_space(24.0); @@ -411,7 +411,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { ui.add(egui::DragValue::new(&mut draft.every).range(1..=n.max(1))); }); }); - ui.radio_value(&mut draft.mode, M::Size, tl!("File size")); + widgets::radio_value(ui, &mut draft.mode, M::Size, tl!("File size")); ui.add_enabled_ui(draft.mode == M::Size, |ui| { ui.horizontal(|ui| { ui.add_space(24.0); @@ -420,14 +420,15 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { }); }); ui.add_enabled_ui(!marks.is_empty(), |ui| { - ui.radio_value( + widgets::radio_value( + ui, &mut draft.mode, M::Bookmarks, crate::i18n::fmt(tl!("Top-level bookmarks ({n})"), &[("n", &marks.len().to_string())]), ) }); ui.add_enabled_ui(!selected.is_empty(), |ui| { - ui.radio_value(&mut draft.mode, M::Selection, tl!("Before each selected page (select pages in Organize)")) + widgets::radio_value(ui, &mut draft.mode, M::Selection, tl!("Before each selected page (select pages in Organize)")) }); let plan = match draft.mode { M::Pages => crate::SplitPlan::By(pdfcraft_engine::SplitBy::PageCount(draft.every)), @@ -570,8 +571,8 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { let l = ui.label(tl!("File name")); ui.add(egui::TextEdit::singleline(&mut app.extract_draft.name).desired_width(240.0)).labelled_by(l.id); }); - ui.checkbox(&mut app.extract_draft.delete, tl!("Delete pages after extracting")); - ui.checkbox(&mut app.extract_draft.separate, tl!("Extract pages as separate files")); + ui.add(widgets::checkbox(&mut app.extract_draft.delete, tl!("Delete pages after extracting"))); + ui.add(widgets::checkbox(&mut app.extract_draft.separate, tl!("Extract pages as separate files"))); ui.add_space(12.0); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { if widgets::pill_button(ui, tl!("Extract"), true).clicked() { @@ -606,10 +607,10 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { ui.end_row(); ui.label(tl!("Pages:")); ui.vertical(|ui| { - ui.radio_value(&mut d.which, 0, tl!("All")); - ui.radio_value(&mut d.which, 1, tl!("Selection")); + widgets::radio_value(ui, &mut d.which, 0, tl!("All")); + widgets::radio_value(ui, &mut d.which, 1, tl!("Selection")); ui.horizontal(|ui| { - ui.radio_value(&mut d.which, 2, tl!("From")); + widgets::radio_value(ui, &mut d.which, 2, tl!("From")); ui.add_enabled(d.which == 2, egui::DragValue::new(&mut d.from).range(1..=n)); ui.label(tl!("to")); ui.add_enabled(d.which == 2, egui::DragValue::new(&mut d.to).range(1..=n)); @@ -666,9 +667,9 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { ui.label(egui::RichText::new(tl!("Duplicate Field")).font(theme::semibold(18.0))); ui.add_space(8.0); ui.label(crate::i18n::fmt(tl!("Duplicate \"{name}\" onto:"), &[("name", &d.name)])); - ui.radio_value(&mut d.all, true, tl!("All pages")); + widgets::radio_value(ui, &mut d.all, true, tl!("All pages")); ui.horizontal(|ui| { - ui.radio_value(&mut d.all, false, tl!("From")); + widgets::radio_value(ui, &mut d.all, false, tl!("From")); ui.add_enabled(!d.all, egui::DragValue::new(&mut d.from).range(1..=n)); ui.label(tl!("to")); ui.add_enabled(!d.all, egui::DragValue::new(&mut d.to).range(1..=n)); @@ -734,7 +735,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { ui.label(egui::RichText::new(tl!("Summarize Options")).font(theme::semibold(18.0))); ui.add_space(8.0); ui.label(tl!("Choose a layout:")); - let _ = ui.radio(true, tl!("Comments only")); + let _ = ui.add(widgets::radio(true, tl!("Comments only"))); ui.add_space(6.0); ui.horizontal(|ui| { ui.label(tl!("Sort comments by:")); diff --git a/crates/ui-egui/src/forms_ui.rs b/crates/ui-egui/src/forms_ui.rs index b34a9e6c..12068f14 100644 --- a/crates/ui-egui/src/forms_ui.rs +++ b/crates/ui-egui/src/forms_ui.rs @@ -12,6 +12,7 @@ use pdfcraft_render::DocInfo; use crate::canvas::{DocView, PageXform}; use crate::theme::Tokens; +use crate::widgets; const FOCUS_BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6); /// Background of the in-place text field editor, the same in every theme. @@ -425,7 +426,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, f let on = if multi { fx.picked.contains(export) } else { f.value.first() == Some(export) }; if multi { let mut c = on; - if ui.checkbox(&mut c, display).changed() { + if ui.add(widgets::checkbox(&mut c, display)).changed() { if c { fx.picked.push(export.clone()); } else { diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index fb7d64a4..2412651a 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -291,7 +291,7 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok (crate::Mode::Convert, "Convert"), (crate::Mode::Sign, "E-Sign"), ] { - ui.radio_value(&mut app.default_mode, mode, tl!(label)); + widgets::radio_value(ui, &mut app.default_mode, mode, tl!(label)); } }); ui.label( @@ -303,16 +303,16 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok ui.horizontal(|ui| { ui.label(tl!("Default page display")); for l in crate::canvas::PageLayout::ORDER { - ui.radio_value(&mut defaults.layout, l, tl!(l.label())); + widgets::radio_value(ui, &mut defaults.layout, l, tl!(l.label())); } - ui.checkbox(&mut defaults.continuous, tl!("Continuous scrolling")); + ui.add(widgets::checkbox(&mut defaults.continuous, tl!("Continuous scrolling"))); }); ui.horizontal(|ui| { use crate::canvas::Fit; ui.label(tl!("Default zoom")); - ui.radio_value(&mut defaults.fit, Fit::Width, tl!("Fit to width")); - ui.radio_value(&mut defaults.fit, Fit::Page, tl!("Zoom to page level")); - ui.radio_value(&mut defaults.fit, Fit::None, tl!("Custom")); + widgets::radio_value(ui, &mut defaults.fit, Fit::Width, tl!("Fit to width")); + widgets::radio_value(ui, &mut defaults.fit, Fit::Page, tl!("Zoom to page level")); + widgets::radio_value(ui, &mut defaults.fit, Fit::None, tl!("Custom")); // Editing the percentage selects Custom. let mut percent = defaults.zoom * 100.0; if ui.add(egui::DragValue::new(&mut percent).range(8.0..=6400.0).max_decimals(0).suffix("%")).changed() { @@ -327,20 +327,20 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok ui.horizontal(|ui| { ui.label(tl!("Units")); for u in crate::units::Unit::ALL { - ui.radio_value(&mut app.units, u, u.label()); + widgets::radio_value(ui, &mut app.units, u, u.label()); } }); ui.label(egui::RichText::new(tl!("For page sizes and margins.")).small().color(t.text_muted)); // The web build has no file paths to reopen. #[cfg(not(target_arch = "wasm32"))] - ui.checkbox(&mut app.reopen_last_session, tl!("Reopen the files that were open when PdfCraft last closed")); + ui.add(widgets::checkbox(&mut app.reopen_last_session, tl!("Reopen the files that were open when PdfCraft last closed"))); // The web build has no files on disk to watch. #[cfg(not(target_arch = "wasm32"))] - ui.checkbox(&mut app.reload_changed_files, tl!("Reload documents when their files change on disk")); + ui.add(widgets::checkbox(&mut app.reload_changed_files, tl!("Reload documents when their files change on disk"))); ui.horizontal(|ui| { ui.label(tl!("Mouse wheel scroll speed")); for speed in crate::scroll_speed::ScrollSpeed::ALL { - ui.radio_value(&mut app.scroll_speed, speed, tl_ctx!("scroll speed", speed.label())); + widgets::radio_value(ui, &mut app.scroll_speed, speed, tl_ctx!("scroll speed", speed.label())); } }); ui.add_space(8.0); @@ -352,7 +352,7 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok }); ui.add_space(8.0); ui.label(egui::RichText::new(tl!("Fill & Sign")).font(theme::semibold(13.0))); - ui.checkbox(&mut app.flatten_fill_sign_on_save, tl!("Flatten Fill & Sign when saving")); + ui.add(widgets::checkbox(&mut app.flatten_fill_sign_on_save, tl!("Flatten Fill & Sign when saving"))); ui.label( egui::RichText::new(tl!("Text, marks and signatures become part of the page. Other comments stay editable.")).small().color(t.text_muted), ); @@ -363,7 +363,7 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(10)).show(ui, |ui| { ui.set_width(ui.available_width()); let mut on = app.session.javascript(); - if ui.checkbox(&mut on, tl!("Enable Acrobat JavaScript")).changed() { + if ui.add(widgets::checkbox(&mut on, tl!("Enable Acrobat JavaScript"))).changed() { app.session.set_javascript(on); } ui.label( diff --git a/crates/ui-egui/src/link_ui.rs b/crates/ui-egui/src/link_ui.rs index 1079a468..8dc91cf7 100644 --- a/crates/ui-egui/src/link_ui.rs +++ b/crates/ui-egui/src/link_ui.rs @@ -249,14 +249,14 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut LinkDraft, pages: usize, t: &Token }); ui.add_space(8.0); widgets::section_title(ui, tl!("Link Action")); - ui.radio_value(&mut d.web, true, tl!("Open a web page")); + widgets::radio_value(ui, &mut d.web, true, tl!("Open a web page")); ui.add_enabled_ui(d.web, |ui| { ui.horizontal(|ui| { ui.add_space(24.0); ui.add(crate::widgets::line(&mut d.url).desired_width(360.0).hint_text("https://example.org")); }); }); - ui.radio_value(&mut d.web, false, tl!("Go to a page view")); + widgets::radio_value(ui, &mut d.web, false, tl!("Go to a page view")); ui.add_enabled_ui(!d.web, |ui| { ui.horizontal(|ui| { ui.add_space(24.0); diff --git a/crates/ui-egui/src/marks_ui.rs b/crates/ui-egui/src/marks_ui.rs index 2bf7d6f7..84214fc2 100644 --- a/crates/ui-egui/src/marks_ui.rs +++ b/crates/ui-egui/src/marks_ui.rs @@ -49,8 +49,8 @@ impl PageRange { pub(crate) fn ui(&mut self, ui: &mut egui::Ui, count: usize) { ui.horizontal(|ui| { - ui.radio_value(&mut self.all, true, tl!("All pages")); - ui.radio_value(&mut self.all, false, tl!("Pages from")); + widgets::radio_value(ui, &mut self.all, true, tl!("All pages")); + widgets::radio_value(ui, &mut self.all, false, tl!("Pages from")); ui.add_enabled(!self.all, egui::DragValue::new(&mut self.from).range(1..=count.max(1))); ui.label(tl!("to")); if self.to == 1 && self.from == 1 { @@ -164,7 +164,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M ui.label(tl!("Font: Helvetica")); ui.label(tl!("Size")); ui.add(egui::DragValue::new(&mut d.hf.font_size).range(4.0..=72.0).speed(0.5)); - ui.checkbox(&mut d.hf.underline, tl!("Underline")); + ui.add(widgets::checkbox(&mut d.hf.underline, tl!("Underline"))); color_button(ui, &mut d.hf.color); ui.add_space(16.0); ui.label(format!("{} ({})", tl!("Margins"), units.label())); @@ -228,8 +228,8 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M MarkKind::Watermark => { ui.horizontal(|ui| { ui.label(tl!("Source")); - ui.radio_value(&mut d.use_file, false, tl!("Text")); - ui.radio_value(&mut d.use_file, true, tl!("File")); + widgets::radio_value(ui, &mut d.use_file, false, tl!("Text")); + widgets::radio_value(ui, &mut d.use_file, true, tl!("File")); }); if d.use_file { browse |= file_source(ui, d, MarkKind::Watermark); @@ -243,7 +243,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M ); }); ui.horizontal(|ui| { - ui.add_enabled_ui(!d.use_file, |ui| ui.checkbox(&mut d.fit, tl!("Fit to page"))); + ui.add_enabled_ui(!d.use_file, |ui| ui.add(widgets::checkbox(&mut d.fit, tl!("Fit to page")))); ui.add_enabled_ui(!d.fit, |ui| { ui.label(tl!("Size")); if d.wm.font_size == 0.0 { @@ -256,7 +256,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M ui.horizontal(|ui| { ui.label(tl!("Rotation")); for r in [-45.0, 0.0, 45.0] { - ui.radio_value(&mut d.wm.rotation, r, if r == 0.0 { tl!("None").to_string() } else { format!("{r}°") }); + widgets::radio_value(ui, &mut d.wm.rotation, r, if r == 0.0 { tl!("None").to_string() } else { format!("{r}°") }); } ui.add(egui::DragValue::new(&mut d.wm.rotation).range(-180.0..=180.0).suffix("°")); }); @@ -269,8 +269,8 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M }); ui.horizontal(|ui| { ui.label(tl!("Location")); - ui.radio_value(&mut d.wm.behind, true, tl!("Appear behind page")); - ui.radio_value(&mut d.wm.behind, false, tl!("Appear on top of page")); + widgets::radio_value(ui, &mut d.wm.behind, true, tl!("Appear behind page")); + widgets::radio_value(ui, &mut d.wm.behind, false, tl!("Appear on top of page")); }); ui.horizontal(|ui| { ui.label(tl!("Offset from centre")); @@ -284,10 +284,10 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M } MarkKind::Background => { ui.horizontal(|ui| { - ui.radio_value(&mut d.use_file, false, tl!("From colour")); + widgets::radio_value(ui, &mut d.use_file, false, tl!("From colour")); ui.add_enabled_ui(!d.use_file, |ui| color_button(ui, &mut d.bg.color)); ui.add_space(12.0); - ui.radio_value(&mut d.use_file, true, tl!("File")); + widgets::radio_value(ui, &mut d.use_file, true, tl!("File")); }); if d.use_file { browse |= file_source(ui, d, MarkKind::Background); diff --git a/crates/ui-egui/src/measure_ui.rs b/crates/ui-egui/src/measure_ui.rs index 1a1f83c3..f031a8d0 100644 --- a/crates/ui-egui/src/measure_ui.rs +++ b/crates/ui-egui/src/measure_ui.rs @@ -1,5 +1,5 @@ //! Measure objects: page-space clicks, live readings and persistent drawing scales. -use crate::{LeftPanel, PdfCraftApp, QuickTool, canvas::DocView, theme::Tokens}; +use crate::{LeftPanel, PdfCraftApp, QuickTool, canvas::DocView, theme::Tokens, widgets}; use egui::{Color32, Pos2, Stroke}; use pdfcraft_engine::{ Document, Edit, Style, @@ -338,15 +338,15 @@ fn panel_body(app: &mut PdfCraftApp, ui: &mut egui::Ui, _t: &Tokens) { ui.add(crate::widgets::line(&mut state.label).desired_width(130.0).char_limit(128)); }); ui.separator(); - ui.checkbox(&mut state.snap_enabled, tl!("Snap to drawing")); + ui.add(widgets::checkbox(&mut state.snap_enabled, tl!("Snap to drawing"))); if state.snap_enabled { ui.horizontal(|ui| { - ui.checkbox(&mut state.snaps.endpoints, tl!("Endpoints")); - ui.checkbox(&mut state.snaps.midpoints, tl!("Midpoints")); + ui.add(widgets::checkbox(&mut state.snaps.endpoints, tl!("Endpoints"))); + ui.add(widgets::checkbox(&mut state.snaps.midpoints, tl!("Midpoints"))); }); ui.horizontal(|ui| { - ui.checkbox(&mut state.snaps.intersections, tl!("Intersections")); - ui.checkbox(&mut state.snaps.paths, tl!("Paths")); + ui.add(widgets::checkbox(&mut state.snaps.intersections, tl!("Intersections"))); + ui.add(widgets::checkbox(&mut state.snaps.paths, tl!("Paths"))); }); ui.add(egui::Slider::new(&mut state.sensitivity, 1.0..=20.0).text(tl!("Sensitivity"))); } @@ -365,7 +365,7 @@ fn panel_body(app: &mut PdfCraftApp, ui: &mut egui::Ui, _t: &Tokens) { if ui.button(tl!("Calibrate from two points")).clicked() { tool = Some(Tool::Calibrate); } - ui.checkbox(&mut state.whole_page, tl!("Entire page")); + ui.add(widgets::checkbox(&mut state.whole_page, tl!("Entire page"))); if !state.whole_page { ui.label(tl!("Viewport rectangle in page points (x, y, width, height)")); for (i, name) in ["X", "Y", "Width", "Height"].iter().enumerate() { diff --git a/crates/ui-egui/src/ocr_ui.rs b/crates/ui-egui/src/ocr_ui.rs index f6d38e1e..5bde1c95 100644 --- a/crates/ui-egui/src/ocr_ui.rs +++ b/crates/ui-egui/src/ocr_ui.rs @@ -75,10 +75,10 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo ui.add_space(8.0); }; group(ui, "Pages", &mut |ui| { - ui.radio_value(&mut d.pages, OcrPages::All, tl!("All pages")); - ui.radio_value(&mut d.pages, OcrPages::Current, tl!("Current page")); + widgets::radio_value(ui, &mut d.pages, OcrPages::All, tl!("All pages")); + widgets::radio_value(ui, &mut d.pages, OcrPages::Current, tl!("Current page")); ui.horizontal(|ui| { - ui.radio_value(&mut d.pages, OcrPages::Range, tl!("From")); + widgets::radio_value(ui, &mut d.pages, OcrPages::Range, tl!("From")); let on = d.pages == OcrPages::Range; ui.add_enabled(on, egui::DragValue::new(&mut d.from).range(1..=pages)); ui.label(tl!("to")); diff --git a/crates/ui-egui/src/optimize_ui.rs b/crates/ui-egui/src/optimize_ui.rs index 4d6c843e..a35f0297 100644 --- a/crates/ui-egui/src/optimize_ui.rs +++ b/crates/ui-egui/src/optimize_ui.rs @@ -107,7 +107,7 @@ fn summary(r: &OptimizeReport) -> String { fn image_row(ui: &mut egui::Ui, id: &str, title: &str, s: &mut ImageSettings) { ui.label(egui::RichText::new(tl!(title)).font(theme::semibold(13.0))); ui.horizontal(|ui| { - ui.checkbox(&mut s.downsample, tl!("Bicubic downsampling to")); + ui.add(widgets::checkbox(&mut s.downsample, tl!("Bicubic downsampling to"))); ui.add_enabled(s.downsample, egui::DragValue::new(&mut s.target_ppi).range(9.0..=2400.0).suffix(" ppi")); ui.label(tl!("for images above")); ui.add_enabled(s.downsample, egui::DragValue::new(&mut s.above_ppi).range(9.0..=2400.0).suffix(" ppi")); @@ -144,7 +144,7 @@ fn image_row(ui: &mut egui::Ui, id: &str, title: &str, s: &mut ImageSettings) { fn discard_box(ui: &mut egui::Ui, list: &mut Vec, h: Hidden, label: &str) { let mut on = list.contains(&h); - if ui.checkbox(&mut on, tl!(label)).changed() { + if ui.add(widgets::checkbox(&mut on, tl!(label))).changed() { if on { list.push(h); } else { @@ -184,10 +184,10 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo } OptimizeTab::DiscardObjects => { discard_box(ui, &mut d.discard, Hidden::LinksActionsScripts, "Discard all links, actions and JavaScript"); - ui.checkbox(&mut s.discard_alternate_images, tl!("Discard alternate images")); - ui.checkbox(&mut s.discard_thumbnails, tl!("Discard embedded page thumbnails")); - ui.checkbox(&mut s.discard_tags, tl!("Discard document tags")); - ui.checkbox(&mut s.discard_print_settings, tl!("Discard embedded print settings")); + ui.add(widgets::checkbox(&mut s.discard_alternate_images, tl!("Discard alternate images"))); + ui.add(widgets::checkbox(&mut s.discard_thumbnails, tl!("Discard embedded page thumbnails"))); + ui.add(widgets::checkbox(&mut s.discard_tags, tl!("Discard document tags"))); + ui.add(widgets::checkbox(&mut s.discard_print_settings, tl!("Discard embedded print settings"))); discard_box(ui, &mut d.discard, Hidden::Bookmarks, "Discard bookmarks"); discard_box(ui, &mut d.discard, Hidden::FormFields, "Flatten form fields"); discard_box(ui, &mut d.discard, Hidden::HiddenLayers, "Discard hidden layer content"); @@ -200,11 +200,11 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo discard_box(ui, &mut d.discard, Hidden::HiddenText, "Discard hidden text"); } OptimizeTab::CleanUp => { - ui.checkbox(&mut s.flate_unencoded, tl!("Use Flate to encode streams that are not encoded")); - ui.checkbox(&mut s.remove_invalid_links, tl!("Remove invalid links and bookmarks")); - ui.checkbox(&mut s.remove_unreferenced_dests, tl!("Remove unreferenced named destinations")); - ui.add_enabled(false, egui::Checkbox::new(&mut true, tl!("Compress document structure (object streams)"))); - ui.add_enabled(false, egui::Checkbox::new(&mut true, tl!("Remove unused objects and merge identical ones"))); + ui.add(widgets::checkbox(&mut s.flate_unencoded, tl!("Use Flate to encode streams that are not encoded"))); + ui.add(widgets::checkbox(&mut s.remove_invalid_links, tl!("Remove invalid links and bookmarks"))); + ui.add(widgets::checkbox(&mut s.remove_unreferenced_dests, tl!("Remove unreferenced named destinations"))); + ui.add_enabled(false, widgets::checkbox(&mut true, tl!("Compress document structure (object streams)"))); + ui.add_enabled(false, widgets::checkbox(&mut true, tl!("Remove unused objects and merge identical ones"))); } } ui.add_space(12.0); diff --git a/crates/ui-egui/src/pageboxes.rs b/crates/ui-egui/src/pageboxes.rs index fb998805..1522d000 100644 --- a/crates/ui-egui/src/pageboxes.rs +++ b/crates/ui-egui/src/pageboxes.rs @@ -135,15 +135,20 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo ui.end_row(); ui.label(tl!("Pages")); ui.vertical(|ui| { - ui.radio_value(&mut d.range, Range::All, tl!("All")); - ui.radio_value(&mut d.range, Range::Current, crate::i18n::fmt(tl!("Current page ({n})"), &[("n", &(current + 1).to_string())])); + widgets::radio_value(ui, &mut d.range, Range::All, tl!("All")); + widgets::radio_value( + ui, + &mut d.range, + Range::Current, + crate::i18n::fmt(tl!("Current page ({n})"), &[("n", &(current + 1).to_string())]), + ); let (mut a, mut b) = match d.range { Range::Pages(a, b) => (a, b), _ => (1, count), }; ui.horizontal(|ui| { let on = matches!(d.range, Range::Pages(..)); - if ui.radio(on, tl!("From")).clicked() { + if ui.add(widgets::radio(on, tl!("From"))).clicked() { d.range = Range::Pages(a, b); } let ra = ui.add(egui::DragValue::new(&mut a).range(1..=count)); diff --git a/crates/ui-egui/src/prepare.rs b/crates/ui-egui/src/prepare.rs index a0ebc211..37747f14 100644 --- a/crates/ui-egui/src/prepare.rs +++ b/crates/ui-egui/src/prepare.rs @@ -12,6 +12,7 @@ use pdfcraft_render::DocInfo; use crate::canvas::{DocView, PageXform}; use crate::theme; +use crate::widgets; const SELECT_BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6); /// Acrobat's field fill in Prepare Form (light blue). @@ -918,7 +919,7 @@ const OPTION_FLAGS: [u32; 10] = { /// A check box for a `/Ff` bit (`inverted`: checked when the bit is clear). fn flag_box(ui: &mut egui::Ui, flags: &mut u32, bit: u32, inverted: bool, label: &str) { let mut on = (*flags & bit != 0) != inverted; - if ui.checkbox(&mut on, label).changed() { + if ui.add(widgets::checkbox(&mut on, label)).changed() { if on != inverted { *flags |= bit; } else { @@ -965,15 +966,15 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke }); ui.add_space(12.0); widgets::section_title(ui, tl!("Common Properties")); - ui.checkbox(&mut d.read_only, tl!("Read Only")); - ui.checkbox(&mut d.required, tl!("Required")); + ui.add(widgets::checkbox(&mut d.read_only, tl!("Read Only"))); + ui.add(widgets::checkbox(&mut d.required, tl!("Required"))); } FieldTab::Appearance => { egui::Grid::new("field-appearance").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| { ui.label(tl!("Font Size:")); let mut auto = d.font_size == 0.0; ui.horizontal(|ui| { - if ui.checkbox(&mut auto, tl!("Auto")).changed() { + if ui.add(widgets::checkbox(&mut auto, tl!("Auto"))).changed() { d.font_size = if auto { 0.0 } else { 12.0 }; } ui.add_enabled(!auto, egui::DragValue::new(&mut d.font_size).range(2.0..=100.0).speed(0.25).suffix(" pt")); @@ -1004,7 +1005,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke ui.push_id(("field-color", index), |ui| { ui.horizontal(|ui| { let mut none = slot.is_none(); - if ui.checkbox(&mut none, tl!("No color")).changed() { + if ui.add(widgets::checkbox(&mut none, tl!("No color"))).changed() { *slot = if none { None } else { Some([0.0; 3]) }; } if let Some(c) = crate::comments::swatch_grid(ui, *slot) { @@ -1094,11 +1095,11 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke ui.end_row(); }); ui.add_space(6.0); - ui.checkbox(&mut d.multiline, tl!("Multi-line")); + ui.add(widgets::checkbox(&mut d.multiline, tl!("Multi-line"))); flag_box(ui, &mut d.flags, ff::DO_NOT_SCROLL, true, tl!("Scroll long text")); flag_box(ui, &mut d.flags, ff::RICH_TEXT, false, tl!("Allow Rich Text Formatting")); ui.horizontal(|ui| { - ui.checkbox(&mut d.limit, tl!("Limit of")); + ui.add(widgets::checkbox(&mut d.limit, tl!("Limit of"))); ui.add_enabled(d.limit, egui::DragValue::new(&mut d.max_len).range(0..=10_000)); ui.label(tl!("characters")); }); @@ -1126,7 +1127,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke let mut checked = !d.default.is_empty() && d.default == on; let label = if d.kind == FormFieldKind::CheckBox { tl!("Check box is checked by default") } else { tl!("Button is checked by default") }; - if ui.checkbox(&mut checked, label).changed() { + if ui.add(widgets::checkbox(&mut checked, label)).changed() { d.default = if checked { on } else { String::new() }; } if let Some(style) = d.check_style.as_mut() { @@ -1210,7 +1211,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke } }); ui.add_space(8.0); - ui.checkbox(&mut d.locked, tl!("Locked")); + ui.add(widgets::checkbox(&mut d.locked, tl!("Locked"))); ui.add_space(6.0); let (mut apply, mut cancel) = (enter, false); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { @@ -1323,7 +1324,7 @@ fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) { ui.selectable_value(currency, c.to_string(), if c.is_empty() { tl!("None") } else { c }); } }); - ui.checkbox(prepend, tl!("Before the number")); + ui.add(widgets::checkbox(prepend, tl!("Before the number"))); }); ui.end_row(); ui.label(tl!("Negative number style:")); @@ -1406,8 +1407,8 @@ fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) { fn validate_tab(ui: &mut egui::Ui, d: &mut FieldDraft) { let mut ranged = matches!(d.validate, Validate::Range { .. }); - ui.radio_value(&mut ranged, false, tl!("Field value is not validated")); - ui.radio_value(&mut ranged, true, tl!("Field value is in range:")); + widgets::radio_value(ui, &mut ranged, false, tl!("Field value is not validated")); + widgets::radio_value(ui, &mut ranged, true, tl!("Field value is in range:")); if !ranged { d.validate = Validate::None; return; @@ -1418,12 +1419,12 @@ fn validate_tab(ui: &mut egui::Ui, d: &mut FieldDraft) { if let Validate::Range { min, max } = &mut d.validate { ui.horizontal(|ui| { let mut has = min.is_some(); - ui.checkbox(&mut has, tl!("From:")); + ui.add(widgets::checkbox(&mut has, tl!("From:"))); let mut v = min.unwrap_or(0.0); ui.add_enabled(has, egui::DragValue::new(&mut v).speed(1.0)); *min = has.then_some(v); let mut has = max.is_some(); - ui.checkbox(&mut has, tl!("To:")); + ui.add(widgets::checkbox(&mut has, tl!("To:"))); let mut v = max.unwrap_or(100.0); ui.add_enabled(has, egui::DragValue::new(&mut v).speed(1.0)); *max = has.then_some(v); @@ -1521,8 +1522,8 @@ fn calculate_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens Calculate::Notation(_) => Mode::Notation, }; let mut m = mode; - ui.radio_value(&mut m, Mode::None, tl!("Value is not calculated")); - ui.radio_value(&mut m, Mode::Simple, tl!("Value is the")); + widgets::radio_value(ui, &mut m, Mode::None, tl!("Value is not calculated")); + widgets::radio_value(ui, &mut m, Mode::Simple, tl!("Value is the")); if m != mode { d.calculate = match m { Mode::None => Calculate::None, @@ -1543,7 +1544,7 @@ fn calculate_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens egui::ScrollArea::vertical().max_height(110.0).show(ui, |ui| { for name in &d.others { let mut on = fields.contains(name); - if ui.checkbox(&mut on, name).changed() { + if ui.add(widgets::checkbox(&mut on, name)).changed() { if on { fields.push(name.clone()); } else { @@ -1557,7 +1558,9 @@ fn calculate_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens Calculate::Notation(_) => Mode::Notation, _ => m, }; - if ui.radio_value(&mut m2, Mode::Notation, tl!("Simplified field notation:")).clicked() && !matches!(d.calculate, Calculate::Notation(_)) { + if widgets::radio_value(ui, &mut m2, Mode::Notation, tl!("Simplified field notation:")).clicked() + && !matches!(d.calculate, Calculate::Notation(_)) + { d.calculate = Calculate::Notation(String::new()); } if let Calculate::Notation(expr) = &mut d.calculate { diff --git a/crates/ui-egui/src/print_ui.rs b/crates/ui-egui/src/print_ui.rs index 213de3cf..2f1924d1 100644 --- a/crates/ui-egui/src/print_ui.rs +++ b/crates/ui-egui/src/print_ui.rs @@ -591,8 +591,8 @@ pub(crate) fn body( ui.label(tl!("Copies:")); ui.horizontal(|ui| { ui.add(egui::DragValue::new(&mut d.copies).range(1..=999)); - ui.checkbox(&mut d.collate, tl!("Collate")); - ui.checkbox(&mut d.grayscale, tl!("Print in grayscale")); + ui.add(widgets::checkbox(&mut d.collate, tl!("Collate"))); + ui.add(widgets::checkbox(&mut d.grayscale, tl!("Print in grayscale"))); }); ui.end_row(); ui.label(tl!("Two-sided:")); @@ -641,9 +641,9 @@ pub(crate) fn body( ui.add_space(6.0); widgets::section_title(ui, tl!("Pages to Print")); ui.horizontal(|ui| { - ui.radio_value(&mut d.which, Which::All, tl!("All")); - ui.radio_value(&mut d.which, Which::Current, tl!("Current page")); - ui.radio_value(&mut d.which, Which::Range, tl!("Pages")); + widgets::radio_value(ui, &mut d.which, Which::All, tl!("All")); + widgets::radio_value(ui, &mut d.which, Which::Current, tl!("Current page")); + widgets::radio_value(ui, &mut d.which, Which::Range, tl!("Pages")); let r = ui.add_enabled( d.which == Which::Range, crate::widgets::line(&mut d.range).hint_text(format!("1-{}", sizes.len())).desired_width(110.0), @@ -655,7 +655,7 @@ pub(crate) fn body( // Offered only when pages were picked before the dialog opened. if !d.selected.is_empty() { let label = format!("{} ({})", tl!("Selected pages"), d.selected.len()); - ui.radio_value(&mut d.which, Which::Selected, label); + widgets::radio_value(ui, &mut d.which, Which::Selected, label); } ui.horizontal(|ui| { ui.label(tl!("More options:")); @@ -666,7 +666,7 @@ pub(crate) fn body( &[(Subset::All, tl!("All pages in range")), (Subset::Odd, tl!("Odd pages only")), (Subset::Even, tl!("Even pages only"))], 150.0, ); - ui.checkbox(&mut d.reverse, tl!("Reverse pages")); + ui.add(widgets::checkbox(&mut d.reverse, tl!("Reverse pages"))); }); ui.add_space(6.0); widgets::section_title(ui, tl!("Page Sizing & Handling")); @@ -687,13 +687,13 @@ pub(crate) fn body( match d.handling { Handling::Size => { ui.horizontal(|ui| { - ui.radio_value(&mut d.size, SizeMode::Fit, tl!("Fit")); - ui.radio_value(&mut d.size, SizeMode::Actual, tl!("Actual size")); - ui.radio_value(&mut d.size, SizeMode::Shrink, tl!("Shrink oversized pages")); + widgets::radio_value(ui, &mut d.size, SizeMode::Fit, tl!("Fit")); + widgets::radio_value(ui, &mut d.size, SizeMode::Actual, tl!("Actual size")); + widgets::radio_value(ui, &mut d.size, SizeMode::Shrink, tl!("Shrink oversized pages")); }); ui.horizontal(|ui| { let custom = matches!(d.size, SizeMode::Custom(_)); - if ui.radio(custom, tl!("Custom scale:")).clicked() { + if ui.add(widgets::radio(custom, tl!("Custom scale:"))).clicked() { d.size = SizeMode::Custom(d.custom_scale); } ui.add_enabled(custom, egui::DragValue::new(&mut d.custom_scale).range(1.0..=1000.0).suffix(" %")); @@ -705,7 +705,7 @@ pub(crate) fn body( ui.add(egui::DragValue::new(&mut d.poster_scale).range(10.0..=1000.0).suffix(" %")); ui.label(tl!("Overlap:")); ui.add(egui::DragValue::new(&mut d.overlap).range(0.0..=144.0).suffix(" pt")); - ui.checkbox(&mut d.cut_marks, tl!("Cut marks")); + ui.add(widgets::checkbox(&mut d.cut_marks, tl!("Cut marks"))); }); } Handling::Multiple => { @@ -728,8 +728,8 @@ pub(crate) fn body( ); }); ui.horizontal(|ui| { - ui.checkbox(&mut d.border, tl!("Print page border")); - ui.checkbox(&mut d.auto_rotate, tl!("Auto-rotate pages")); + ui.add(widgets::checkbox(&mut d.border, tl!("Print page border"))); + ui.add(widgets::checkbox(&mut d.auto_rotate, tl!("Auto-rotate pages"))); }); if d.order == PageOrder::CutStack { ui.label(tl!( @@ -759,9 +759,9 @@ pub(crate) fn body( ui.add_space(6.0); ui.horizontal(|ui| { ui.label(tl!("Orientation:")); - ui.radio_value(&mut d.orientation, Orientation::Auto, tl!("Auto portrait/landscape")); - ui.radio_value(&mut d.orientation, Orientation::Portrait, tl!("Portrait")); - ui.radio_value(&mut d.orientation, Orientation::Landscape, tl!("Landscape")); + widgets::radio_value(ui, &mut d.orientation, Orientation::Auto, tl!("Auto portrait/landscape")); + widgets::radio_value(ui, &mut d.orientation, Orientation::Portrait, tl!("Portrait")); + widgets::radio_value(ui, &mut d.orientation, Orientation::Landscape, tl!("Landscape")); }); ui.add_space(6.0); widgets::section_title(ui, tl!("Comments & Forms")); diff --git a/crates/ui-egui/src/protect.rs b/crates/ui-egui/src/protect.rs index 9406b0dc..a4336d54 100644 --- a/crates/ui-egui/src/protect.rs +++ b/crates/ui-egui/src/protect.rs @@ -152,10 +152,10 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo ui.end_row(); ui.label(tl!("Encrypt")); ui.vertical(|ui| { - ui.radio_value(&mut p.encrypt_metadata, true, tl!("All document contents")); + widgets::radio_value(ui, &mut p.encrypt_metadata, true, tl!("All document contents")); // Unencrypted metadata needs crypt filters (Acrobat 6.0 and later). ui.add_enabled_ui(p.algorithm != A::Rc4_40 && p.algorithm != A::Rc4_128, |ui| { - ui.radio_value(&mut p.encrypt_metadata, false, tl!("All document contents except metadata")); + widgets::radio_value(ui, &mut p.encrypt_metadata, false, tl!("All document contents except metadata")); }); }); ui.end_row(); @@ -187,8 +187,8 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo ui.end_row(); ui.label(""); ui.vertical(|ui| { - ui.checkbox(&mut p.copy, tl!("Enable copying of text, images, and other content")); - ui.checkbox(&mut p.accessibility, tl!("Enable text access for screen reader devices for the visually impaired")); + ui.add(widgets::checkbox(&mut p.copy, tl!("Enable copying of text, images, and other content"))); + ui.add(widgets::checkbox(&mut p.accessibility, tl!("Enable text access for screen reader devices for the visually impaired"))); }); ui.end_row(); } diff --git a/crates/ui-egui/src/redact_ui.rs b/crates/ui-egui/src/redact_ui.rs index 4af45895..a0dc247b 100644 --- a/crates/ui-egui/src/redact_ui.rs +++ b/crates/ui-egui/src/redact_ui.rs @@ -248,9 +248,9 @@ impl PdfCraftApp { pub(crate) fn pages_body(ui: &mut egui::Ui, d: &mut PagesDraft, pages: usize, _t: &Tokens) -> (bool, bool) { ui.label(egui::RichText::new(tl!("Mark Page Range")).font(crate::theme::semibold(18.0))); ui.add_space(8.0); - ui.radio_value(&mut d.current, true, tl!("Current page")); + widgets::radio_value(ui, &mut d.current, true, tl!("Current page")); ui.horizontal(|ui| { - ui.radio_value(&mut d.current, false, tl!("Pages from")); + widgets::radio_value(ui, &mut d.current, false, tl!("Pages from")); ui.add_enabled(!d.current, egui::DragValue::new(&mut d.from).range(1..=pages)); ui.label(tl!("to")); ui.add_enabled(!d.current, egui::DragValue::new(&mut d.to).range(1..=pages)); @@ -266,14 +266,14 @@ pub(crate) fn search_body(ui: &mut egui::Ui, d: &mut SearchDraft, t: &Tokens) -> ui.set_width(420.0); ui.label(egui::RichText::new(tl!("Find text and redact")).font(crate::theme::semibold(18.0))); ui.add_space(8.0); - ui.radio_value(&mut d.mode, SearchMode::Phrase, tl!("Single word or phrase")); + widgets::radio_value(ui, &mut d.mode, SearchMode::Phrase, tl!("Single word or phrase")); let mut enter = false; ui.add_enabled_ui(d.mode == SearchMode::Phrase, |ui| { let r = ui.add(crate::widgets::line(&mut d.text).hint_text(tl!("Text to find")).desired_width(380.0)); enter = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); }); ui.add_space(6.0); - ui.radio_value(&mut d.mode, SearchMode::Words, tl!("Multiple words or phrases")); + widgets::radio_value(ui, &mut d.mode, SearchMode::Words, tl!("Multiple words or phrases")); let mut import = false; ui.add_enabled_ui(d.mode == SearchMode::Words, |ui| { egui::ScrollArea::vertical().id_salt("redact-words").max_height(120.0).show(ui, |ui| { @@ -286,7 +286,7 @@ pub(crate) fn search_body(ui: &mut egui::Ui, d: &mut SearchDraft, t: &Tokens) -> }); }); ui.add_space(6.0); - ui.radio_value(&mut d.mode, SearchMode::Patterns, tl!("Patterns")); + widgets::radio_value(ui, &mut d.mode, SearchMode::Patterns, tl!("Patterns")); ui.add_enabled_ui(d.mode == SearchMode::Patterns, |ui| { egui::ComboBox::from_id_salt("redact-pattern").selected_text(tl!(d.pattern.label())).width(240.0).show_ui(ui, |ui| { for p in REDACT_PATTERNS { @@ -320,7 +320,7 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) -> ui.label(tl!("Redacted area fill colour:")); ui.horizontal(|ui| { let mut none = d.fill.is_none(); - if ui.checkbox(&mut none, tl!("No colour")).changed() { + if ui.add(widgets::checkbox(&mut none, tl!("No colour"))).changed() { d.fill = if none { None } else { Some([0.0, 0.0, 0.0]) }; } }); @@ -331,16 +331,16 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) -> } ui.end_row(); ui.label(""); - ui.checkbox(&mut d.use_overlay, tl!("Use overlay text")); + ui.add(widgets::checkbox(&mut d.use_overlay, tl!("Use overlay text"))); ui.end_row(); let on = d.use_overlay; - let l = ui.add_enabled(on, egui::RadioButton::new(!d.use_code, tl!("Custom text:"))); + let l = ui.add_enabled(on, widgets::radio(!d.use_code, tl!("Custom text:"))); if l.clicked() { d.use_code = false; } ui.add_enabled(on && !d.use_code, crate::widgets::line(&mut d.overlay).desired_width(220.0)).labelled_by(l.id); ui.end_row(); - if ui.add_enabled(on, egui::RadioButton::new(d.use_code, tl!("Redaction code:"))).clicked() { + if ui.add_enabled(on, widgets::radio(d.use_code, tl!("Redaction code:"))).clicked() { d.use_code = true; } let codes_on = on && d.use_code; @@ -361,7 +361,7 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) -> ui.horizontal(|ui| { for &code in row { let mut picked = d.codes.contains(&code); - if ui.add_sized([62.0, 18.0], egui::Checkbox::new(&mut picked, code)).changed() { + if ui.add_sized([62.0, 18.0], widgets::checkbox(&mut picked, code)).changed() { d.codes.retain(|c| *c != code); if picked { d.codes.push(code); @@ -386,7 +386,7 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) -> ui.add_enabled_ui(on, |ui| { ui.horizontal(|ui| { let mut auto = look.size <= 0.0; - if ui.checkbox(&mut auto, tl!("Auto-size text to fit redaction region")).changed() { + if ui.add(widgets::checkbox(&mut auto, tl!("Auto-size text to fit redaction region"))).changed() { look.size = if auto { 0.0 } else { 10.0 }; } if !auto { @@ -403,13 +403,13 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) -> }); ui.end_row(); ui.label(""); - ui.add_enabled(on, egui::Checkbox::new(&mut look.repeat, tl!("Repeat overlay text"))); + ui.add_enabled(on, widgets::checkbox(&mut look.repeat, tl!("Repeat overlay text"))); ui.end_row(); ui.label(tl!("Text alignment:")); ui.add_enabled_ui(on, |ui| { ui.horizontal(|ui| { for (a, label) in [(0u8, tl!("Left")), (1, tl!("Center")), (2, tl!("Right"))] { - ui.radio_value(&mut look.align, a, label); + widgets::radio_value(ui, &mut look.align, a, label); } }); }); @@ -480,7 +480,7 @@ pub(crate) fn hidden_body(ui: &mut egui::Ui, d: &mut HiddenDraft, t: &Tokens) -> for (h, n, on) in d.found.iter_mut() { ui.add_enabled_ui(*n > 0, |ui| { ui.horizontal(|ui| { - ui.checkbox(on, tl!(h.label())); + ui.add(widgets::checkbox(on, tl!(h.label()))); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.label(egui::RichText::new(if *n == 0 { tl!("None found").to_string() } else { n.to_string() }).color(t.text_muted)); }); diff --git a/crates/ui-egui/src/search_ui.rs b/crates/ui-egui/src/search_ui.rs index c23968ba..630ca898 100644 --- a/crates/ui-egui/src/search_ui.rs +++ b/crates/ui-egui/src/search_ui.rs @@ -6,6 +6,7 @@ use egui::{Color32, FontId, TextFormat}; use crate::canvas::DocView; use crate::theme::{self, Tokens}; +use crate::widgets; /// Glyphs of context shown on each side of a match. const CONTEXT: usize = 36; @@ -28,8 +29,8 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, view: &mut DocView, pages: us let w = &mut ui.visuals_mut().widgets; w.inactive.bg_stroke = egui::Stroke::new(1.0, t.border); w.inactive.bg_fill = t.hover; - let a = ui.checkbox(&mut find.whole_words, tl!("Whole words only")).changed(); - let b = ui.checkbox(&mut find.case_sensitive, tl!("Case-sensitive")).changed(); + let a = ui.add(widgets::checkbox(&mut find.whole_words, tl!("Whole words only"))).changed(); + let b = ui.add(widgets::checkbox(&mut find.case_sensitive, tl!("Case-sensitive"))).changed(); if a || b { find.case_query.clear(); } diff --git a/crates/ui-egui/src/sign_ui.rs b/crates/ui-egui/src/sign_ui.rs index 14f90956..b2cc5f9a 100644 --- a/crates/ui-egui/src/sign_ui.rs +++ b/crates/ui-egui/src/sign_ui.rs @@ -652,8 +652,8 @@ fn configure(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { // Browse… is desktop-only. #[cfg_attr(target_arch = "wasm32", allow(unused_mut))] let mut browse = false; - ui.radio_value(&mut d.new_id.create, false, tl!("Use a Digital ID from a file")); - ui.radio_value(&mut d.new_id.create, true, tl!("Create a new Digital ID (self-signed, saved to a password-protected file)")); + widgets::radio_value(ui, &mut d.new_id.create, false, tl!("Use a Digital ID from a file")); + widgets::radio_value(ui, &mut d.new_id.create, true, tl!("Create a new Digital ID (self-signed, saved to a password-protected file)")); ui.add_space(8.0); let n = &mut d.new_id; if n.create { @@ -831,24 +831,24 @@ fn sign_as(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { preview(ui, t, &entry.name, d, graphic.as_ref(), &mut app.sign_graphic_preview); ui.horizontal_wrapped(|ui| { let a = &mut d.appearance; - ui.checkbox(&mut a.name, tl!("Name")); - ui.checkbox(&mut a.date, tl!("Date")); - ui.checkbox(&mut a.reason, tl!("Reason")); - ui.checkbox(&mut a.location, tl!("Location")); - ui.checkbox(&mut a.distinguished_name, tl!("Distinguished name")); - ui.checkbox(&mut a.labels, tl!("Labels")); + ui.add(widgets::checkbox(&mut a.name, tl!("Name"))); + ui.add(widgets::checkbox(&mut a.date, tl!("Date"))); + ui.add(widgets::checkbox(&mut a.reason, tl!("Reason"))); + ui.add(widgets::checkbox(&mut a.location, tl!("Location"))); + ui.add(widgets::checkbox(&mut a.distinguished_name, tl!("Distinguished name"))); + ui.add(widgets::checkbox(&mut a.labels, tl!("Labels"))); }); // Acrobat: Configure Signature Appearance, Text, Draw or Image. ui.horizontal_wrapped(|ui| { ui.label(tl!("Graphic")); - ui.radio_value(&mut d.graphic, SignGraphic::Name, tl!("Signer's name")); + widgets::radio_value(ui, &mut d.graphic, SignGraphic::Name, tl!("Signer's name")); ui.add_enabled_ui(app.signature.is_some(), |ui| { - if ui.radio(d.graphic == SignGraphic::FillSign, tl!("Fill & Sign signature")).clicked() { + if ui.add(widgets::radio(d.graphic == SignGraphic::FillSign, tl!("Fill & Sign signature"))).clicked() { d.graphic = SignGraphic::FillSign; } }); ui.add_enabled_ui(d.image.is_some(), |ui| { - if ui.radio(matches!(d.graphic, SignGraphic::Image(_)), tl!("Image")).clicked() + if ui.add(widgets::radio(matches!(d.graphic, SignGraphic::Image(_)), tl!("Image"))).clicked() && let Some(image) = &d.image { d.graphic = SignGraphic::Image(image.clone()); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 1fef87e4..bf440d3a 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -188,6 +188,76 @@ pub fn menu_item(ui: &mut egui::Ui, label: &str, shortcut: &str) -> Response { ui.add(egui::Button::new(label).shortcut_text(shortcut)) } +/// Corner radius of a checkbox's box. With the widgets' radius (`Tokens::radius`, 6) egui's 14 px +/// box is a circle, the shape of a radio button, and View ▸ Page display has both. +const CHECKBOX_RADIUS: u8 = 3; + +/// A checkbox whose box is an outlined rounded square, never mistaken for a radio button. Every +/// checkbox goes through here: clippy.toml disallows `ui.checkbox` and `egui::Checkbox`. +pub fn checkbox<'a>(checked: &'a mut bool, atoms: impl egui::IntoAtoms<'a>) -> impl egui::Widget + 'a { + #[expect(clippy::disallowed_methods, reason = "the one place that styles the box")] + let widget = egui::Checkbox::new(checked, atoms); + toggle(widget, Some(CHECKBOX_RADIUS)) +} + +/// [`checkbox`] without a label: the selection box on Organize Pages thumbnails. +pub fn checkbox_without_text(checked: &mut bool) -> impl egui::Widget + '_ { + #[expect(clippy::disallowed_methods, reason = "the one place that styles the box")] + let widget = egui::Checkbox::without_text(checked); + toggle(widget, Some(CHECKBOX_RADIUS)) +} + +/// A radio button with an outline and a fill, like [`checkbox`]. Every radio button goes through +/// here or [`radio_value`]: clippy.toml disallows `ui.radio`, `ui.radio_value` and +/// `egui::RadioButton`. +pub fn radio<'a>(selected: bool, atoms: impl egui::IntoAtoms<'a>) -> impl egui::Widget + 'a { + #[expect(clippy::disallowed_methods, reason = "the one place that styles the circle")] + let widget = egui::RadioButton::new(selected, atoms); + toggle(widget, None) +} + +/// [`radio`] selected while `*current == alternative`; a click selects it (`Ui::radio_value`). +pub fn radio_value<'a, V: PartialEq>(ui: &mut egui::Ui, current: &mut V, alternative: V, atoms: impl egui::IntoAtoms<'a>) -> Response { + let mut response = ui.add(radio(*current == alternative, atoms)); + if response.clicked() && *current != alternative { + *current = alternative; + response.mark_changed(); + } + response +} + +/// The box of a checkbox or the circle of a radio button. egui draws it with the widgets' visuals +/// and has no style of its own for it, so it is set in a scope around the widget, keeping what +/// the caller set (dialogs and the search panel already outline theirs): +/// - an outline, darker on hover; +/// - on the light theme, the plain fill (`Tokens::hover`), as dialogs have: the field colour is +/// the panels' white, so the box would vanish; +/// - for a checkbox, a corner radius small enough to keep the box square. +fn toggle<'a>(widget: impl egui::Widget + 'a, radius: Option) -> impl egui::Widget + 'a { + move |ui: &mut egui::Ui| { + ui.scope(|ui| { + let t = Tokens::get(ui.ctx()); + let w = &mut ui.visuals_mut().widgets; + if let Some(radius) = radius { + for v in [&mut w.noninteractive, &mut w.inactive, &mut w.hovered, &mut w.active, &mut w.open] { + v.corner_radius = CornerRadius::same(radius); + } + } + if !t.dark() && w.inactive.bg_fill == t.field { + w.inactive.bg_fill = t.hover; + } + if w.inactive.bg_stroke.is_empty() { + w.inactive.bg_stroke = Stroke::new(1.0, t.border); + } + if w.hovered.bg_stroke.is_empty() { + w.hovered.bg_stroke = Stroke::new(1.0, t.text_muted); + } + ui.add(widget) + }) + .inner + } +} + pub fn section_title(ui: &mut egui::Ui, text: &str) { let t = Tokens::get(ui.ctx()); ui.add_space(10.0); @@ -443,6 +513,70 @@ mod tests { crate::i18n::set_current(previous); } + /// View ▸ Page display mixes radio buttons and checkboxes. With the theme's 6 px widget radius + /// egui's 14 px checkbox box drew as a circle, and on the light theme an unticked box or an + /// unselected radio button was white on white with no outline, so it couldn't be seen. + #[test] + fn checkboxes_and_radio_buttons_are_outlined_filled_and_distinct() { + for kind in [theme::ThemeKind::Light, theme::ThemeKind::Dark] { + let ctx = egui::Context::default(); + theme::apply(&ctx, kind); + let t = Tokens::for_kind(kind); + let side = ctx.global_style().spacing.icon_width; + let output = ctx.run_ui(egui::RawInput::default(), |ui| { + ui.add(checkbox(&mut false, "Show cover page in two-page view")); + ui.add(radio(false, "Single page")); + }); + let boxes: Vec<_> = output + .shapes + .iter() + .filter_map(|s| match &s.shape { + egui::Shape::Rect(r) if (r.rect.width() - side).abs() < 0.5 && (r.rect.height() - side).abs() < 0.5 => Some(r), + _ => None, + }) + .collect(); + let circles: Vec<_> = output + .shapes + .iter() + .filter_map(|s| match &s.shape { + egui::Shape::Circle(c) if (c.radius * 2.0 - side).abs() < 1.0 => Some(c), + _ => None, + }) + .collect(); + assert_eq!((boxes.len(), circles.len()), (1, 1), "{kind:?}: one checkbox box and one radio circle"); + let r = boxes[0].corner_radius; + let radius = r.nw.max(r.ne).max(r.sw).max(r.se); + assert!(f32::from(radius) <= side / 4.0, "{kind:?}: a {side} px box with corner radius {radius} looks like a radio button"); + for (what, fill, edge) in [("checkbox", boxes[0].fill, boxes[0].stroke), ("radio button", circles[0].fill, circles[0].stroke)] { + assert!(fill != t.panel && fill != t.card, "{kind:?}: an unselected {what} is filled like the panel behind it"); + assert!( + edge.width >= 1.0 && edge.color != t.panel && edge.color != t.card, + "{kind:?}: an unselected {what} needs an outline, got {edge:?}" + ); + } + output.drop_without_applying_deltas(); + } + // An outline the caller set (dialogs) is kept. + let ctx = egui::Context::default(); + let own = Stroke::new(1.5, Color32::RED); + let output = ctx.run_ui(egui::RawInput::default(), |ui| { + ui.visuals_mut().widgets.inactive.bg_stroke = own; + ui.add(checkbox(&mut true, "Collate")); + ui.add(radio(false, "All")); + }); + let kept = output + .shapes + .iter() + .filter(|s| match &s.shape { + egui::Shape::Rect(r) => r.stroke == own, + egui::Shape::Circle(c) => c.stroke == own, + _ => false, + }) + .count(); + assert_eq!(kept, 2, "the checkbox and the radio button keep the caller's outline"); + output.drop_without_applying_deltas(); + } + fn toast_rect(app: &mut PdfCraftApp, ctx: &egui::Context, width: f32, now: &mut f64) -> Rect { // Let the Area settle after a notice/viewport change, without advancing to // expiry. No native rendering or renderer worker is needed for this layout.