diff --git a/crates/ui-egui/src/previews.rs b/crates/ui-egui/src/previews.rs index 1fc566e..5c1545b 100644 --- a/crates/ui-egui/src/previews.rs +++ b/crates/ui-egui/src/previews.rs @@ -256,7 +256,18 @@ pub fn ink_mask(img: &mut egui::ColorImage) { /// Lay out `para` (with `base`'s styles) in a `w`×`h` pt box and rasterise it. pub fn snippet(base: &Document, para: Paragraph, w: f32, h: f32, ppp: f32, margin: f32) -> Option { - snippet_blocks(base, vec![Block::Para(para)], w, h, ppp, margin, None) + snippet_blocks(base, vec![Block::Para(para)], w, h, ppp, margin, None, false) +} + +/// [`snippet`] on the dark page of View › Switch Modes when `dark` (the Styles gallery, #663). +pub fn style_snippet(base: &Document, para: Paragraph, w: f32, h: f32, ppp: f32, margin: f32, dark: bool) -> Option { + snippet_blocks(base, vec![Block::Para(para)], w, h, ppp, margin, None, dark) +} + +/// How a gallery draws a page preview: as rendered on a light interface or on Switch Modes' dark +/// page, and dimmed in the dark theme so a white page isn't the brightest thing on screen (#663). +pub fn preview_tint(t: &Tokens, dark_page: bool) -> egui::Color32 { + if t.dark && !dark_page { egui::Color32::from_gray(0xC8) } else { egui::Color32::WHITE } } fn snippet_blocks( @@ -267,6 +278,7 @@ fn snippet_blocks( ppp: f32, margin: f32, paper: Option, + dark: bool, ) -> Option { let mut d = Document::new(); d.styles = base.styles.clone(); @@ -284,6 +296,7 @@ fn snippet_blocks( if let Some(p) = paper { opts.paper = p; } + opts.dark = dark; let img = wordcraft_render::render_area(&d, page, 0.0, 0.0, w + 2.0 * margin, h + margin, ppp, &opts); let px = img.to_straight(); Some(egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &px)) @@ -294,7 +307,7 @@ fn snippet_blocks( pub fn style_preview(app: &mut WordApp, ui: &mut Ui, id: &str, ty: &str, size: egui::Vec2) { let t = Tokens::get(ui.ctx()); let (r, _) = ui.allocate_exact_size(size, Sense::hover()); - ui.painter().rect(r, 3.0, egui::Color32::WHITE, Stroke::new(1.0, t.border), egui::StrokeKind::Inside); + ui.painter().rect(r, 3.0, preview_tint(&t, false), Stroke::new(1.0, t.border), egui::StrokeKind::Inside); let rev = app.session.rev(); let skey = app.previews.styles_key(&app.session.doc, rev); let ppp = ui.ctx().pixels_per_point(); @@ -328,13 +341,13 @@ pub fn style_preview(app: &mut WordApp, ui: &mut Ui, id: &str, ty: &str, size: e Block::Para(p) } }; - snippet_blocks(doc, vec![block], w, h, ppp, 4.0, None) + snippet_blocks(doc, vec![block], w, h, ppp, 4.0, None, false) }); if let Some(hd) = tex { let sz = hd.size_vec2() / ppp; let ir = Rect::from_min_size(pos2(r.min.x + 2.0, r.min.y + 2.0), sz.min(r.size() - vec2(4.0, 4.0))); let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2((ir.width() / sz.x).min(1.0), (ir.height() / sz.y).min(1.0))); - ui.painter().with_clip_rect(r).image(hd.id(), ir, uv, egui::Color32::WHITE); + ui.painter().with_clip_rect(r).image(hd.id(), ir, uv, preview_tint(&t, false)); } } @@ -360,7 +373,8 @@ pub fn style_gallery(app: &mut WordApp, ui: &mut Ui, state: &Value) { ui.painter().rect_filled(r, 3.0, t.hover); } let lang = crate::i18n::current().code(); - let key = format!("style:{id}:{skey}:{ppp}:{lang}"); + let dark_page = app.session.view.dark_mode; + let key = format!("style:{id}:{skey}:{ppp}:{lang}:{dark_page}"); let doc = &app.session.doc; let tex = app.previews.get_or(ui.ctx(), &key, || { // Sample letters in the interface's script (`АаБбВвГг` in Russian). @@ -372,13 +386,13 @@ pub fn style_gallery(app: &mut WordApp, ui: &mut Ui, state: &Value) { p.props.indent_left = Some(if first.is_finite() { (-first).max(0.0) } else { 0.0 }); p.props.borders = None; p.props.shading = None; - snippet(doc, p, 220.0, 30.0, ppp, 1.0) + style_snippet(doc, p, 220.0, 30.0, ppp, 1.0, dark_page) }); if let Some(h) = tex { let sz = h.size_vec2() / ppp; let ir = Rect::from_min_size(pos2(r.min.x + 3.0, r.min.y + 4.0), sz.min(vec2(68.0, 32.0))); let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2((ir.width() / sz.x).min(1.0), (ir.height() / sz.y).min(1.0))); - ui.painter().with_clip_rect(r).image(h.id(), ir, uv, egui::Color32::WHITE); + ui.painter().with_clip_rect(r).image(h.id(), ir, uv, preview_tint(&t, dark_page)); } let mut job = egui::text::LayoutJob::single_section(name.clone(), egui::TextFormat::simple(regular(10.5), t.text)); job.wrap = egui::text::TextWrapping::truncate_at_width(r.width() - 6.0); @@ -446,14 +460,14 @@ pub fn style_set_gallery(app: &mut WordApp, ui: &mut Ui) { Block::Para(Paragraph::with_text(tl!("Heading 1"), CharProps::default()).styled("Heading1")), Block::Para(Paragraph::with_text(tl!("Body text in a short paragraph to show spacing."), CharProps::default())), ]; - snippet_blocks(&s.doc, blocks, 120.0, 110.0, ppp * 0.42, 6.0, None) + snippet_blocks(&s.doc, blocks, 120.0, 110.0, ppp * 0.42, 6.0, None, false) }); if let Some(h) = tex { let sz = h.size_vec2() / ppp; let ir = Rect::from_min_size(pos2(r.min.x + 3.0, r.min.y + 3.0), sz.min(vec2(50.0, 60.0))); ui.painter().rect_stroke(ir, 0.0, Stroke::new(1.0, t.border), egui::StrokeKind::Outside); let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2((ir.width() / sz.x).min(1.0), (ir.height() / sz.y).min(1.0))); - ui.painter().image(h.id(), ir, uv, egui::Color32::WHITE); + ui.painter().image(h.id(), ir, uv, preview_tint(&t, false)); } if resp.on_hover_text(format!("Style set: {name}")).clicked() { let _ = app.run("design.styleSet", json!({"name": name})); @@ -481,13 +495,13 @@ pub fn table_style_tile(ui: &mut Ui, app: &mut WordApp, style: &str) -> Response c.blocks = vec![para_block(Paragraph::with_text(" ", CharProps { size: Some(5.0), ..Default::default() }))]; } } - snippet_blocks(doc, vec![Block::Table(tb), Block::Para(Paragraph::new())], 104.0, 70.0, ppp * 0.48, 4.0, None) + snippet_blocks(doc, vec![Block::Table(tb), Block::Para(Paragraph::new())], 104.0, 70.0, ppp * 0.48, 4.0, None, false) }); if let Some(h) = tex { let sz = h.size_vec2() / ppp; let ir = Rect::from_center_size(r.center(), sz.min(vec2(54.0, 42.0))); let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2((ir.width() / sz.x).min(1.0), (ir.height() / sz.y).min(1.0))); - ui.painter().image(h.id(), ir, uv, egui::Color32::WHITE); + ui.painter().image(h.id(), ir, uv, preview_tint(&t, false)); } resp } @@ -496,6 +510,17 @@ pub fn table_style_tile(ui: &mut Ui, app: &mut WordApp, style: &str) -> Response mod tests { use super::*; + /// #663: gallery previews are dimmed in the dark theme, unless they show Switch Modes' dark + /// page; the light theme keeps a white page. + #[test] + fn previews_are_dimmed_in_the_dark_theme() { + let (light, dark) = (crate::theme::Tokens::light(), crate::theme::Tokens::dark()); + assert_eq!(super::preview_tint(&light, false), egui::Color32::WHITE); + assert_eq!(super::preview_tint(&dark, true), egui::Color32::WHITE, "the dark page is already dark"); + let dim = super::preview_tint(&dark, false); + assert!(dim.r() < 0xE0 && dim.r() > 0xA0, "{dim:?}"); + } + #[test] fn font_menu_previews_render_defer_and_reuse_without_deadlocking() { // A nested Context lock used to hang the first uncached menu entry. Bound the wait so diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 1657246..49b6ffd 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (hard-coded interface colours moved to the theme, #656; previously minor: status bar language follows the caret and sets the proofing language; Review › Language is a menu; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** trivial (gallery previews dimmed in the dark theme, #663; previously trivial: hard-coded interface colours moved to the theme, #656; previously minor: status bar language follows the caret and sets the proofing language; Review › Language is a menu; previously trivial: Customize Ribbon and the Quick Access Toolbar, #379; previously: major, first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch. Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our @@ -69,6 +69,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | 2026-10-11 | trivial | Text field and combo outlines reach 3:1 against their fill and surroundings in both themes (light #8A8A8A, dark #7A7A7A); a test pins the ratios (#657) | | 2026-10-11 | trivial | Font dialog: Asian text font and Latin text font boxes, each with its own list and type-to-find, shown for a Chinese/Japanese interface or East Asian text in the selection; OK sets only the fonts changed; `format.font {"eastAsia"}` (#505) | | 2026-10-11 | trivial | References › Table of Authorities: Insert Table of Authorities is labelled and joined by Update Table (`references.updateAuthorities`), as in the Index group (#661) | +| 2026-10-11 | trivial | Dark theme: Styles, style-set and table-style previews sit on a dimmer page tone; with View › Switch Modes on, the Styles gallery previews the dark page (#663) | | 2026-10-11 | trivial | Dialog primary buttons, the Discord pill, the table eraser line and the Navigation search highlight take their colours from the theme in both themes (`widgets::primary_button`); a test bans the old fixed values (#656) | | 2026-10-11 | trivial | Dark theme: Share, the avatar initials, the AutoSave knob and keytips use dark `on_accent` text on the light accent (6.2:1, 8.4:1 on hover; white was 2.9:1) (#655) | | 2026-10-11 | trivial | Text field and combo outlines reach 3:1 against their fill and surroundings in both themes (light #8A8A8A, dark #7A7A7A); a test pins the ratios (#657) |