From 03996b10258288e723c4e07334c9cd1bae1f68a8 Mon Sep 17 00:00:00 2001 From: Lucas Sampaio Dias Date: Wed, 28 Oct 2026 18:41:29 -0300 Subject: [PATCH] Improve reading-colour preferences and add a settings shortcut (#2627) * M3: improve reading-colour preferences and add settings shortcut * M3: clarify the disabled reading-colour preview * M3: adapt rebased preferences to current UI dependencies --- crates/ui-egui/src/chrome.rs | 9 +- crates/ui-egui/src/menus.rs | 4 + crates/ui-egui/src/workflows/js_ui.rs | 147 +++++++++--------- crates/ui-egui/tests/it/preferences_layout.rs | 24 +-- crates/ui-egui/tests/it/reading_colors.rs | 54 +++++-- crates/ui-egui/tests/it/theme_button.rs | 14 ++ crates/ui-egui/tests/it/ui.rs | 8 +- parity/acrobat-features.toml | 4 +- 8 files changed, 154 insertions(+), 110 deletions(-) diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index a46f2173c8..aa6233c133 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -69,10 +69,10 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) { app.active = None; app.combine_tab.focused = false; } - // Keep the two icon buttons, Discord's text/padding and three gaps outside the scrolling tabs. + // Reserve the three icon buttons, Discord and their gaps outside the scrolling tabs. let controls_width = ui.fonts_mut(|f| f.layout_no_wrap("Discord".into(), theme::medium(13.0), t.text).size().x) - + 2.0 * button - + 50.0 + + 3.0 * button + + 54.0 + window_controls::width(&controls.right); // Open stays in sight after the tabs: its icon, text and padding, and the gaps around it. let open_width = ui.fonts_mut(|f| f.layout_no_wrap(tl!("Open").into(), theme::medium(13.0), t.text).size().x) + 38.0 + 12.0; @@ -287,6 +287,9 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) { }); ui.add_space(window_controls::SIDE_GAP - 4.0); } + if icons::button(ui, "settings", button, false, tl!("Preferences")).clicked() { + app.execute("app.preferences"); + } let (icon, label) = match app.theme_preference { ThemePreference::System => ("monitor", tl!("Use system setting")), ThemePreference::Light => ("sun", tl!("Light gray")), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index d7c8bef1cd..f58a7d972f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -254,6 +254,10 @@ fn english(labeled: Labeled<'_>) -> Option { } fn translate(english: &'static str) -> String { + // The bundled UI fonts do not all cover the menu's mathematical ellipsis. + if english == "\u{22ef}" { + return "...".to_owned(); + } tl!(english).to_owned() } diff --git a/crates/ui-egui/src/workflows/js_ui.rs b/crates/ui-egui/src/workflows/js_ui.rs index 0ebdbb07a6..4296487e4c 100644 --- a/crates/ui-egui/src/workflows/js_ui.rs +++ b/crates/ui-egui/src/workflows/js_ui.rs @@ -265,18 +265,6 @@ pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok close } -/// The label for a [`pdfcraft_render::ReadingColors`] mode name (`mode_name`). -fn reading_mode_label(mode: &str) -> &'static str { - match mode { - "black-on-white" => "Black on white", - "white-on-black" => "White on black", - "yellow-on-black" => "Yellow on black", - "green-on-black" => "Green on black", - "custom" => "Custom", - _ => "Off", - } -} - /// 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; @@ -394,6 +382,8 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok 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")); @@ -478,70 +468,6 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok .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); - // 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))); - ui.horizontal(|ui| { - let label = ui.label(tl!("Replace document colours")); - let mode = app.reading_colors.mode_name(); - craft_ui::ComboBox::from_id_salt("reading-colors") - .selected_text(tl!(reading_mode_label(mode))) - .show_ui(ui, |ui| { - for (m, label) in [ - ("off", "Off"), - ("black-on-white", "Black on white"), - ("white-on-black", "White on black"), - ("yellow-on-black", "Yellow on black"), - ("green-on-black", "Green on black"), - ("custom", "Custom"), - ] { - if ui.selectable_label(mode == m, tl!(label)).clicked() { - match m { - "off" => app.set_reading_colors(pdfcraft_render::ReadingColors::default()), - "custom" => { - // Custom starts from the colours already chosen, or black on white. - let colors = app.reading_colors.params.map_or(pdfcraft_render::ReadingColors::BLACK_ON_WHITE, |params| { - pdfcraft_render::ReadingColors { params: Some(params) } - }); - app.set_reading_colors(colors); - } - // `parse` knows these names; a preset comes with its own flags. - preset => { - if let Ok(colors) = pdfcraft_render::ReadingColors::parse(preset, None, None, None, None) { - app.set_reading_colors(colors); - } - } - } - ui.close(); - } - } - }) - .response - .labelled_by(label.id); - }); - if app.reading_colors.params.is_some() { - let mut changed = false; - if let Some(params) = app.reading_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(); - }); - if ui.add(crate::widgets::checkbox(&mut params.only_black, tl!("Only change the colour of black text or line art"))).changed() { - changed = true; - } - if ui.add(crate::widgets::checkbox(&mut params.line_art, tl!("Change the colour of line art as well as text"))).changed() { - changed = true; - } - } - if changed { - app.set_reading_colors(app.reading_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); // Identity: the author of new comments (Acrobat: Preferences ▸ Identity). ui.label(egui::RichText::new(tl!("Identity")).font(theme::semibold(13.0))); ui.horizontal(|ui| { @@ -679,6 +605,75 @@ fn date_format(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) { ); } +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); +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ui-egui/tests/it/preferences_layout.rs b/crates/ui-egui/tests/it/preferences_layout.rs index 3b8d41ec06..1ebe06b7a0 100644 --- a/crates/ui-egui/tests/it/preferences_layout.rs +++ b/crates/ui-egui/tests/it/preferences_layout.rs @@ -29,7 +29,7 @@ fn ok_is_on_screen_without_scrolling_at_any_window_height() { let h = harness(height); let ok = h.get_by_label("OK").rect(); assert!(on_screen(ok, height), "{height}: OK at {ok:?} is off the {height} pt window"); - let title = h.get_by_label("Preferences").rect(); + let title = h.get_by(|n| n.role() == egui::accesskit::Role::Label && n.value().as_deref() == Some("Preferences")).rect(); assert!(on_screen(title, height), "{height}: the title at {title:?} is off the window"); // The OK row sits below the scrolled settings, never over them: the settings' last visible // row ends above it. The title is above everything. @@ -43,13 +43,13 @@ fn ok_closes_the_dialog_in_a_short_window_and_the_settings_still_scroll() { // A setting far down the list starts out below the visible settings; scrolling brings it in // between the title and the OK row, and OK did not move. let ok_before = h.get_by_label("OK").rect(); - let title = h.get_by_label("Preferences").rect(); - let far = h.get_by_label_contains("Reading colours").rect(); - assert!(far.min.y >= ok_before.min.y, "Reading colours starts out of sight: {far:?} vs OK {ok_before:?}"); - h.get_by_label_contains("Reading colours").scroll_to_me(); + let title = h.get_by(|n| n.role() == egui::accesskit::Role::Label && n.value().as_deref() == Some("Preferences")).rect(); + let far = h.get_by_label_contains("Enable Acrobat JavaScript").rect(); + assert!(far.min.y >= ok_before.min.y, "JavaScript setting starts out of sight: {far:?} vs OK {ok_before:?}"); + h.get_by_label_contains("Enable Acrobat JavaScript").scroll_to_me(); h.run_steps(4); - let far = h.get_by_label_contains("Reading colours").rect(); - assert!(far.min.y >= title.max.y && far.max.y <= ok_before.min.y, "Reading colours scrolled in between the title and OK: {far:?}"); + let far = h.get_by_label_contains("Enable Acrobat JavaScript").rect(); + assert!(far.min.y >= title.max.y && far.max.y <= ok_before.min.y, "JavaScript setting scrolled in between the title and OK: {far:?}"); assert_eq!(h.get_by_label("OK").rect(), ok_before, "OK stays put while the settings scroll"); assert!(h.state().dialog.is_some()); h.get_by_label("OK").click(); @@ -64,9 +64,15 @@ fn the_dialog_opens_at_its_full_size_and_does_not_grow_afterwards() { // in its first frames landed on controls that were still moving. for height in [260.0, 560.0, 900.0] { let mut h = harness(height); - let settled = (h.get_by_label("Preferences").rect(), h.get_by_label("OK").rect()); + let settled = ( + h.get_by(|n| n.role() == egui::accesskit::Role::Label && n.value().as_deref() == Some("Preferences")).rect(), + h.get_by_label("OK").rect(), + ); h.run_steps(8); - let later = (h.get_by_label("Preferences").rect(), h.get_by_label("OK").rect()); + let later = ( + h.get_by(|n| n.role() == egui::accesskit::Role::Label && n.value().as_deref() == Some("Preferences")).rect(), + h.get_by_label("OK").rect(), + ); assert_eq!(settled, later, "{height}: the dialog moved after it had been shown for six frames"); } // And it is in place within a few frames of opening, for the first click: a frame or two of diff --git a/crates/ui-egui/tests/it/reading_colors.rs b/crates/ui-egui/tests/it/reading_colors.rs index 398a85e8e0..ac48d89d05 100644 --- a/crates/ui-egui/tests/it/reading_colors.rs +++ b/crates/ui-egui/tests/it/reading_colors.rs @@ -100,17 +100,6 @@ fn png() -> Vec { out } -/// The Reading colours combo of the preferences dialog: it shows the chosen mode as its value, -/// and no other combo in the dialog shows `Off` while the setting is off. -fn combo(h: &mut Harness<'static, PdfCraftApp>) { - // The preferences dialog grew taller than the window, which leaves this combo below the - // fold where a click lands outside the dialog and dismisses it; scroll it into view and - // let the scroll settle before clicking. - h.get_by(|n| n.role() == Role::ComboBox && n.value().is_some_and(|v| v == "Off")).scroll_to_me(); - h.run_steps(4); - h.get_by(|n| n.role() == Role::ComboBox && n.value().is_some_and(|v| v == "Off")).click(); -} - #[test] fn reading_colours_preference_round_trips() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { @@ -122,11 +111,9 @@ fn reading_colours_preference_round_trips() { }); h.run_steps(4); assert_eq!(h.state().reading_colors, ReadingColors::default()); - // Switch the preset to white-on-black in the dialog's Reading colours combo. The open - // document keeps repainting, so run a fixed number of steps and let the popup settle. - combo(&mut h); - h.run_steps(20); - h.get_by_label("White on black").click(); + h.get_by(|n| n.role() == Role::Button && n.label().as_deref() == Some("White on black")).scroll_to_me(); + h.run_steps(4); + h.get_by(|n| n.role() == Role::Button && n.label().as_deref() == Some("White on black")).click(); h.run_steps(2); assert_eq!(h.state().reading_colors, ReadingColors::WHITE_ON_BLACK); // The preference is app-wide: the already open document follows it too. @@ -151,6 +138,41 @@ fn reading_colours_preference_round_trips() { assert_eq!(fresh.reading_colors, ReadingColors::default()); } +#[test] +fn reading_presets_follow_appearance_and_do_not_change_the_interface_theme() { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1000.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.set_option("dialog", "preferences").unwrap(); + app + }); + h.run_steps(6); + assert!(h.query_by_label("Original").is_some()); + let theme = h.state().theme_preference; + let appearance = h.get_by_label("Light theme").rect(); + let preset = h.get_by(|n| n.role() == Role::Button && n.label().as_deref() == Some("White on black")).rect(); + let documents = h.get_by_label("Documents and view").rect(); + assert!(appearance.bottom() < preset.top() && preset.bottom() < documents.top()); + for (label, expected) in [ + ("White on black", ReadingColors::WHITE_ON_BLACK), + ("Yellow on black", ReadingColors::YELLOW_ON_BLACK), + ("Green on black", ReadingColors::GREEN_ON_BLACK), + ("Black on white", ReadingColors::BLACK_ON_WHITE), + ("Off", ReadingColors::default()), + ] { + h.get_by(|n| n.role() == Role::Button && n.label().as_deref() == Some(label)).scroll_to_me(); + h.run_steps(4); + h.get_by(|n| n.role() == Role::Button && n.label().as_deref() == Some(label)).click(); + h.run_steps(4); + assert_eq!(h.state().reading_colors, expected); + assert_eq!(h.state().theme_preference, theme); + assert_eq!(h.query_by_label("Original").is_some(), expected.params.is_none()); + } + if let Ok(path) = std::env::var("PDFCRAFT_READING_SHOT") { + h.render().unwrap().save(path).unwrap(); + } +} + #[test] fn reading_colours_seed_on_open() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { diff --git a/crates/ui-egui/tests/it/theme_button.rs b/crates/ui-egui/tests/it/theme_button.rs index 758aa1a277..6568fca156 100644 --- a/crates/ui-egui/tests/it/theme_button.rs +++ b/crates/ui-egui/tests/it/theme_button.rs @@ -19,6 +19,20 @@ fn preference(h: &Harness<'static, PdfCraftApp>) -> String { format!("{:?}", h.state().theme_preference) } +#[test] +fn preferences_gear_is_right_of_theme_and_opens_preferences() { + let mut h = harness(); + let theme = h.get_by_label("Display theme: Use system setting").rect(); + let gear = h.get_by_label("Preferences").rect(); + assert!(gear.left() >= theme.right()); + h.get_by_label("Preferences").click(); + h.run_steps(6); + assert!(h.query_by_label("Appearance mode").is_some()); + assert!(h.query_by_label("Light theme").is_some()); + assert!(h.query_by_label("Dark theme").is_some()); + assert_eq!(preference(&h), "System"); +} + #[test] fn a_click_cycles_the_theme_and_a_right_click_opens_the_menu() { let mut h = harness(); diff --git a/crates/ui-egui/tests/it/ui.rs b/crates/ui-egui/tests/it/ui.rs index cd5ce8dbb1..69be82e7a7 100644 --- a/crates/ui-egui/tests/it/ui.rs +++ b/crates/ui-egui/tests/it/ui.rs @@ -706,11 +706,11 @@ fn narrow_tab_strips_accept_vertical_wheel_scrolling() { app }); h.run_steps(4); - let last = h.get_by_label("narrow-11.pdf").rect(); + let last = h.get_by(|n| n.role() == egui::accesskit::Role::Tab && n.label().as_deref() == Some("narrow-11.pdf")).rect(); assert!(last.center().x > 28.0 && last.right() < width - 100.0, "active tab: {last:?}"); h.state_mut().active = Some(0); h.run_steps(4); - let first = h.get_by_label("narrow-00.pdf").rect(); + let first = h.get_by(|n| n.role() == egui::accesskit::Role::Tab && n.label().as_deref() == Some("narrow-00.pdf")).rect(); assert!(first.center().x > 28.0 && first.right() < width - 100.0); h.event(egui::Event::PointerMoved(first.center())); for _ in 0..8 { @@ -723,9 +723,9 @@ fn narrow_tab_strips_accept_vertical_wheel_scrolling() { }); h.run_steps(8); } - let last = h.get_by_label("narrow-11.pdf").rect(); + let last = h.get_by(|n| n.role() == egui::accesskit::Role::Tab && n.label().as_deref() == Some("narrow-11.pdf")).rect(); assert!(last.center().x > 28.0 && last.right() < width - 100.0, "scrolled tab: {last:?}"); - h.get_by_label("narrow-11.pdf").click(); + h.get_by(|n| n.role() == egui::accesskit::Role::Tab && n.label().as_deref() == Some("narrow-11.pdf")).click(); h.run_steps(4); assert_eq!(h.state().active, Some(11)); assert!(h.get_by_label_contains("Display theme:").rect().right() <= width); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 47ac06fdd4..2a17a9f02e 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -9180,8 +9180,8 @@ milestone = "M12" status = "shipped" commands = [] tools = ["view_reading_colors"] -evidence = ["crates/automation/tests/it/automation.rs::reading_colors_through_tools", "crates/render/src/raster/tests.rs::reading_colors_map_solid_paints_and_leave_images", "crates/render/src/raster/tests.rs::reading_colors_tiles_match_full_render", "crates/render/src/raster/tests.rs::print_preview_requests_render_with_reading_colors_off", "crates/render/src/raster/tests.rs::reading_colors_keep_highlight_annotations_readable", "crates/ui-egui/tests/it/reading_colors.rs::reading_colours_preference_round_trips", "crates/ui-egui/tests/it/reading_colors.rs::reading_colours_seed_on_open", "crates/ui-egui/tests/it/reading_colors.rs::pending_pages_use_document_reading_background"] -notes = "View only, off by default: Preferences ▸ Reading colours holds the app-wide preference, seeded into every document as it opens (including created ones), and view_reading_colors reads and sets it per document (Acrobat's Replace Document Colors). Solid text paints and, by default, line-art paints (including stencil-mask image paints) are remapped by luminance — each lerps between the preset's text and background colour by how dark or light the original paint is — while images, shadings and tiling patterns pass through untouched. Annotations keep the colours they were given (both /AP appearances and the no-/AP Highlight fallback), so a Multiply highlight over a dark background shows the marked-up words in the highlight's colour instead of a flat bar that hides them (#1548). Presets: black on white, white on black, yellow on black, green on black, or a custom #RRGGBB pair; only_black limits the mapping to near-black paints (a 10% luminance threshold — our choice; Acrobat's was not observed). Display only: the document is not changed and print and export are unaffected; print previews and view snapshots render with the document's true colours (a preview matches the uncoloured printout, and a snapshot is an export surface), so reading colours apply to the reading view only. Pending canvas pages use the configured reading background before their raster arrives (#2059). Closes #897 (duplicate #488)." +evidence = ["crates/automation/tests/it/automation.rs::reading_colors_through_tools", "crates/render/src/raster/tests.rs::reading_colors_map_solid_paints_and_leave_images", "crates/render/src/raster/tests.rs::reading_colors_tiles_match_full_render", "crates/render/src/raster/tests.rs::print_preview_requests_render_with_reading_colors_off", "crates/render/src/raster/tests.rs::reading_colors_keep_highlight_annotations_readable", "crates/ui-egui/tests/it/reading_colors.rs::reading_colours_preference_round_trips", "crates/ui-egui/tests/it/reading_colors.rs::reading_colours_seed_on_open", "crates/ui-egui/tests/it/reading_colors.rs::pending_pages_use_document_reading_background", "crates/ui-egui/tests/it/reading_colors.rs::reading_presets_follow_appearance_and_do_not_change_the_interface_theme", "crates/ui-egui/tests/it/theme_button.rs::preferences_gear_is_right_of_theme_and_opens_preferences"] +notes = "View only, off by default: Preferences ▸ Reading colours, directly below Appearance, offers colour samples and a live preview; the gear beside the theme button opens Preferences. Reading colours holds the app-wide preference, seeded into every document as it opens (including created ones), and view_reading_colors reads and sets it per document (Acrobat's Replace Document Colors). Solid text paints and, by default, line-art paints (including stencil-mask image paints) are remapped by luminance — each lerps between the preset's text and background colour by how dark or light the original paint is — while images, shadings and tiling patterns pass through untouched. Annotations keep the colours they were given (both /AP appearances and the no-/AP Highlight fallback), so a Multiply highlight over a dark background shows the marked-up words in the highlight's colour instead of a flat bar that hides them (#1548). Presets: black on white, white on black, yellow on black, green on black, or a custom #RRGGBB pair; only_black limits the mapping to near-black paints (a 10% luminance threshold — our choice; Acrobat's was not observed). Display only: the document is not changed and print and export are unaffected; print previews and view snapshots render with the document's true colours (a preview matches the uncoloured printout, and a snapshot is an export surface), so reading colours apply to the reading view only. Pending canvas pages use the configured reading background before their raster arrives (#2059). Closes #897 (duplicate #488)." [[feature]] id = "a11y.reflow-tagged"