diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index d1712c42d7..b761e6fd66 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -951,8 +951,11 @@ fn font_height_reference(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) { let labels = [tl!("Em Box"), tl!("Cap Height"), tl!("x-Height"), tl!("ICF Box")]; let current = shared(app, |s| s.font_height_reference); let shown = FONT_HEIGHT_REFERENCES.iter().position(|(r, _)| Some(*r) == current); - let mark = Tokens::get(ui.ctx()).accent; - let icon = |ui: &Ui, rect: egui::Rect, i: usize, ink: egui::Color32| { + let accent = Tokens::get(ui.ctx()).accent; + let icon = |ui: &Ui, rect: egui::Rect, i: usize, ink: egui::Color32, highlighted: bool| { + // On the highlighted row the selection fill is accent-tinted, so the measure takes the + // name's colour there to keep its contrast. + let mark = if highlighted { ink } else { accent }; if let Some((r, _)) = FONT_HEIGHT_REFERENCES.get(i) { paint_font_height_icon(ui.painter(), rect, *r, ink, mark); } diff --git a/crates/ui-egui/src/panels/character/tests.rs b/crates/ui-egui/src/panels/character/tests.rs index 4e403f0903..dd4b12adf5 100644 --- a/crates/ui-egui/src/panels/character/tests.rs +++ b/crates/ui-egui/src/panels/character/tests.rs @@ -886,6 +886,71 @@ fn font_height_references_show_their_icons() { } } +/// In the open Set Font Height Reference list, the current reference's row is drawn on the +/// accent-tinted selection fill, so its icon draws the measure in the name's colour rather than +/// the accent, which would lose its contrast there; the other rows keep the accent (#6304). +#[test] +fn the_highlighted_font_height_icon_keeps_its_contrast() { + use super::super::tests_appearance::click; + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "Hx", "size": 12})).unwrap()["id"].clone(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + set_pstate(&ctx, FONT_HEIGHT_OPTIONS, true); + let accent = Tokens::get(&ctx).accent; + // Every text drawn with its rect, and every stroke drawn with its rect and colour. + let frame = |ctx: &egui::Context, app: &mut VectorcraftApp| { + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui)); + out.textures_delta.clear(); + fn collect(s: &egui::Shape, texts: &mut Vec<(String, egui::Rect)>, strokes: &mut Vec<(egui::Rect, egui::Color32)>) { + match s { + egui::Shape::Text(t) => texts.push((t.galley.text().to_string(), t.visual_bounding_rect())), + egui::Shape::Vec(v) => v.iter().for_each(|s| collect(s, texts, strokes)), + egui::Shape::LineSegment { stroke, .. } => strokes.push((s.visual_bounding_rect(), stroke.color)), + egui::Shape::Path(p) => { + if let egui::epaint::ColorMode::Solid(c) = p.stroke.color { + strokes.push((s.visual_bounding_rect(), c)); + } + } + egui::Shape::Rect(r) => strokes.push((s.visual_bounding_rect(), r.stroke.color)), + _ => {} + } + } + let (mut texts, mut strokes) = (vec![], vec![]); + for c in &out.shapes { + collect(&c.shape, &mut texts, &mut strokes); + } + (texts, strokes) + }; + // The stroke colours in the icon's place before the text `rect`. + let colours = |strokes: &[(egui::Rect, egui::Color32)], rect: egui::Rect| -> Vec { + let place = egui::Rect::from_min_max( + egui::pos2(rect.min.x - FONT_HEIGHT_ICON - 8.0, rect.center().y - 10.0), + egui::pos2(rect.min.x, rect.center().y + 10.0), + ); + strokes.iter().filter(|(r, _)| r.is_finite() && place.contains_rect(*r)).map(|(_, c)| *c).collect() + }; + frame(&ctx, &mut app); + let (texts, _) = frame(&ctx, &mut app); + let field = texts.iter().find(|(t, _)| t == "Em Box").map(|(_, r)| *r).expect("the closed field shows Em Box"); + click(&ctx, &mut app, field.center(), show); + frame(&ctx, &mut app); + let (texts, strokes) = frame(&ctx, &mut app); + let row = |name: &str| texts.iter().rev().find(|(t, _)| t == name).map(|(_, r)| *r).unwrap_or_else(|| panic!("the open list shows {name}")); + // The list fades in, so colours are compared without their alpha. + let rgb = |c: egui::Color32| { + let [r, g, b, _] = c.to_srgba_unmultiplied(); + [r, g, b] + }; + let is_accent = |c: &egui::Color32| rgb(*c).iter().zip(rgb(accent)).all(|(a, b)| a.abs_diff(b) <= 4); + let current = colours(&strokes, row("Em Box")); + assert!(!current.is_empty() && !current.iter().any(is_accent), "the highlighted Em Box icon draws in the accent: {current:?}"); + let other = colours(&strokes, row("Cap Height")); + assert!(other.iter().any(is_accent), "the other rows keep the accent measure: {other:?}"); +} + /// Tate-chu-yoko is in the panel menu with the East Asian options, and choosing it marks the /// selected characters. #[test] diff --git a/crates/ui-egui/src/widgets/dropdowns.rs b/crates/ui-egui/src/widgets/dropdowns.rs index c8fba54aed..a2741ff1cb 100644 --- a/crates/ui-egui/src/widgets/dropdowns.rs +++ b/crates/ui-egui/src/widgets/dropdowns.rs @@ -37,9 +37,11 @@ pub fn dropdown_names_named(ui: &mut Ui, id: impl std::hash::Hash, name: Option< /// [`dropdown_names`] whose options each show an icon before their name, in the open list and in /// the closed field, so that a name that says little on its own is shown with a picture of what it -/// means (#6304). `icon(ui, rect, index, ink)` paints option `index`'s icon into `rect`, an -/// `icon_size` square, with `ink`, the colour the row's name is drawn in. `current` is the option -/// shown; `None` shows a blank field (characters that differ). +/// means (#6304). `icon(ui, rect, index, ink, highlighted)` paints option `index`'s icon into +/// `rect`, an `icon_size` square, with `ink`, the colour the row's name is drawn in; +/// `highlighted` is true on the open list's row of the current option, drawn on the selection +/// fill, where an accent-coloured mark would lose its contrast. `current` is the option shown; +/// `None` shows a blank field (characters that differ). pub fn dropdown_icon_names( ui: &mut Ui, id: impl std::hash::Hash, @@ -47,18 +49,21 @@ pub fn dropdown_icon_names( options: &[&str], width: f32, icon_size: f32, - icon: impl Fn(&Ui, Rect, usize, egui::Color32), + icon: impl Fn(&Ui, Rect, usize, egui::Color32, bool), ) -> Option { let icon = &icon; - let atom = move |i: usize| egui::Atom::paint(Vec2::splat(icon_size), move |ui, a| icon(ui, a.rect, i, a.fallback_text_color)); + let atom = move |i: usize, highlighted: bool| { + egui::Atom::paint(Vec2::splat(icon_size), move |ui, a| icon(ui, a.rect, i, a.fallback_text_color, highlighted)) + }; let shown = match current.and_then(|i| options.get(i).map(|name| (i, *name))) { - Some((i, name)) => (atom(i), egui::RichText::new(name).size(12.0)).into_atoms(), + Some((i, name)) => (atom(i, false), egui::RichText::new(name).size(12.0)).into_atoms(), None => egui::RichText::new("").size(12.0).into_atoms(), }; combo_atoms_named(ui, id, None, shown, width, |ui| { let mut chosen = None; for (i, o) in options.iter().enumerate() { - if ui.add(egui::Button::selectable(current == Some(i), (atom(i), *o))).clicked() { + let on = current == Some(i); + if ui.add(egui::Button::selectable(on, (atom(i, on), *o))).clicked() { chosen = Some(i); } }