mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
Fix #458: Font Size and Leading combo boxes in the Character and Properties panels and the Control bar
The Properties panel's Font Size and Leading were plain typed fields and the
Control bar had no type controls. All three places now use the shared
editable combo (`widgets::spin_field`: typing, arrow and spinner stepping,
label scrubbing, a preset dropdown in the type unit):
- Character panel: the size and leading grid is shared with the Properties
panel (`character::metrics_grid`), which now shows the font style, Font
Size, Leading, Kerning and Tracking as combos and applies them to the
Type tool's selected characters as the Character panel does.
- Control bar: with type selected or edited, a Character link (the Character
panel in a popover), the font family and style menus and the Font Size combo.
- Presets as Illustrator lists them: sizes 6-72 pt; Leading has Auto first
(`widgets::spin_field_auto`, `text.setStyle {leading: "auto"}`) and then
the size ladder instead of 120% values; Kerning and Tracking -100...200.
- Font Size, Leading, Kerning and Tracking show blank where the selected
text's values differ (`character::shared`); the spinner does nothing on a
blank field instead of jumping to the step size.
Each pick is one `text.setStyle` / `text.setRangeStyle` step, so MCP and the
control channel are unchanged. Translations for the two new strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fa405da794
commit
0c4df274aa
@@ -180,6 +180,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
egui::Popup::menu(&resp).show(|ui| crate::panels::graphic_styles::picker(app, ui));
|
||||
}
|
||||
ui.separator();
|
||||
crate::panels::character::control_bar(app, ui);
|
||||
if sel.is_empty() {
|
||||
if widgets::flat_button(ui, tl!("Document Setup"), 112.0).clicked() {
|
||||
app.run("file.documentSetup", json!({})).ok();
|
||||
|
||||
@@ -2530,6 +2530,8 @@
|
||||
Stroke (X), double-click for the Color Picker Trazo (X), doble clic para el selector de color
|
||||
Stroke Color Color de trazo
|
||||
Stroke Options Opciones de trazo
|
||||
Character: Carácter:
|
||||
Character options Opciones de carácter
|
||||
Stroke options Opciones de trazo
|
||||
Stroke Weight Grosor de trazo
|
||||
Stroke: Trazo:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2334,6 +2334,8 @@
|
||||
Stop editing the opacity mask 不透明マスクの編集を終了
|
||||
Strikethrough 打ち消し線
|
||||
Stroke Options 線オプション
|
||||
Character: 文字:
|
||||
Character options 文字オプション
|
||||
Stroke options 線オプション
|
||||
Stroke: 線:
|
||||
Style スタイル
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2240,6 +2240,8 @@
|
||||
Stroke Color Cor do Traçado
|
||||
Stroke Options Opções de Traçado
|
||||
Stroke Weight Peso do Traçado
|
||||
Character: Caractere:
|
||||
Character options Opções de caractere
|
||||
Stroke options Opções de traçado
|
||||
Stroke: Traçado:
|
||||
Style Estilo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2466,6 +2466,8 @@
|
||||
Stroke (X), double-click for the Color Picker 描边 (X),双击打开拾色器
|
||||
Stroke Color 描边颜色
|
||||
Stroke Options 描边选项
|
||||
Character: 字符:
|
||||
Character options 字符选项
|
||||
Stroke options 描边选项
|
||||
Stroke Weight 描边粗细
|
||||
Stroke: 描边:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2548,6 +2548,8 @@
|
||||
Stroke (X), double-click for the Color Picker 筆畫 (X),按兩下開啟檢色器
|
||||
Stroke Color 筆畫顏色
|
||||
Stroke Options 筆畫選項
|
||||
Character: 字元:
|
||||
Character options 字元選項
|
||||
Stroke options 筆畫選項
|
||||
Stroke Weight 筆畫寬度
|
||||
Stroke: 筆畫:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -11,10 +11,13 @@ use vectorcraft_tools::{Mods, ToolKey};
|
||||
use super::{first_selected, pstate, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{self, menu_item};
|
||||
use crate::widgets::{self, SpinPick, menu_item};
|
||||
|
||||
pub const SIZE_PRESETS: [f64; 16] = [6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 18.0, 21.0, 24.0, 36.0, 48.0, 60.0, 72.0, 96.0];
|
||||
pub const TRACKING_PRESETS: [f64; 13] = [-100.0, -75.0, -50.0, -25.0, -10.0, -5.0, 0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0];
|
||||
/// The Font Size and Leading dropdowns' presets in points (Leading's after its Auto entry), shown
|
||||
/// in the type unit.
|
||||
pub const SIZE_PRESETS: [f64; 15] = [6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 18.0, 21.0, 24.0, 36.0, 48.0, 60.0, 72.0];
|
||||
/// The Kerning and Tracking dropdowns' presets in 1/1000 em.
|
||||
pub const TRACKING_PRESETS: [f64; 14] = [-100.0, -75.0, -50.0, -25.0, -10.0, -5.0, 0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 200.0];
|
||||
pub const SCALE_PRESETS: [f64; 8] = [25.0, 50.0, 75.0, 90.0, 100.0, 110.0, 125.0, 150.0];
|
||||
|
||||
/// The character style shown by the panel: the selected range's (or the caret's) while the Type
|
||||
@@ -32,6 +35,64 @@ pub(crate) fn text_style(app: &VectorcraftApp) -> Option<(CharStyle, vectorcraft
|
||||
}
|
||||
}
|
||||
|
||||
/// `f` of the character styles the panels act on: the runs the Type tool's selection covers (the
|
||||
/// caret's style at a caret), else every run of the selected type, groups' included. `None` (a
|
||||
/// blank field) where they differ, or with no type.
|
||||
pub(crate) fn shared<T: PartialEq>(app: &VectorcraftApp, f: impl Fn(&CharStyle) -> T) -> Option<T> {
|
||||
let st = app.session.active()?;
|
||||
let text = |id: NodeId| match st.doc.node(id).map(|n| &n.kind) {
|
||||
Some(NodeKind::Text(t)) => Some(t),
|
||||
_ => None,
|
||||
};
|
||||
let mut acc = Shared::None;
|
||||
if let Some((id, a, b)) = text_editing(app) {
|
||||
let t = text(id)?;
|
||||
if b <= a {
|
||||
return Some(f(&vectorcraft_text::edit::insertion_style(&t.runs, a, b)));
|
||||
}
|
||||
let mut end = 0;
|
||||
for r in &t.runs {
|
||||
let start = end;
|
||||
end += r.text.len();
|
||||
if start < b && end > a {
|
||||
acc.add(f(&r.style));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for n in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
|
||||
n.walk(&mut |c| {
|
||||
if let NodeKind::Text(t) = &c.kind {
|
||||
t.runs.iter().for_each(|r| acc.add(f(&r.style)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
acc.value()
|
||||
}
|
||||
|
||||
/// The value a run of styles share so far ([`shared`]).
|
||||
enum Shared<T> {
|
||||
None,
|
||||
Same(T),
|
||||
Differ,
|
||||
}
|
||||
|
||||
impl<T: PartialEq> Shared<T> {
|
||||
fn add(&mut self, v: T) {
|
||||
match self {
|
||||
Shared::None => *self = Shared::Same(v),
|
||||
Shared::Same(x) if *x != v => *self = Shared::Differ,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn value(self) -> Option<T> {
|
||||
match self {
|
||||
Shared::Same(v) => Some(v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn style(app: &mut VectorcraftApp, p: Value) {
|
||||
char_cmd(app, "text.setStyle", p);
|
||||
}
|
||||
@@ -234,74 +295,115 @@ fn cell(ui: &mut Ui, label: &str, tip: &str, add: impl FnOnce(&mut Ui)) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The Character panel's grid of numeric fields, `w` wide: Font Size, Leading, Kerning and
|
||||
/// Tracking, then with `more` the scales, Baseline Shift and Character Rotation (`s`: the style
|
||||
/// shown). The Properties panel shows its first rows.
|
||||
pub(crate) fn metrics_grid(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, s: &CharStyle, w: f32, more: bool) {
|
||||
let fw = ((w - 66.0) / 2.0).clamp(60.0, 100.0);
|
||||
let unit = app.session.type_unit();
|
||||
egui::Grid::new(id).num_columns(2).spacing([6.0, 4.0]).show(ui, |ui| {
|
||||
cell(ui, "T", tl!("Font Size"), |ui| size_field(app, ui, "ch-size", fw));
|
||||
cell(ui, "A↕", tl!("Leading"), |ui| {
|
||||
let leading = shared(app, CharStyle::effective_leading);
|
||||
let auto = shared(app, |s| s.leading.is_none()) == Some(true);
|
||||
match widgets::spin_field_auto(ui, "ch-lead", leading, Some(auto), unit, fw, 1.0, 0.1, &SIZE_PRESETS) {
|
||||
Some(SpinPick::Value(v)) => style(app, json!({"leading": v})),
|
||||
Some(SpinPick::Auto) => style(app, json!({"leading": "auto"})),
|
||||
None => {}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
cell(ui, "VA", tl!("Kerning (0 = Auto)"), |ui| {
|
||||
let kerning = shared(app, |s| s.kerning.unwrap_or(0.0));
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-kern", kerning, "", 0, fw, 10.0, -1000.0, &TRACKING_PRESETS) {
|
||||
format(app, if v == 0.0 { json!({"kerning": "auto"}) } else { json!({"kerning": v}) });
|
||||
}
|
||||
});
|
||||
cell(ui, "VA↔", tl!("Tracking"), |ui| {
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-track", shared(app, |s| s.tracking), "", 0, fw, 10.0, -1000.0, &TRACKING_PRESETS) {
|
||||
style(app, json!({"tracking": v}));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
if !more {
|
||||
return;
|
||||
}
|
||||
cell(ui, "IT", tl!("Vertical Scale %"), |ui| {
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-vs", s.v_scale, "%", 1, fw, 1.0, 1.0, &SCALE_PRESETS) {
|
||||
format(app, json!({"vScale": v}));
|
||||
}
|
||||
});
|
||||
cell(ui, "T↔", tl!("Horizontal Scale %"), |ui| {
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-hs", s.h_scale, "%", 1, fw, 1.0, 1.0, &SCALE_PRESETS) {
|
||||
format(app, json!({"hScale": v}));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
cell(ui, "Aª", tl!("Baseline Shift"), |ui| {
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-bs", Some(s.baseline_shift), unit, fw, 1.0, -1296.0, &[]) {
|
||||
format(app, json!({"baselineShift": v}));
|
||||
}
|
||||
});
|
||||
cell(ui, "⟲T", tl!("Character Rotation"), |ui| {
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-rot", s.rotation, "°", 1, fw, 15.0, -360.0, &super::transform::ANGLE_PRESETS) {
|
||||
format(app, json!({"rotation": v}));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
|
||||
/// The Font Size combo (Character and Properties panels, Control bar): typed, stepped, scrubbed or
|
||||
/// picked from the presets, in the type unit, blank where the selected text's sizes differ.
|
||||
pub(crate) fn size_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, width: f32) {
|
||||
let size = shared(app, |s| s.size);
|
||||
if let Some(v) = widgets::spin_field(ui, id, size, app.session.type_unit(), width, 1.0, 0.1, &SIZE_PRESETS) {
|
||||
style(app, json!({"size": v}));
|
||||
}
|
||||
}
|
||||
|
||||
/// The font family menu and the font style dropdown of style `s` (Character and Properties panels,
|
||||
/// Control bar), with their `ids` and `widths`.
|
||||
pub(crate) fn font_pickers(app: &mut VectorcraftApp, ui: &mut Ui, s: &CharStyle, ids: (&str, &str), widths: (f32, f32)) {
|
||||
let sample = crate::font_menu::sample_text(app);
|
||||
if let Some(pick) = crate::font_menu::font_menu(ui, ids.0, &s.font_family, widths.0, sample.as_deref(), crate::font_menu::MenuLook::of(app)) {
|
||||
crate::font_menu::apply(app, ui.ctx(), pick);
|
||||
}
|
||||
let styles = vectorcraft_text::FontDb::global().styles(&s.font_family);
|
||||
let names: Vec<&str> = styles.iter().map(String::as_str).collect();
|
||||
if let Some(name) = widgets::dropdown_names(ui, ids.1, &s.font_style, &names, widths.1).and_then(|i| names.get(i)) {
|
||||
style(app, json!({"style": name}));
|
||||
}
|
||||
}
|
||||
|
||||
/// The Control bar's type controls while type is selected or edited: the Character link (the
|
||||
/// Character panel in a popover), the font family and style menus and the Font Size combo.
|
||||
pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some((s, _)) = text_style(app) else { return };
|
||||
let link = ui
|
||||
.link(egui::RichText::new(tl!("Character:")).size(12.0).color(Tokens::get(ui.ctx()).text).underline())
|
||||
.on_hover_text(tl!("Character options"));
|
||||
widgets::popover(&link, link.clicked(), |ui| {
|
||||
ui.set_width(260.0);
|
||||
show(app, ui);
|
||||
});
|
||||
font_pickers(app, ui, &s, ("cb-font", "cb-font-style"), (150.0, 96.0));
|
||||
size_field(app, ui, "cb-font-size", 96.0);
|
||||
ui.add_space(4.0);
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some((s, _)) = text_style(app) else {
|
||||
super::empty_state(ui, "type", tl!("No text selected"), tl!("Select a text object to edit its character attributes."));
|
||||
return;
|
||||
};
|
||||
let w = ui.available_width();
|
||||
let sample = crate::font_menu::sample_text(app);
|
||||
if let Some(pick) = crate::font_menu::font_menu(ui, "ch-font", &s.font_family, w - 4.0, sample.as_deref(), crate::font_menu::MenuLook::of(app)) {
|
||||
crate::font_menu::apply(app, ui.ctx(), pick);
|
||||
}
|
||||
let styles = vectorcraft_text::FontDb::global().styles(&s.font_family);
|
||||
let snames: Vec<&str> = styles.iter().map(String::as_str).collect();
|
||||
if let Some(i) = widgets::dropdown_names(ui, "ch-style", &s.font_style, &snames, w - 4.0) {
|
||||
style(app, json!({"style": snames[i]}));
|
||||
}
|
||||
font_pickers(app, ui, &s, ("ch-font", "ch-style"), (w - 4.0, w - 4.0));
|
||||
ui.add_space(2.0);
|
||||
let fw = ((w - 66.0) / 2.0).clamp(60.0, 100.0);
|
||||
let unit = app.session.type_unit();
|
||||
egui::Grid::new("ch-grid").num_columns(2).spacing([6.0, 4.0]).show(ui, |ui| {
|
||||
cell(ui, "T", tl!("Font Size"), |ui| {
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-size", Some(s.size), unit, fw, 1.0, 0.1, &SIZE_PRESETS) {
|
||||
style(app, json!({"size": v}));
|
||||
}
|
||||
});
|
||||
cell(ui, "A↕", tl!("Leading"), |ui| {
|
||||
let presets: Vec<f64> = SIZE_PRESETS.iter().map(|v| v * 1.2).collect();
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-lead", Some(s.effective_leading()), unit, fw, 1.0, 0.1, &presets) {
|
||||
style(app, json!({"leading": v}));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
cell(ui, "VA", tl!("Kerning (0 = Auto)"), |ui| {
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-kern", s.kerning.unwrap_or(0.0), "", 0, fw, 10.0, -1000.0, &TRACKING_PRESETS) {
|
||||
format(app, if v == 0.0 { json!({"kerning": "auto"}) } else { json!({"kerning": v}) });
|
||||
}
|
||||
});
|
||||
cell(ui, "VA↔", tl!("Tracking"), |ui| {
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-track", s.tracking, "", 0, fw, 10.0, -1000.0, &TRACKING_PRESETS) {
|
||||
style(app, json!({"tracking": v}));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
if !pstate::<bool>(ui.ctx(), "ch-hide-options") {
|
||||
cell(ui, "IT", tl!("Vertical Scale %"), |ui| {
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-vs", s.v_scale, "%", 1, fw, 1.0, 1.0, &SCALE_PRESETS) {
|
||||
format(app, json!({"vScale": v}));
|
||||
}
|
||||
});
|
||||
cell(ui, "T↔", tl!("Horizontal Scale %"), |ui| {
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-hs", s.h_scale, "%", 1, fw, 1.0, 1.0, &SCALE_PRESETS) {
|
||||
format(app, json!({"hScale": v}));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
cell(ui, "Aª", tl!("Baseline Shift"), |ui| {
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-bs", Some(s.baseline_shift), unit, fw, 1.0, -1296.0, &[]) {
|
||||
format(app, json!({"baselineShift": v}));
|
||||
}
|
||||
});
|
||||
cell(ui, "⟲T", tl!("Character Rotation"), |ui| {
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-rot", s.rotation, "°", 1, fw, 15.0, -360.0, &super::transform::ANGLE_PRESETS) {
|
||||
format(app, json!({"rotation": v}));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
if pstate::<bool>(ui.ctx(), "ch-hide-options") {
|
||||
let hidden = pstate::<bool>(ui.ctx(), "ch-hide-options");
|
||||
metrics_grid(app, ui, "ch-grid", &s, w, !hidden);
|
||||
if hidden {
|
||||
return;
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
|
||||
@@ -473,6 +473,8 @@ mod tests_css_properties;
|
||||
#[cfg(test)]
|
||||
mod tests_effectedit;
|
||||
#[cfg(test)]
|
||||
mod tests_fontsize;
|
||||
#[cfg(test)]
|
||||
mod tests_freeform;
|
||||
#[cfg(test)]
|
||||
mod tests_links;
|
||||
|
||||
@@ -388,29 +388,10 @@ pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
};
|
||||
let s = tx.first_style();
|
||||
section_header(ui, tl!("Character"));
|
||||
let sample = crate::font_menu::sample_text(app);
|
||||
if let Some(pick) =
|
||||
crate::font_menu::font_menu(ui, "font", &s.font_family, ui.available_width() - 4.0, sample.as_deref(), crate::font_menu::MenuLook::of(app))
|
||||
{
|
||||
crate::font_menu::apply(app, ui.ctx(), pick);
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
dim_label(ui, tl!("Size"));
|
||||
let type_unit = app.session.type_unit();
|
||||
if let Some(v) = widgets::num_field(ui, "fsize", Some(s.size), type_unit, 70.0) {
|
||||
app.run("text.setStyle", json!({"size": v})).ok();
|
||||
}
|
||||
dim_label(ui, tl!("Leading"));
|
||||
if let Some(v) = widgets::num_field(ui, "lead", Some(s.effective_leading()), type_unit, 70.0) {
|
||||
app.run("text.setStyle", json!({"leading": v})).ok();
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
dim_label(ui, tl!("Tracking"));
|
||||
if let Some(v) = widgets::plain_field(ui, "track", s.tracking, "", 0, 60.0) {
|
||||
app.run("text.setStyle", json!({"tracking": v})).ok();
|
||||
}
|
||||
});
|
||||
// The font, Font Size, Leading, Kerning and Tracking as in the Character panel.
|
||||
let w = ui.available_width();
|
||||
super::character::font_pickers(app, ui, &s, ("font", "font-style"), (w - 4.0, w - 4.0));
|
||||
super::character::metrics_grid(app, ui, "props-char", &s, w, false);
|
||||
section_header(ui, tl!("Paragraph"));
|
||||
ui.horizontal(|ui| {
|
||||
for (icon, j) in [("align-start-vertical", "left"), ("align-center-vertical", "center"), ("align-end-vertical", "right")] {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
//! Headless frames of the Font Size and Leading combos (#458): a preset picked from the dropdown
|
||||
//! applies in one undo step, the presets show in the type unit, Leading's Auto entry, sizes that
|
||||
//! differ show blank, and the Control bar's Character controls.
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{CharStyle, NodeKind, Unit};
|
||||
|
||||
use super::tests_appearance::{app_with_rect, click, frame_events, run, text_rect};
|
||||
use super::*;
|
||||
|
||||
type Texts = Vec<(String, egui::Rect)>;
|
||||
|
||||
fn shows(t: &Texts, s: &str) -> bool {
|
||||
t.iter().any(|(x, _)| x == s)
|
||||
}
|
||||
|
||||
/// [`app_with_rect`] with 12 pt type selected instead; returns its id.
|
||||
fn app_with_text() -> (VectorcraftApp, u64) {
|
||||
let mut app = app_with_rect();
|
||||
let id = run(&mut app, "text.create", json!({"x": 10, "y": 150, "text": "Hi", "size": 12}))["id"].as_u64().unwrap();
|
||||
run(&mut app, "select.set", json!({ "ids": [id] }));
|
||||
(app, id)
|
||||
}
|
||||
|
||||
fn style(app: &VectorcraftApp, id: u64) -> CharStyle {
|
||||
match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.first_style(),
|
||||
_ => panic!("not type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn undo_len(app: &VectorcraftApp) -> usize {
|
||||
app.session.active().unwrap().history.undo.len()
|
||||
}
|
||||
|
||||
/// A 120 pt Font Size combo in a row; `at` keeps where it starts.
|
||||
fn size_combo(at: &Cell<egui::Pos2>) -> impl FnMut(&mut VectorcraftApp, &mut Ui) + '_ {
|
||||
|app, ui| {
|
||||
at.set(ui.max_rect().min);
|
||||
ui.horizontal(|ui| character::size_field(app, ui, "size", 120.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_picked_font_size_preset_applies_in_one_undo_step() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let (mut app, id) = app_with_text();
|
||||
let at = Cell::new(egui::Pos2::ZERO);
|
||||
let t = frame_events(&ctx, &mut app, vec![], size_combo(&at));
|
||||
assert!(shows(&t, "12 pt") && !shows(&t, "24 pt"), "closed: {t:?}");
|
||||
// The chevron is the combo's last 20 pt.
|
||||
click(&ctx, &mut app, at.get() + egui::vec2(110.0, 13.0), size_combo(&at));
|
||||
let t = frame_events(&ctx, &mut app, vec![], size_combo(&at));
|
||||
assert!(shows(&t, "6 pt") && shows(&t, "24 pt") && shows(&t, "72 pt"), "open: {t:?}");
|
||||
let undo = undo_len(&app);
|
||||
click(&ctx, &mut app, text_rect(&t, "24 pt").center(), size_combo(&at));
|
||||
assert_eq!(style(&app, id).size, 24.0);
|
||||
assert_eq!(undo_len(&app), undo + 1, "one undo step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_size_presets_show_in_the_type_unit() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let (mut app, _) = app_with_text();
|
||||
app.run("prefs.set", json!({"key": "unitsType", "value": "millimeters"})).unwrap();
|
||||
let at = Cell::new(egui::Pos2::ZERO);
|
||||
frame_events(&ctx, &mut app, vec![], size_combo(&at));
|
||||
click(&ctx, &mut app, at.get() + egui::vec2(110.0, 13.0), size_combo(&at));
|
||||
let t = frame_events(&ctx, &mut app, vec![], size_combo(&at));
|
||||
assert!(shows(&t, &Unit::Millimeters.format(24.0)) && !shows(&t, "24 pt"), "{t:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_lists_auto_first() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let (mut app, id) = app_with_text();
|
||||
run(&mut app, "text.setStyle", json!({"leading": 30}));
|
||||
let t = frame_events(&ctx, &mut app, vec![], character::show);
|
||||
// The Leading cell: its 22 pt label, the spacing, then the spinner (16), field (100 - 36)
|
||||
// and chevron (20) of a 100 pt combo.
|
||||
let label = text_rect(&t, "A↕");
|
||||
let sp = ctx.global_style().spacing.item_spacing.x;
|
||||
click(&ctx, &mut app, egui::pos2(label.center().x + 11.0 + sp + 90.0, label.center().y), character::show);
|
||||
let t = frame_events(&ctx, &mut app, vec![], character::show);
|
||||
click(&ctx, &mut app, text_rect(&t, "Auto").center(), character::show);
|
||||
assert_eq!(style(&app, id).leading, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sizes_that_differ_show_blank() {
|
||||
let ctx = egui::Context::default();
|
||||
let (mut app, a) = app_with_text();
|
||||
let b = run(&mut app, "text.create", json!({"x": 10, "y": 180, "text": "Ho", "size": 24}))["id"].as_u64().unwrap();
|
||||
run(&mut app, "select.set", json!({ "ids": [a, b] }));
|
||||
let t = frame_events(&ctx, &mut app, vec![], character::show);
|
||||
assert!(!shows(&t, "12 pt") && !shows(&t, "24 pt"), "{t:?}");
|
||||
run(&mut app, "select.set", json!({ "ids": [b] }));
|
||||
assert!(shows(&frame_events(&ctx, &mut app, vec![], character::show), "24 pt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_control_bar_shows_the_font_size_of_selected_type() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let (mut app, _) = app_with_text();
|
||||
let t = frame_events(&ctx, &mut app, vec![], crate::chrome::control_bar);
|
||||
assert!(shows(&t, "Character:") && shows(&t, "12 pt"), "{t:?}");
|
||||
run(&mut app, "select.set", json!({ "ids": [] }));
|
||||
assert!(!shows(&frame_events(&ctx, &mut app, vec![], crate::chrome::control_bar), "Character:"));
|
||||
}
|
||||
@@ -830,7 +830,10 @@ pub fn radio(ui: &mut Ui, label: &str, selected: bool, enabled: bool) -> bool {
|
||||
}
|
||||
|
||||
/// Numeric field with an up/down spinner on the left and a preset dropdown on the right
|
||||
/// (Stroke weight, font size, leading…). `presets` empty = no dropdown. Returns the committed value.
|
||||
/// (Stroke weight, font size…): an editable combo box whose field keeps typing, ↑/↓ stepping and
|
||||
/// label scrubbing. `value` (points) shows in `unit`, blank when `None` (the selection's values
|
||||
/// differ); `presets` (points) are listed in `unit` too, and empty = no dropdown. Returns the
|
||||
/// committed, stepped or picked value.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spin_field(
|
||||
ui: &mut Ui,
|
||||
@@ -842,15 +845,50 @@ pub fn spin_field(
|
||||
min: f64,
|
||||
presets: &[f64],
|
||||
) -> Option<f64> {
|
||||
spin_generic(ui, value, width, step, min, presets, &|v| unit.format(v), &mut |ui, fw| num_field(ui, id, value, unit, fw))
|
||||
spin_field_auto(ui, id, value, None, unit, width, step, min, presets).and_then(SpinPick::value)
|
||||
}
|
||||
|
||||
/// [`spin_field`] for unitless values (percent, degrees, 1/1000 em) with a display suffix.
|
||||
/// What a [`spin_field_auto`] returns: a value (points), or its Auto entry.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum SpinPick {
|
||||
Value(f64),
|
||||
Auto,
|
||||
}
|
||||
|
||||
impl SpinPick {
|
||||
/// The value, `None` for Auto.
|
||||
pub fn value(self) -> Option<f64> {
|
||||
match self {
|
||||
SpinPick::Value(v) => Some(v),
|
||||
SpinPick::Auto => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`spin_field`] whose dropdown starts with an Auto entry (Leading): `auto` is `Some(on)`, with
|
||||
/// `on` checking it (no preset is then checked), or `None` for no Auto entry.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spin_field_auto(
|
||||
ui: &mut Ui,
|
||||
id: impl std::hash::Hash + std::fmt::Debug + Copy,
|
||||
value: Option<f64>,
|
||||
auto: Option<bool>,
|
||||
unit: Unit,
|
||||
width: f32,
|
||||
step: f64,
|
||||
min: f64,
|
||||
presets: &[f64],
|
||||
) -> Option<SpinPick> {
|
||||
spin_generic(ui, value, auto, width, step, min, presets, &|v| unit.format(v), &mut |ui, fw| num_field(ui, id, value, unit, fw))
|
||||
}
|
||||
|
||||
/// [`spin_field`] for unitless values (percent, degrees, 1/1000 em) with a display suffix; a
|
||||
/// `None` value (the selection's values differ) shows blank.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spin_plain(
|
||||
ui: &mut Ui,
|
||||
id: impl std::hash::Hash + std::fmt::Debug + Copy,
|
||||
value: f64,
|
||||
value: impl Into<Option<f64>>,
|
||||
suffix: &str,
|
||||
decimals: usize,
|
||||
width: f32,
|
||||
@@ -858,21 +896,24 @@ pub fn spin_plain(
|
||||
min: f64,
|
||||
presets: &[f64],
|
||||
) -> Option<f64> {
|
||||
let value = value.into();
|
||||
let fmt = |v: f64| format!("{v}{suffix}");
|
||||
spin_generic(ui, Some(value), width, step, min, presets, &fmt, &mut |ui, fw| plain_field(ui, id, value, suffix, decimals, fw))
|
||||
spin_generic(ui, value, None, width, step, min, presets, &fmt, &mut |ui, fw| mixed_field(ui, id, value, suffix, decimals, fw))
|
||||
.and_then(SpinPick::value)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn spin_generic(
|
||||
ui: &mut Ui,
|
||||
value: Option<f64>,
|
||||
auto: Option<bool>,
|
||||
width: f32,
|
||||
step: f64,
|
||||
min: f64,
|
||||
presets: &[f64],
|
||||
fmt: &dyn Fn(f64) -> String,
|
||||
field: &mut dyn FnMut(&mut Ui, f32) -> Option<f64>,
|
||||
) -> Option<f64> {
|
||||
) -> Option<SpinPick> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut out = None;
|
||||
let h = 26.0;
|
||||
@@ -898,24 +939,31 @@ fn spin_generic(
|
||||
ui.painter().line_segment([m + vec2(-3.0, -d), m + vec2(0.0, d)], Stroke::new(1.2, c));
|
||||
ui.painter().line_segment([m + vec2(0.0, d), m + vec2(3.0, -d)], Stroke::new(1.2, c));
|
||||
}
|
||||
// A blank field (values that differ) has nothing to step from.
|
||||
if sresp.clicked()
|
||||
&& let Some(p) = sresp.interact_pointer_pos()
|
||||
&& let Some(v) = value
|
||||
{
|
||||
let v = value.unwrap_or(0.0);
|
||||
let nv = if up.contains(p) { v + step } else { v - step };
|
||||
out = Some(nv.max(min));
|
||||
out = Some(SpinPick::Value(nv.max(min)));
|
||||
}
|
||||
let fw = if presets.is_empty() { width - 16.0 } else { width - 36.0 };
|
||||
if let Some(v) = field(ui, fw) {
|
||||
out = Some(v.max(min));
|
||||
out = Some(SpinPick::Value(v.max(min)));
|
||||
}
|
||||
if !presets.is_empty() {
|
||||
let dresp = chevron_cell(ui, h);
|
||||
egui::Popup::menu(&dresp).show(|ui| {
|
||||
ui.set_min_width(width - 10.0);
|
||||
if let Some(on) = auto
|
||||
&& ui.selectable_label(on, tl!("Auto")).clicked()
|
||||
{
|
||||
out = Some(SpinPick::Auto);
|
||||
}
|
||||
let current = value.filter(|_| auto != Some(true));
|
||||
for p in presets {
|
||||
if ui.selectable_label(value.is_some_and(|v| (v - p).abs() < 1e-6), fmt(*p)).clicked() {
|
||||
out = Some(*p);
|
||||
if ui.selectable_label(current.is_some_and(|v| (v - p).abs() < 1e-6), fmt(*p)).clicked() {
|
||||
out = Some(SpinPick::Value(*p));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user