mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 21:17:48 +00:00
Merge pull request #7077 from Hassan1305/feat-6304-font-height-icons
Character panel: icons for the font height references (#6304)
This commit is contained in:
@@ -940,18 +940,92 @@ const FONT_HEIGHT_REFERENCES: [(vectorcraft_doc::FontHeightReference, &str); 4]
|
||||
(vectorcraft_doc::FontHeightReference::IcfBox, "icfBox"),
|
||||
];
|
||||
|
||||
/// The size of the icon before each name in the Set Font Height Reference dropdown.
|
||||
const FONT_HEIGHT_ICON: f32 = 16.0;
|
||||
|
||||
/// The Set Font Height Reference dropdown, `width` wide: the height of the font that the Font Size
|
||||
/// measures, blank where the characters the panel acts on differ.
|
||||
/// measures, blank where the characters the panel acts on differ. Each reference shows an icon of
|
||||
/// the height it measures before its name ([`paint_font_height_icon`], #6304).
|
||||
fn font_height_reference(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) {
|
||||
// The labels of [`FONT_HEIGHT_REFERENCES`], in its order.
|
||||
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().zip(labels).find(|(r, _)| Some(r.0) == current).map_or("", |(_, l)| l);
|
||||
if let Some((_, key)) = widgets::dropdown_names(ui, "ch-font-height", shown, &labels, width).and_then(|i| FONT_HEIGHT_REFERENCES.get(i)) {
|
||||
let shown = FONT_HEIGHT_REFERENCES.iter().position(|(r, _)| Some(*r) == current);
|
||||
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);
|
||||
}
|
||||
};
|
||||
let picked = widgets::dropdown_icon_names(ui, "ch-font-height", shown, &labels, width, FONT_HEIGHT_ICON, icon);
|
||||
if let Some((_, key)) = picked.and_then(|i| FONT_HEIGHT_REFERENCES.get(i)) {
|
||||
format(app, json!({"fontHeightReference": key}));
|
||||
}
|
||||
}
|
||||
|
||||
/// Paints the icon of font height reference `r` into the square `rect` (#6304): a letter drawn in
|
||||
/// `ink` on a baseline, and in `mark` the height the reference measures. Em Box frames the whole
|
||||
/// square around an "A"; Cap Height puts a bar from the baseline to the top of a capital "A"
|
||||
/// beside it; x-Height a bar from the baseline to the top of a lowercase "x"; ICF Box frames a
|
||||
/// square inset in the faint em square around an ideograph-like cross, as the ideographic
|
||||
/// character face sits inside the em box. The drawings are VectorCraft's own.
|
||||
fn paint_font_height_icon(
|
||||
painter: &egui::Painter,
|
||||
rect: egui::Rect,
|
||||
r: vectorcraft_doc::FontHeightReference,
|
||||
ink: egui::Color32,
|
||||
mark: egui::Color32,
|
||||
) {
|
||||
use vectorcraft_doc::FontHeightReference as R;
|
||||
// A point at (x, y) in the unit square mapped onto `rect`.
|
||||
let p = |x: f32, y: f32| rect.lerp_inside(vec2(x, y));
|
||||
let glyph = egui::Stroke::new(1.3, ink);
|
||||
let measure = egui::Stroke::new(1.2, mark);
|
||||
let line = |a: (f32, f32), b: (f32, f32), stroke: egui::Stroke| painter.line_segment([p(a.0, a.1), p(b.0, b.1)], stroke);
|
||||
let frame = |min: f32, max: f32, stroke: egui::Stroke| {
|
||||
painter.rect_stroke(egui::Rect::from_min_max(p(min, min), p(max, max)), 0.0, stroke, egui::StrokeKind::Middle);
|
||||
};
|
||||
// A capital A from `left` to `right`, its apex at `top` and its feet on `base`.
|
||||
let capital = |left: f32, right: f32, top: f32, base: f32| {
|
||||
let mid = (left + right) / 2.0;
|
||||
painter.add(egui::Shape::line(vec![p(left, base), p(mid, top), p(right, base)], glyph));
|
||||
let bar = top + (base - top) * 0.62;
|
||||
let inset = (right - left) * 0.5 * 0.62;
|
||||
line((mid - inset, bar), (mid + inset, bar), glyph);
|
||||
};
|
||||
// A vertical measure from `top` to `base` at `x`, with ticks at its ends.
|
||||
let height_bar = |x: f32, top: f32, base: f32| {
|
||||
line((x, top), (x, base), measure);
|
||||
line((x - 0.1, top), (x + 0.1, top), measure);
|
||||
line((x - 0.1, base), (x + 0.1, base), measure);
|
||||
};
|
||||
let (base, cap_top, x_top) = (0.86, 0.16, 0.46);
|
||||
match r {
|
||||
R::EmBox => {
|
||||
frame(0.06, 0.94, measure);
|
||||
capital(0.26, 0.74, 0.26, 0.76);
|
||||
}
|
||||
R::CapHeight => {
|
||||
capital(0.06, 0.6, cap_top, base);
|
||||
height_bar(0.82, cap_top, base);
|
||||
}
|
||||
R::XHeight => {
|
||||
line((0.1, x_top), (0.54, base), glyph);
|
||||
line((0.54, x_top), (0.1, base), glyph);
|
||||
height_bar(0.82, x_top, base);
|
||||
}
|
||||
R::IcfBox => {
|
||||
frame(0.06, 0.94, egui::Stroke::new(1.0, ink.gamma_multiply(0.4)));
|
||||
frame(0.2, 0.8, measure);
|
||||
line((0.33, 0.5), (0.67, 0.5), glyph);
|
||||
line((0.5, 0.31), (0.5, 0.69), glyph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The font family menu and the font style dropdown of style `s` (Character and Properties panels,
|
||||
/// Control bar), with their `ids` and `widths`. The mouse wheel over the closed family field steps
|
||||
/// the text through the families.
|
||||
|
||||
@@ -822,6 +822,135 @@ fn show_font_height_options_shows_the_font_height_reference() {
|
||||
assert!(item(&texts(&ctx, &mut app, show), "Cap Height").is_none());
|
||||
}
|
||||
|
||||
/// The Set Font Height Reference dropdown shows an icon before each reference's name, in the
|
||||
/// closed field and in the open list, and each reference's icon is its own (#6304).
|
||||
#[test]
|
||||
fn font_height_references_show_their_icons() {
|
||||
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);
|
||||
// Every text drawn with its rect, and the rects of every other shape drawn.
|
||||
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)>, marks: &mut Vec<egui::Rect>) {
|
||||
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, marks)),
|
||||
egui::Shape::Noop => {}
|
||||
s => marks.push(s.visual_bounding_rect()),
|
||||
}
|
||||
}
|
||||
let (mut texts, mut marks) = (vec![], vec![]);
|
||||
for c in &out.shapes {
|
||||
collect(&c.shape, &mut texts, &mut marks);
|
||||
}
|
||||
(texts, marks)
|
||||
};
|
||||
// The shapes in the icon's place before the text `rect`, relative to that place and rounded.
|
||||
let icon = |marks: &[egui::Rect], rect: egui::Rect| {
|
||||
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),
|
||||
);
|
||||
let mut found: Vec<[i32; 4]> = marks
|
||||
.iter()
|
||||
.filter(|m| m.is_finite() && place.contains_rect(**m))
|
||||
.map(|m| [m.min.x - place.min.x, m.min.y - place.min.y, m.max.x - place.min.x, m.max.y - place.min.y].map(|v| v.round() as i32))
|
||||
.collect();
|
||||
found.sort_unstable();
|
||||
found
|
||||
};
|
||||
frame(&ctx, &mut app);
|
||||
let (texts, marks) = frame(&ctx, &mut app);
|
||||
let field = texts.iter().find(|(t, _)| t == "Em Box").map(|(_, r)| *r).expect("the closed field shows Em Box");
|
||||
assert!(icon(&marks, field).len() >= 2, "no icon before Em Box in the closed field: {:?}", icon(&marks, field));
|
||||
click(&ctx, &mut app, field.center(), show);
|
||||
frame(&ctx, &mut app);
|
||||
let (texts, marks) = frame(&ctx, &mut app);
|
||||
let mut icons = vec![];
|
||||
for name in ["Em Box", "Cap Height", "x-Height", "ICF Box"] {
|
||||
// The list's row is the last text of that name drawn (the closed field is drawn first).
|
||||
let row = texts.iter().rev().find(|(t, _)| t == name).map(|(_, r)| *r).unwrap_or_else(|| panic!("the open list shows {name}: {texts:?}"));
|
||||
let found = icon(&marks, row);
|
||||
assert!(found.len() >= 2, "no icon before {name} in the open list: {found:?}");
|
||||
icons.push(found);
|
||||
}
|
||||
for (i, a) in icons.iter().enumerate() {
|
||||
assert!(icons.iter().skip(i + 1).all(|b| a != b), "two references share an icon: {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<egui::Color32> {
|
||||
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]
|
||||
|
||||
@@ -47,8 +47,8 @@ pub use drag::{
|
||||
use dropdowns::combo_button_id;
|
||||
pub(crate) use dropdowns::{combo, combo_menu, combo_menu_field, combo_named, field_focused_in, popover_is_open};
|
||||
pub use dropdowns::{
|
||||
dropdown, dropdown_named, dropdown_names, dropdown_names_named, dropdown_with, dropdown_with_named, list_scroll_style, menu_room, menu_scroll,
|
||||
panel_scroll, popover, set_menu_floor, strip_scroll,
|
||||
dropdown, dropdown_icon_names, dropdown_named, dropdown_names, dropdown_names_named, dropdown_with, dropdown_with_named, list_scroll_style,
|
||||
menu_room, menu_scroll, panel_scroll, popover, set_menu_floor, strip_scroll,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use dropdowns::{menu_floor, menu_room_either};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use egui::IntoAtoms as _;
|
||||
|
||||
use super::drag::PanelDrag;
|
||||
use super::labels::{PreviousLabel, label_field, name_field, take_previous_label};
|
||||
use super::text::take_armed_focus;
|
||||
@@ -33,6 +35,42 @@ 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, 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,
|
||||
current: Option<usize>,
|
||||
options: &[&str],
|
||||
width: f32,
|
||||
icon_size: f32,
|
||||
icon: impl Fn(&Ui, Rect, usize, egui::Color32, bool),
|
||||
) -> Option<usize> {
|
||||
let icon = &icon;
|
||||
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, 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() {
|
||||
let on = current == Some(i);
|
||||
if ui.add(egui::Button::selectable(on, (atom(i, on), *o))).clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
}
|
||||
chosen
|
||||
})
|
||||
}
|
||||
|
||||
/// [`dropdown`] whose options can be disabled (`enabled(index)`: greyed and not choosable).
|
||||
pub fn dropdown_with(
|
||||
ui: &mut Ui,
|
||||
@@ -88,6 +126,18 @@ pub(crate) fn combo_named<R>(
|
||||
current: &str,
|
||||
width: f32,
|
||||
list: impl FnOnce(&mut Ui) -> Option<R>,
|
||||
) -> Option<R> {
|
||||
combo_atoms_named(ui, id, name, egui::RichText::new(current).size(12.0), width, list)
|
||||
}
|
||||
|
||||
/// [`combo_named`] whose closed field shows the atoms `current` (an icon and a name, say).
|
||||
fn combo_atoms_named<'a, R>(
|
||||
ui: &mut Ui,
|
||||
id: impl std::hash::Hash,
|
||||
name: Option<&str>,
|
||||
current: impl egui::IntoAtoms<'a>,
|
||||
width: f32,
|
||||
list: impl FnOnce(&mut Ui) -> Option<R>,
|
||||
) -> Option<R> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let label = take_previous_label(ui);
|
||||
@@ -99,7 +149,7 @@ pub(crate) fn combo_named<R>(
|
||||
let button_id = combo_button_id(ui, &id);
|
||||
let enter_for_combo = ui.memory(|m| m.has_focus(button_id)) || craft_ui::ComboBox::is_open(ui.ctx(), button_id);
|
||||
let response = craft_ui::ComboBox::from_id_salt(HashOnly(&id))
|
||||
.selected_text(egui::RichText::new(current).size(12.0))
|
||||
.selected_text(current)
|
||||
.width(width - 4.0)
|
||||
.height(ui.spacing().combo_height)
|
||||
.show_ui(ui, list);
|
||||
|
||||
Reference in New Issue
Block a user