mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-06 02:21:45 +00:00
Merge branch 'storytold:main' into fix/panel-resize-scrollbar-5730
This commit is contained in:
7 files changed
+1277
-486
No files matched your search
@@ -180,3 +180,6 @@ sync_github_name = true
|
||||
|
||||
[people.dylanmtaylor]
|
||||
sync_github_name = true
|
||||
|
||||
[people.Combiler01]
|
||||
sync_github_name = true
|
||||
@@ -14,6 +14,7 @@ pub(super) mod credits;
|
||||
pub(super) mod marks_ui;
|
||||
pub(super) mod pageboxes;
|
||||
pub(super) mod palette;
|
||||
pub(super) mod preferences;
|
||||
mod prompts;
|
||||
pub(super) mod protect;
|
||||
pub(super) mod standards_ui;
|
||||
@@ -127,6 +128,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
Dialog::Properties(_) => 640.0,
|
||||
Dialog::Print => 820.0,
|
||||
Dialog::FieldProps | Dialog::BulkFieldProps => 600.0,
|
||||
Dialog::Preferences => 610.0,
|
||||
Dialog::About => 780.0,
|
||||
Dialog::PinTool => 540.0,
|
||||
_ => 520.0,
|
||||
@@ -1356,10 +1358,13 @@ fn dialog_signature(ui: &mut egui::Ui, app: &mut PdfCraftApp, req: &mut Pending)
|
||||
|
||||
/// `Dialog::Preferences` content. Returns `false` when it has its own buttons and no footer is wanted.
|
||||
fn dialog_preferences(ui: &mut egui::Ui, app: &mut PdfCraftApp, req: &mut Pending) -> bool {
|
||||
ui.set_width(610.0);
|
||||
let t = Tokens::get(ui.ctx());
|
||||
preferences::preferences_header(ui, &t);
|
||||
ui.add_space(10.0);
|
||||
// The settings outgrow a short window: scroll them rather than run off the screen.
|
||||
// The OK row stays below the scroll, in reach however far the settings are scrolled.
|
||||
let max = (ui.ctx().content_rect().height() - 120.0 - crate::js_ui::PREFERENCES_BUTTON_ROW).max(crate::js_ui::PREFERENCES_MIN_SCROLL);
|
||||
let max = (ui.ctx().content_rect().height() - 120.0 - preferences::PREFERENCES_BUTTON_ROW - 40.0).max(preferences::PREFERENCES_MIN_SCROLL);
|
||||
// A scroll area starts from the room the dialog offers it, and a modal offers more
|
||||
// each frame for the first few, so with OK outside the scroll the dialog would grow
|
||||
// over those frames. Asking for the full height as the least it may scroll in
|
||||
@@ -1368,9 +1373,17 @@ fn dialog_preferences(ui: &mut egui::Ui, app: &mut PdfCraftApp, req: &mut Pendin
|
||||
// cards and other full-width rows (#3557).
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().scroll.floating = false;
|
||||
egui::ScrollArea::vertical().max_height(max).min_scrolled_height(max).show(ui, |ui| crate::js_ui::preferences_body(ui, app, &t));
|
||||
ui.spacing_mut().scroll.bar_inner_margin = 14.0;
|
||||
ui.spacing_mut().scroll.bar_outer_margin = 2.0;
|
||||
ui.style_mut().scroll_animation = egui::style::ScrollAnimation::duration(0.25);
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("preferences-dialog-scroll")
|
||||
.max_height(max)
|
||||
.min_scrolled_height(max)
|
||||
.animated(true)
|
||||
.show(ui, |ui| preferences::preferences_body(ui, app, &t));
|
||||
});
|
||||
req.close = crate::js_ui::preferences_buttons(ui);
|
||||
req.close = preferences::preferences_buttons(ui);
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
//! Preferences › Appearance: the appearance mode (follow the system, light or dark) above a light
|
||||
//! and a dark theme card, each with a small picture of PdfCraft in that theme.
|
||||
|
||||
use egui::{Rect, RichText, pos2, vec2};
|
||||
use egui::{CornerRadius, Rect, RichText, Stroke, 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")];
|
||||
@@ -82,43 +81,70 @@ fn preview(ui: &mut egui::Ui, kind: ThemeKind, width: f32) {
|
||||
}
|
||||
}
|
||||
|
||||
/// One theme card: its title, Active when it is what shows now, a preview of the chosen theme
|
||||
/// and a radio row per theme. Returns true when the card or one of its radio rows was clicked.
|
||||
fn card(ui: &mut egui::Ui, t: &Tokens, title: &str, active: bool, themes: &[(ThemeKind, &str)]) -> bool {
|
||||
let inner = egui::Frame::new()
|
||||
.fill(t.card)
|
||||
.stroke(egui::Stroke::new(1.0, if active { t.accent } else { t.border }))
|
||||
.corner_radius(6.0)
|
||||
.inner_margin(egui::Margin::same(8))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new(title).font(theme::semibold(12.5)).color(t.text));
|
||||
if active {
|
||||
ui.label(RichText::new(tl!("Active")).color(t.accent_text).small());
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let shown = themes.first().map_or(ThemeKind::Light, |(kind, _)| *kind);
|
||||
preview(ui, shown, ui.available_width());
|
||||
ui.add_space(4.0);
|
||||
let mut clicked = false;
|
||||
for (kind, label) in themes {
|
||||
// One theme per card today, so it is always the chosen one.
|
||||
clicked |= ui.add(widgets::radio(*kind == shown, tl!(label))).clicked();
|
||||
}
|
||||
clicked
|
||||
});
|
||||
// The whole card picks its theme, not just the radio row.
|
||||
let whole = inner.response.interact(egui::Sense::click()).on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
inner.inner || whole.clicked()
|
||||
/// One theme card: its title, Active when it is what shows now, and a preview of the chosen theme.
|
||||
/// Returns whether the card was clicked and the control's Response.
|
||||
fn card(ui: &mut egui::Ui, t: &Tokens, title: &str, active: bool, themes: &[(ThemeKind, &str)], query: &str) -> (bool, egui::Response) {
|
||||
let mut card_clicked = false;
|
||||
let frame_stroke = if active { Stroke::new(1.5, t.accent) } else { Stroke::new(1.0, t.border) };
|
||||
let scope_res = ui.scope_builder(egui::UiBuilder::new().id_salt(title).sense(egui::Sense::click()), |ui| {
|
||||
egui::Frame::new().fill(t.card).stroke(frame_stroke).corner_radius(CornerRadius::same(6)).inner_margin(egui::Margin::same(8)).show(
|
||||
ui,
|
||||
|ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
let font = theme::semibold(12.5);
|
||||
let ranges = crate::dialogs::preferences::find_match_ranges(title, query);
|
||||
let galley = if ranges.is_empty() {
|
||||
crate::bidi::layout_ui(ui, title.to_owned(), font, t.text)
|
||||
} else {
|
||||
let (job, _) = crate::dialogs::preferences::create_highlight_job(title, &ranges, font, t.text);
|
||||
ui.painter().layout_job(job)
|
||||
};
|
||||
let (title_rect, _) = ui.allocate_exact_size(galley.size(), egui::Sense::hover());
|
||||
ui.painter().galley(title_rect.min, galley, t.text);
|
||||
if active {
|
||||
ui.label(RichText::new(tl!("Active")).color(t.accent_text).small());
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let shown = themes.first().map_or(ThemeKind::Light, |(kind, _)| *kind);
|
||||
preview(ui, shown, ui.available_width());
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
let resp = scope_res.response;
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::RadioButton, true, active, title));
|
||||
|
||||
if resp.has_focus() {
|
||||
ui.painter().rect_stroke(resp.rect, CornerRadius::same(6), Stroke::new(2.0, t.accent_text), egui::StrokeKind::Outside);
|
||||
}
|
||||
|
||||
let resp = theme::hand(resp);
|
||||
let key_select = resp.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Space) || i.key_pressed(egui::Key::Enter));
|
||||
if resp.clicked() || key_select {
|
||||
card_clicked = true;
|
||||
}
|
||||
(card_clicked, resp)
|
||||
}
|
||||
|
||||
/// The Appearance section of Preferences.
|
||||
#[cfg_attr(not(test), expect(dead_code, reason = "called by unit tests"))]
|
||||
pub fn preferences_section(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) {
|
||||
ui.label(RichText::new(tl_ctx!("theme", "Appearance")).font(theme::semibold(13.0)));
|
||||
let mut ctx = crate::dialogs::preferences::PrefSearchContext::new("", false);
|
||||
preferences_section_with_context(ui, app, t, &mut ctx);
|
||||
}
|
||||
|
||||
/// The Appearance section of Preferences with live search highlighting and scrolling context.
|
||||
pub(crate) fn preferences_section_with_context(
|
||||
ui: &mut egui::Ui,
|
||||
app: &mut PdfCraftApp,
|
||||
t: &Tokens,
|
||||
ctx: &mut crate::dialogs::preferences::PrefSearchContext,
|
||||
) {
|
||||
ctx.label(ui, tl_ctx!("theme", "Appearance"), theme::semibold(13.0), t.text);
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Appearance mode"));
|
||||
let label = ctx.label(ui, tl!("Appearance mode"), theme::regular(13.0), t.text);
|
||||
let combo = craft_ui::ComboBox::from_id_salt("appearance-mode").selected_text(tl!(mode_label(app.theme_preference))).show_ui(ui, |ui| {
|
||||
for preference in [ThemePreference::System, ThemePreference::Light, ThemePreference::Dark] {
|
||||
if ui.selectable_label(app.theme_preference == preference, tl!(mode_label(preference))).clicked() {
|
||||
@@ -132,18 +158,29 @@ pub fn preferences_section(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let light_active = app.theme == ThemeKind::Light;
|
||||
let light_title = tl!("Light theme");
|
||||
let dark_title = tl!("Dark theme");
|
||||
let light_matches = crate::dialogs::preferences::find_match_ranges(light_title, ctx.query).len();
|
||||
let dark_matches = crate::dialogs::preferences::find_match_ranges(dark_title, ctx.query).len();
|
||||
ctx.total_matches += light_matches + dark_matches;
|
||||
|
||||
ui.columns(2, |cols| {
|
||||
let [light, dark] = cols else { return };
|
||||
if card(light, t, tl!("Light theme"), light_active, LIGHT_THEMES) {
|
||||
let (light_clicked, light_resp) = card(light, t, light_title, light_active, LIGHT_THEMES, ctx.query);
|
||||
if light_clicked {
|
||||
app.execute(mode_command(ThemePreference::Light));
|
||||
}
|
||||
if card(dark, t, tl!("Dark theme"), !light_active, DARK_THEMES) {
|
||||
ctx.scroll_if_first_match(&light_resp, light_matches);
|
||||
|
||||
let (dark_clicked, dark_resp) = card(dark, t, dark_title, !light_active, DARK_THEMES, ctx.query);
|
||||
if dark_clicked {
|
||||
app.execute(mode_command(ThemePreference::Dark));
|
||||
}
|
||||
ctx.scroll_if_first_match(&dark_resp, dark_matches);
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
// Acrobat's "Highlight existing fields": shared with the toggle in the form bar.
|
||||
ui.add(crate::widgets::checkbox(&mut app.view_defaults.highlight_fields, tl!("Highlight fields")));
|
||||
ctx.checkbox(ui, &mut app.view_defaults.highlight_fields, tl!("Highlight fields"), t);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -175,7 +212,6 @@ mod tests {
|
||||
);
|
||||
h.run();
|
||||
assert!(h.query_by_label("Light theme").is_some() && h.query_by_label("Dark theme").is_some());
|
||||
assert!(h.query_by_label("Light gray").is_some() && h.query_by_label("Dark gray").is_some());
|
||||
assert_eq!(h.query_all_by_label("Active").count(), 1);
|
||||
if let Ok(path) = std::env::var("PDFCRAFT_APPEARANCE_SHOT") {
|
||||
h.render().unwrap().save(path).unwrap();
|
||||
@@ -200,7 +236,7 @@ mod tests {
|
||||
assert_eq!(h.state().theme_preference, ThemePreference::System);
|
||||
}
|
||||
|
||||
/// Clicking a theme card, or its radio row, picks that theme, not only the mode combo box.
|
||||
/// Clicking a theme card picks that theme, not only the mode combo box.
|
||||
#[test]
|
||||
fn clicking_a_theme_card_picks_that_theme() {
|
||||
let mut app = PdfCraftApp::new();
|
||||
@@ -219,10 +255,9 @@ mod tests {
|
||||
app,
|
||||
);
|
||||
h.run();
|
||||
h.get_by_label("Dark gray").click();
|
||||
h.get_by_label("Dark theme").click();
|
||||
h.run();
|
||||
assert_eq!((h.state().theme_preference, h.state().theme), (ThemePreference::Dark, ThemeKind::Dark));
|
||||
// The card's title is not a widget of its own: the click lands on the card.
|
||||
h.get_by_label("Light theme").click();
|
||||
h.run();
|
||||
assert_eq!((h.state().theme_preference, h.state().theme), (ThemePreference::Light, ThemeKind::Light));
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -316,445 +316,10 @@ pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
||||
close
|
||||
}
|
||||
|
||||
/// The height the Preferences dialog keeps below its scrolling settings for the OK row: the
|
||||
/// button, and the space above it.
|
||||
pub(crate) const PREFERENCES_BUTTON_ROW: f32 = 52.0;
|
||||
|
||||
/// The least height the scrolling settings are given. The dialog around them takes about 80 more,
|
||||
/// so this still fits the smallest window (520 pt) at the largest interface size (300%, which
|
||||
/// leaves 173 pt): a taller floor would push OK off the screen again.
|
||||
pub(crate) const PREFERENCES_MIN_SCROLL: f32 = 60.0;
|
||||
|
||||
/// Whether the interface can draw a language picker's native name. On the web the browser's glyph
|
||||
/// rasterizer draws scripts no embedded face has, so the native names stay; on native, ask the
|
||||
/// fonts this build installed (including an installed fallback face, unless system fonts are off).
|
||||
fn picker_text_is_drawable(ui: &egui::Ui, text: &str) -> bool {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = (ui, text);
|
||||
true
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
ui.fonts_mut(|fonts| fonts.has_glyphs(&egui::FontId::proportional(13.0), text))
|
||||
}
|
||||
}
|
||||
|
||||
/// The date picker's displayed name. Its languages are the interface languages, so it uses the
|
||||
/// same drawable-name fallback: a native name whose script this build has no face for would
|
||||
/// otherwise abort under egui 0.37's `MissingGlyphPolicy::Panic` (for example Hindi without a
|
||||
/// Devanagari face and installed system fonts turned off).
|
||||
fn date_language_picker_name(l: &pdfcraft_engine::dates::DateLanguage, drawable: impl FnOnce(&str) -> bool) -> &'static str {
|
||||
match crate::i18n::Lang::from_code(l.code) {
|
||||
Some(language) => language.picker_name(drawable),
|
||||
None => l.name,
|
||||
}
|
||||
}
|
||||
|
||||
/// Preferences: interface language and size, appearance, identity and JavaScript. Drawn in a scroll
|
||||
/// area; [`preferences_buttons`] goes below it, outside the scroll, so OK is always in reach.
|
||||
pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) {
|
||||
ui.label(egui::RichText::new(tl!("Preferences")).font(theme::semibold(18.0)));
|
||||
ui.label(egui::RichText::new(tl!("Appearance")).font(theme::semibold(13.0)));
|
||||
if ui.add(widgets::checkbox(&mut app.show_page_edge_shadows, tl!("Show Page Edge Shadows"))).changed() {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Interface language"));
|
||||
// A native name is only drawn when these fonts can draw it; otherwise the English name
|
||||
// keeps the list readable instead of aborting on a missing glyph (for example Hindi
|
||||
// without a Devanagari face, and installed system fonts turned off).
|
||||
let (selected, picker) = {
|
||||
let drawable = |text: &str| picker_text_is_drawable(ui, text);
|
||||
let selected = crate::i18n::Lang::from_code(&app.language)
|
||||
.map_or_else(|| tl!("Auto").to_owned(), |language| language.picker_name(drawable).to_owned());
|
||||
let picker = crate::i18n::Lang::for_picker_by(|language| language.picker_name(drawable))
|
||||
.into_iter()
|
||||
.map(|language| (language, language.picker_name(drawable)))
|
||||
.collect::<Vec<_>>();
|
||||
(selected, picker)
|
||||
};
|
||||
let before = app.language.clone();
|
||||
craft_ui::ComboBox::from_id_salt("interface-language")
|
||||
.selected_text(selected)
|
||||
.show_ui(ui, |ui| {
|
||||
if ui.selectable_value(&mut app.language, crate::i18n::AUTO.to_string(), tl!("Auto")).clicked() {
|
||||
ui.close();
|
||||
}
|
||||
for (language, name) in &picker {
|
||||
if ui.selectable_value(&mut app.language, language.code().to_string(), *name).clicked() {
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
})
|
||||
.response
|
||||
.labelled_by(label.id);
|
||||
// Relabel the rest of this dialog in the new language right away, not next frame.
|
||||
if app.language != before {
|
||||
crate::i18n::set_current(crate::i18n::Lang::from_pref(&app.language));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
use crate::ui_scale;
|
||||
let label = ui.label(tl!("Interface size"));
|
||||
// Auto shows the size it draws at: the display's scale times the system's text scale.
|
||||
let native = ui.ctx().native_pixels_per_point().unwrap_or(1.0);
|
||||
let auto_percent = ui_scale::percent(ui_scale::drawn(None, app.system_text_scale, native));
|
||||
let auto = crate::i18n::fmt(tl!("Auto ({percent}%)"), &[("percent", &auto_percent.to_string())]);
|
||||
let selected = app.ui_scale.map_or_else(|| auto.clone(), |f| format!("{}%", ui_scale::percent(f)));
|
||||
craft_ui::ComboBox::from_id_salt("interface-size")
|
||||
.selected_text(selected)
|
||||
.show_ui(ui, |ui| {
|
||||
if ui.selectable_value(&mut app.ui_scale, None, auto).clicked() {
|
||||
ui.close();
|
||||
}
|
||||
for p in ui_scale::PRESETS {
|
||||
let factor = f32::from(p) / 100.0;
|
||||
if ui.selectable_label(app.ui_scale.is_some_and(|f| ui_scale::percent(f) == u32::from(p)), format!("{p}%")).clicked() {
|
||||
app.ui_scale = Some(factor);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
})
|
||||
.response
|
||||
.labelled_by(label.id);
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new(tl!("Auto follows the system's text size. Pages always render at the display's full resolution."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Tools panel side"));
|
||||
let right = app.side_panels.tools_on_right();
|
||||
if ui.selectable_label(!right, tl!("Left")).clicked() {
|
||||
app.side_panels.tools_side = crate::side_panels::ToolsSide::Left;
|
||||
}
|
||||
if ui.selectable_label(right, tl!("Right")).clicked() {
|
||||
app.side_panels.tools_side = crate::side_panels::ToolsSide::Right;
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Navigation rail side"));
|
||||
let right = app.side_panels.rail_on_right();
|
||||
let left_btn = ui.selectable_label(!right, tl!("Left"));
|
||||
left_btn.widget_info(|| {
|
||||
egui::WidgetInfo::selected(egui::Role::Button, true, !right, format!("{}: {}", tl!("Navigation rail side"), tl!("Left")))
|
||||
});
|
||||
if left_btn.clicked() {
|
||||
app.side_panels.rail_side = crate::side_panels::ToolsSide::Left;
|
||||
}
|
||||
let right_btn = ui.selectable_label(right, tl!("Right"));
|
||||
right_btn.widget_info(|| {
|
||||
egui::WidgetInfo::selected(egui::Role::Button, true, right, format!("{}: {}", tl!("Navigation rail side"), tl!("Right")))
|
||||
});
|
||||
if right_btn.clicked() {
|
||||
app.side_panels.rail_side = crate::side_panels::ToolsSide::Right;
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
crate::appearance::preferences_section(ui, app, t);
|
||||
ui.add_space(8.0);
|
||||
reading_colours_section(ui, app, t);
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new(tl!("Documents and view")).font(theme::semibold(13.0)));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Default workspace mode"));
|
||||
for (mode, label) in [
|
||||
(crate::Mode::AllTools, "All tools"),
|
||||
(crate::Mode::Read, "Read"),
|
||||
(crate::Mode::Edit, "Edit"),
|
||||
(crate::Mode::Convert, "Convert"),
|
||||
(crate::Mode::Sign, "E-Sign"),
|
||||
] {
|
||||
widgets::radio_value(ui, &mut app.default_mode, mode, tl!(label));
|
||||
}
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new(tl!("Used when opening PDFs. An explicit launch or control mode takes precedence for the session."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
let defaults = &mut app.view_defaults;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Default page display"));
|
||||
for l in crate::canvas::PageLayout::ORDER {
|
||||
widgets::radio_value(ui, &mut defaults.layout, l, tl!(l.label()));
|
||||
}
|
||||
ui.add(widgets::checkbox(&mut defaults.continuous, tl!("Continuous scrolling")));
|
||||
});
|
||||
ui.add(widgets::checkbox(&mut defaults.show_page_gaps, tl!("Show gaps between pages")));
|
||||
ui.label(egui::RichText::new(tl!("Hide gaps to join page edges. Margins inside the PDF remain unchanged.")).small().color(t.text_muted));
|
||||
ui.horizontal(|ui| {
|
||||
use crate::canvas::Fit;
|
||||
ui.label(tl!("Default zoom"));
|
||||
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() {
|
||||
(defaults.fit, defaults.zoom) = (Fit::None, percent / 100.0);
|
||||
}
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new(tl!("Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
ui.add(widgets::checkbox(&mut app.always_default_view, tl!("Always use my default zoom and page layout")));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Units"));
|
||||
for u in crate::units::Unit::ALL {
|
||||
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 list.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Recent files to keep"));
|
||||
ui.add(egui::DragValue::new(&mut app.recent_limit).range(0..=crate::RECENT_FILES_MAX)).labelled_by(label.id);
|
||||
});
|
||||
ui.label(egui::RichText::new(tl!("Lowering this forgets the oldest files. At 0 there is no list.")).small().color(t.text_muted));
|
||||
}
|
||||
// The web build has no file paths to reopen.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ui.add(widgets::checkbox(&mut app.reopen_last_session, tl!("Reopen the files that were open when PdfCraft last closed")));
|
||||
// Acrobat's "Restore last view settings when reopening documents" (#1767).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ui.add(widgets::checkbox(&mut app.restore_last_view, tl!("Restore the last view (page, zoom and page display) when a document is opened again")));
|
||||
// The web build has no files on disk to watch.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ui.add(widgets::checkbox(&mut app.reload_changed_files, tl!("Reload documents when their files change on disk")));
|
||||
// The notice's "Don't show again" turns this off (#4035); the restrictions stay either way.
|
||||
ui.add(widgets::checkbox(&mut app.show_secured_notice, tl!("Show a notice when a document is secured")));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Scroll speed (mouse wheel and arrow keys)"));
|
||||
for speed in crate::scroll_speed::ScrollSpeed::ALL {
|
||||
widgets::radio_value(ui, &mut app.scroll_speed, speed, tl_ctx!("scroll speed", speed.label()));
|
||||
}
|
||||
});
|
||||
// Whether anything animates, the wheel's scrolling and zooming included; left alone it
|
||||
// follows the system's animation setting.
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Animation effects"));
|
||||
for choice in crate::motion::AnimationEffects::ALL {
|
||||
widgets::radio_value(ui, &mut app.animation_effects, choice, tl_ctx!("animation effects", choice.label()));
|
||||
}
|
||||
});
|
||||
// Only where the app draws the title bar (GNOME on Wayland); elsewhere the desktop draws it.
|
||||
if app.window_controls.is_some() {
|
||||
ui.add(widgets::checkbox(&mut app.system_title_bar, tl!("Use system title bar")))
|
||||
.on_hover_text(tl!("Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme."));
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
// Identity: the author of new comments (Acrobat: Preferences ▸ Identity).
|
||||
ui.label(egui::RichText::new(tl!("Identity")).font(theme::semibold(13.0)));
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Name on new comments"));
|
||||
ui.add(crate::widgets::line(&mut app.comment_prefs.author).desired_width(220.0).char_limit(crate::MAX_AUTHOR_CHARS)).labelled_by(label.id);
|
||||
});
|
||||
ui.add(widgets::checkbox(&mut app.comment_prefs.hide_empty_highlight_hover, tl!("Hide hover previews for highlights without comment text")));
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new(tl!("Fill & Sign")).font(theme::semibold(13.0)));
|
||||
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),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
date_format(ui, app, t);
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new("JavaScript").font(theme::semibold(13.0)));
|
||||
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.add(widgets::checkbox(&mut on, tl!("Enable Acrobat JavaScript"))).changed() {
|
||||
app.session.set_javascript(on);
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new(tl!("Scripts run in a sandbox without file or network access. A script that asks to open a web page needs your permission first. With JavaScript off, Acrobat's standard format, validate and calculate functions still work."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The Preferences dialog's OK row. Returns `true` to close.
|
||||
pub(crate) fn preferences_buttons(ui: &mut egui::Ui) -> bool {
|
||||
ui.add_space(10.0);
|
||||
buttons(ui, tl!("OK"), &[]).is_some()
|
||||
}
|
||||
|
||||
/// Preferences ▸ Date format: a preset, or any pattern typed in, for Fill & Sign dates, and the
|
||||
/// language of its month and weekday names. A valid pattern applies as it's typed; an invalid
|
||||
/// one stays in the box with the reason, and the last valid one stays in use.
|
||||
fn date_format(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) {
|
||||
use pdfcraft_engine::dates::{DATE_FORMATS, DATE_LANGUAGES, MAX_DATE_FORMAT_CHARS};
|
||||
ui.label(egui::RichText::new(tl!("Date format")).font(theme::semibold(13.0)));
|
||||
let sample = |app: &PdfCraftApp, f: &str| format!("{} {f}", app.date_text(Some(f)).unwrap_or_default());
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Fill & Sign dates"));
|
||||
let current = app.session.date_format().to_string();
|
||||
let selected = if DATE_FORMATS.contains(¤t.as_str()) { sample(app, ¤t) } else { tl!("Custom").to_string() };
|
||||
craft_ui::ComboBox::from_id_salt("date-format")
|
||||
.selected_text(selected)
|
||||
.width(240.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for f in DATE_FORMATS {
|
||||
if ui.selectable_label(current == f, sample(app, f)).clicked() && app.session.set_date_format(f).is_ok() {
|
||||
app.date_format_draft = None;
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
})
|
||||
.response
|
||||
.labelled_by(label.id);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Pattern"));
|
||||
let mut text = app.date_format_draft.clone().unwrap_or_else(|| app.session.date_format().to_string());
|
||||
let edit = ui.add(egui::TextEdit::singleline(&mut text).desired_width(160.0).char_limit(MAX_DATE_FORMAT_CHARS)).labelled_by(label.id);
|
||||
// The box keeps the text as typed (a trailing space included) while it has focus; the
|
||||
// setting takes the trimmed pattern whenever it's valid.
|
||||
if edit.changed() {
|
||||
let _ = app.session.set_date_format(&text);
|
||||
app.date_format_draft = Some(text.clone());
|
||||
}
|
||||
if !edit.has_focus() && pdfcraft_engine::dates::check_date_format(&text).is_ok() {
|
||||
app.date_format_draft = None;
|
||||
}
|
||||
match app.date_text(Some(&text)) {
|
||||
Ok(today) => ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Today: {date}"), &[("date", &today)])).color(t.text_muted)),
|
||||
// The engine's reason, in English.
|
||||
Err(e) => ui.label(egui::RichText::new(e).color(egui::Color32::from_rgb(0xD1, 0x3B, 0x3B))),
|
||||
};
|
||||
});
|
||||
// Fill & Sign refuses a date it would save as "?"; say so before anyone tries.
|
||||
let bad = app.date_text(None).map(|today| pdfcraft_engine::dates::unwritable(&today)).unwrap_or_default();
|
||||
if !bad.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!(
|
||||
"{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy."
|
||||
),
|
||||
&[("chars", &bad)],
|
||||
))
|
||||
.color(egui::Color32::from_rgb(0xD1, 0x3B, 0x3B)),
|
||||
);
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Language"));
|
||||
// Build every label before the combo box mutably borrows `ui`, with the same drawable
|
||||
// fallback the interface language picker uses.
|
||||
let (follow, selected, current, names) = {
|
||||
let drawable = |text: &str| picker_text_is_drawable(ui, text);
|
||||
let interface = crate::i18n::current().picker_name(drawable);
|
||||
let follow = crate::i18n::fmt(tl!("Same as interface ({language})"), &[("language", interface)]);
|
||||
let current = app.session.date_language().map(str::to_string);
|
||||
let selected = DATE_LANGUAGES
|
||||
.iter()
|
||||
.find(|l| current.as_deref() == Some(l.code))
|
||||
.map_or_else(|| follow.clone(), |l| date_language_picker_name(l, drawable).to_string());
|
||||
let names = DATE_LANGUAGES.iter().map(|l| (l, date_language_picker_name(l, drawable))).collect::<Vec<_>>();
|
||||
(follow, selected, current, names)
|
||||
};
|
||||
craft_ui::ComboBox::from_id_salt("date-language")
|
||||
.selected_text(selected)
|
||||
.width(240.0)
|
||||
.show_ui(ui, |ui| {
|
||||
if ui.selectable_label(current.is_none(), follow).clicked() && app.session.set_date_language(None).is_ok() {
|
||||
ui.close();
|
||||
}
|
||||
for (l, name) in &names {
|
||||
if ui.selectable_label(current.as_deref() == Some(l.code), *name).clicked() && app.session.set_date_language(Some(l.code)).is_ok()
|
||||
{
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
})
|
||||
.response
|
||||
.labelled_by(label.id);
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is."),
|
||||
&[("escape", "\\")],
|
||||
))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
}
|
||||
|
||||
fn reading_colours_section(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) {
|
||||
// Reading colours (Acrobat: Replace Document Colors): solid text and line-art paints are
|
||||
// remapped while rendering, and the page clears to the background colour. The preference
|
||||
// is app-wide: every open document follows it, and documents open with it.
|
||||
ui.label(egui::RichText::new(tl!("Reading colours")).font(theme::semibold(13.0)));
|
||||
let mode = app.reading_colors.mode_name();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (name, label, colors) in [
|
||||
("off", "Off", pdfcraft_render::ReadingColors::default()),
|
||||
("black-on-white", "Black on white", pdfcraft_render::ReadingColors::BLACK_ON_WHITE),
|
||||
("white-on-black", "White on black", pdfcraft_render::ReadingColors::WHITE_ON_BLACK),
|
||||
("yellow-on-black", "Yellow on black", pdfcraft_render::ReadingColors::YELLOW_ON_BLACK),
|
||||
("green-on-black", "Green on black", pdfcraft_render::ReadingColors::GREEN_ON_BLACK),
|
||||
] {
|
||||
let params = colors.params.unwrap_or(pdfcraft_render::ReadingParams {
|
||||
text: [0, 0, 0],
|
||||
background: [255, 255, 255],
|
||||
only_black: false,
|
||||
line_art: true,
|
||||
});
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(104.0);
|
||||
let sample = egui::RichText::new("Aa").size(22.0).color(egui::Color32::from_rgb(params.text[0], params.text[1], params.text[2]));
|
||||
let button = egui::Button::new(sample)
|
||||
.fill(egui::Color32::from_rgb(params.background[0], params.background[1], params.background[2]))
|
||||
.min_size(egui::vec2(94.0, 44.0))
|
||||
.stroke(egui::Stroke::new(if mode == name { 2.0 } else { 1.0 }, if mode == name { t.accent } else { t.border }));
|
||||
let response = ui.add(button);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, mode == name, tl!(label)));
|
||||
if response.clicked() {
|
||||
app.set_reading_colors(colors);
|
||||
}
|
||||
ui.label(tl!(label));
|
||||
});
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
if let Some(params) = app.reading_colors.params {
|
||||
let text = egui::Color32::from_rgb(params.text[0], params.text[1], params.text[2]);
|
||||
let background = egui::Color32::from_rgb(params.background[0], params.background[1], params.background[2]);
|
||||
egui::Frame::new().fill(background).inner_margin(16.0).show(ui, |ui| {
|
||||
ui.set_min_width((ui.available_width() - 32.0).max(0.0));
|
||||
ui.label(egui::RichText::new(tl!("Reading colours")).size(18.0).color(text));
|
||||
ui.label(egui::RichText::new(tl!("Colours are remapped while rendering; the document itself never changes.")).color(text));
|
||||
});
|
||||
} else {
|
||||
ui.label(tl!("Original"));
|
||||
}
|
||||
egui::CollapsingHeader::new(tl!("Custom")).default_open(mode == "custom").show(ui, |ui| {
|
||||
let mut colors = if app.reading_colors.params.is_some() { app.reading_colors } else { pdfcraft_render::ReadingColors::BLACK_ON_WHITE };
|
||||
let mut changed = false;
|
||||
if let Some(params) = colors.params.as_mut() {
|
||||
ui.horizontal(|ui| {
|
||||
let text = ui.label(tl!("Text"));
|
||||
changed |= ui.color_edit_button_srgb(&mut params.text).labelled_by(text.id).changed();
|
||||
let background = ui.label(tl!("Page background"));
|
||||
changed |= ui.color_edit_button_srgb(&mut params.background).labelled_by(background.id).changed();
|
||||
});
|
||||
changed |= ui.add(crate::widgets::checkbox(&mut params.only_black, tl!("Only change the colour of black text or line art"))).changed();
|
||||
changed |= ui.add(crate::widgets::checkbox(&mut params.line_art, tl!("Change the colour of line art as well as text"))).changed();
|
||||
}
|
||||
if changed {
|
||||
app.set_reading_colors(colors);
|
||||
}
|
||||
});
|
||||
ui.label(egui::RichText::new(tl!("Colours are remapped while rendering; the document itself never changes.")).small().color(t.text_muted));
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
#[expect(unused_imports, reason = "re-exported for tests and backward compatibility")]
|
||||
pub(crate) use crate::dialogs::preferences::{
|
||||
PREFERENCES_BUTTON_ROW, PREFERENCES_MIN_SCROLL, date_language_picker_name, preferences_body, preferences_buttons, preferences_header,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -102,3 +102,152 @@ fn a_very_tall_window_does_not_leave_a_blank_gap_above_ok() {
|
||||
assert!(on_screen(ok, 2600.0) && on_screen(last, 2600.0), "{last:?} {ok:?}");
|
||||
assert!(ok.min.y - last.max.y < 120.0, "a gap of {} pt between the last setting and OK", ok.min.y - last.max.y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segmented_pill_controls_switch_modes_and_layouts() {
|
||||
let mut h = harness(900.0);
|
||||
// Switch workspace mode via segmented pill
|
||||
h.get_by_role_and_label(egui::accesskit::Role::RadioButton, "Convert").scroll_to_me();
|
||||
h.run_steps(3);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::RadioButton, "Convert").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().default_mode, pdfcraft_ui_egui::Mode::Convert);
|
||||
|
||||
let continuous_before = h.state().view_defaults.continuous;
|
||||
|
||||
// Switch page layout via segmented pill
|
||||
h.get_by_label("Two-page view").scroll_to_me();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Two-page view").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().view_defaults.layout, pdfcraft_ui_egui::canvas::PageLayout::TwoUp);
|
||||
assert_eq!(h.state().view_defaults.continuous, continuous_before, "Two-page view retains continuous scrolling");
|
||||
|
||||
// Switch continuous scrolling pill (can be combined with Two-page view)
|
||||
h.get_by_label("Continuous scrolling").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().view_defaults.continuous, !continuous_before);
|
||||
assert_eq!(h.state().view_defaults.layout, pdfcraft_ui_egui::canvas::PageLayout::TwoUp, "two-page continuous scrolling is supported");
|
||||
|
||||
// Switch to single page mode (continuous setting remains independent)
|
||||
h.get_by_label("Single page").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().view_defaults.layout, pdfcraft_ui_egui::canvas::PageLayout::Single);
|
||||
assert_eq!(h.state().view_defaults.continuous, !continuous_before);
|
||||
|
||||
// Switch tools panel side via segmented pill
|
||||
h.get_by_label("Left").scroll_to_me();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Left").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().side_panels.tools_side, pdfcraft_ui_egui::side_panels::ToolsSide::Left);
|
||||
|
||||
// Switch measurement units via segmented pill
|
||||
h.get_by_label("pt").scroll_to_me();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("pt").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().units, pdfcraft_ui_egui::units::Unit::Points);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_settings_scans_and_highlights_in_situ_without_hiding_cards() {
|
||||
let mut h = harness(900.0);
|
||||
// Initially all card titles exist
|
||||
assert!(h.query_by_label("General & Interface").is_some());
|
||||
assert!(h.query_by_label("Comment").is_some());
|
||||
assert!(h.query_by_label("JavaScript").is_some());
|
||||
|
||||
// Type "Hide hover" in search box
|
||||
h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Search settings…").click();
|
||||
h.run_steps(1);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Search settings…").type_text("Hide hover");
|
||||
h.run_steps(4);
|
||||
|
||||
// In browser-style search, cards are NOT hidden
|
||||
assert!(h.query_by_label("General & Interface").is_some());
|
||||
assert!(h.query_by_label("Comment").is_some());
|
||||
assert!(h.query_by_label("JavaScript").is_some());
|
||||
|
||||
// The setting "Hide hover previews for highlights without comment text" is present and matched
|
||||
assert!(h.query_by_label("Hide hover previews for highlights without comment text").is_some());
|
||||
// Match badge displays 1 match
|
||||
assert!(h.query_by_label("1 match").is_some());
|
||||
|
||||
// Clearing search clears match badge
|
||||
h.get_by_label("Clear search").click();
|
||||
h.run_steps(4);
|
||||
assert!(h.query_by_label("1 match").is_none());
|
||||
|
||||
// Search for "javascript"
|
||||
h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Search settings…").click();
|
||||
h.run_steps(1);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Search settings…").type_text("javascript");
|
||||
h.run_steps(4);
|
||||
|
||||
// Cards remain visible and multiple matches are reported
|
||||
assert!(h.query_by_label("JavaScript").is_some());
|
||||
assert!(h.query_by_label("General & Interface").is_some());
|
||||
assert!(h.query_by_label_contains("matches").is_some());
|
||||
|
||||
// Clearing search shows everything again
|
||||
h.get_by_label("Clear search").click();
|
||||
h.run_steps(4);
|
||||
assert!(h.query_by_label("General & Interface").is_some());
|
||||
assert!(h.query_by_label("Comment").is_some());
|
||||
assert!(h.query_by_label("JavaScript").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_dialog_scrollbar_spacing_and_controls() {
|
||||
let h = harness(600.0);
|
||||
let title = h.get_by(|n| n.role() == egui::accesskit::Role::Label && n.value().as_deref() == Some("Preferences")).rect();
|
||||
let search = h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Search settings…").rect();
|
||||
let ok = h.get_by_label("OK").rect();
|
||||
assert!(on_screen(ok, 600.0));
|
||||
// Header title and search box are vertically aligned
|
||||
assert!((title.center().y - search.center().y).abs() <= 2.0);
|
||||
// Search box right edge is aligned within the dialog
|
||||
assert!(search.max.x <= ok.max.x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_scrolls_option_only_and_theme_card_matches_into_visible_viewport() {
|
||||
let mut h = harness(480.0);
|
||||
let title = h.get_by(|n| n.role() == egui::accesskit::Role::Label && n.value().as_deref() == Some("Preferences")).rect();
|
||||
let ok = h.get_by_label("OK").rect();
|
||||
|
||||
// "Two-page view" is an option in Card 3 (Documents and view).
|
||||
// In a 480 pt window, it starts out below the visible scroll viewport.
|
||||
let opt_before = h.get_by_label("Two-page view").rect();
|
||||
assert!(opt_before.min.y >= ok.min.y, "Two-page option starts out below the visible viewport: {opt_before:?} vs OK {ok:?}");
|
||||
|
||||
// Searching "Two-page" (which only occurs inside that segmented pill option)
|
||||
// auto-scrolls the matching option into the visible viewport between title and OK.
|
||||
h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Search settings…").click();
|
||||
h.run_steps(1);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Search settings…").type_text("Two-page");
|
||||
h.run_steps(4);
|
||||
|
||||
let opt_after = h.get_by_label("Two-page view").rect();
|
||||
assert!(
|
||||
opt_after.min.y >= title.max.y && opt_after.max.y <= ok.min.y,
|
||||
"Two-page option scrolled into visible scroll viewport: {opt_after:?} between title {title:?} and OK {ok:?}"
|
||||
);
|
||||
|
||||
// Clear search and test a theme-card-only match
|
||||
h.get_by_label("Clear search").click();
|
||||
h.run_steps(4);
|
||||
|
||||
// Search for "Dark theme" (which occurs only in the dark theme card in Card 2).
|
||||
h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Search settings…").click();
|
||||
h.run_steps(1);
|
||||
h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Search settings…").type_text("Dark theme");
|
||||
h.run_steps(4);
|
||||
|
||||
let dark_card = h.get_by_label("Dark theme").rect();
|
||||
assert!(
|
||||
dark_card.min.y >= title.max.y && dark_card.max.y <= ok.min.y,
|
||||
"Dark theme card scrolled into visible viewport: {dark_card:?} between title {title:?} and OK {ok:?}"
|
||||
);
|
||||
}
|
||||
@@ -242,11 +242,11 @@ fn preferences_theme_cards_and_use_system_setting_stay_in_sync() {
|
||||
assert_theme(&h, ThemePreference::System, ThemeKind::Dark);
|
||||
assert_eq!(mode_shown(&h), "Use system setting");
|
||||
|
||||
// The OS is dark: the Dark card's radio row picks Dark, and the combo box says so.
|
||||
// The OS is dark: the Dark card picks Dark, and the combo box says so.
|
||||
// The cards sit below the fold of the Preferences scroll area at this window size.
|
||||
h.get_by_label("Dark gray").scroll_to_me();
|
||||
h.get_by_label("Dark theme").scroll_to_me();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Dark gray").click();
|
||||
h.get_by_label("Dark theme").click();
|
||||
h.run_steps(3);
|
||||
assert_theme(&h, ThemePreference::Dark, ThemeKind::Dark);
|
||||
assert_eq!(mode_shown(&h), "Dark");
|
||||
|
||||
Reference in new issue
Block a user