diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 385a6b1ca..20a62c68a 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -112,6 +112,8 @@ pub enum Icon { Fullscreen, Channel, Exposure, + /// Stroke width: three bars, thin to thick. + StrokeWidth, // links Chat, Globe, @@ -548,6 +550,11 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) { } pen.circle(8.0, 8.0, 4.2); } + StrokeWidth => { + pen.rect_fill(2.5, 3.0, 13.5, 3.75); + pen.rect_fill(2.5, 6.5, 13.5, 8.0); + pen.rect_fill(2.5, 10.5, 13.5, 13.0); + } Hamburger => { for y in [4.5, 8.0, 11.5] { pen.line(&[(3.0, y), (13.0, y)]); diff --git a/crates/ui-egui/src/panels/text_panels.rs b/crates/ui-egui/src/panels/text_panels.rs index 4a2a8b6cd..2e63e203e 100644 --- a/crates/ui-egui/src/panels/text_panels.rs +++ b/crates/ui-egui/src/panels/text_panels.rs @@ -127,16 +127,21 @@ fn numeric_fields(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter let row = i / 2; let fx = x0 + col as f32 * (w / 2.0); let fy = y + row as f32 * 28.0; - p.text(pos2(fx, fy + 9.0), Align2::LEFT_CENTER, glyph, Tokens::semibold(11.0), t.text_dim); - let (r, nv, _) = widgets::hot_number_at( - ui, - pos2(fx + 26.0, fy), - egui::Id::unique(("char", key)), - v, - &widgets::HotSpec { speed: 0.5, range, decimals: 0, suffix }, - &t, - ); + let stroke = key == "strokeWidth"; + if stroke { + // The stroke-width icon, as in After Effects, not a letter that reads as "width". + crate::icons::paint(p, Rect::from_min_size(pos2(fx, fy + 1.0), vec2(18.0, 18.0)), crate::icons::Icon::StrokeWidth, t.text_dim); + } else { + p.text(pos2(fx, fy + 9.0), Align2::LEFT_CENTER, glyph, Tokens::semibold(11.0), t.text_dim); + } + // Fractional stroke widths (the 0.25 / 0.5 / 0.75 presets) show their decimals. + let decimals = if stroke && v.fract().abs() > 1e-9 { 2 } else { 0 }; + let (r, nv, _) = + widgets::hot_number_at(ui, pos2(fx + 26.0, fy), egui::Id::unique(("char", key)), v, &widgets::HotSpec { speed: 0.5, range, decimals, suffix }, &t); app.auto.add(&format!("character.{key}"), r, key); + if stroke { + stroke_width_presets(app, ui, r, enabled, actions); + } if let Some(nv) = nv && enabled { @@ -145,6 +150,26 @@ fn numeric_fields(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter } } +/// The stroke width's presets menu beside its value (`r`), as in After Effects. +fn stroke_width_presets(app: &mut EffectcraftApp, ui: &mut egui::Ui, r: Rect, enabled: bool, actions: &mut Vec) { + const PRESETS: [f64; 22] = [0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]; + let t = app.tokens; + let br = Rect::from_min_size(pos2(r.max.x + 4.0, r.min.y + 2.0), vec2(16.0, 16.0)); + let pop = egui::Id::unique("char-stroke-width-pop"); + if widgets::icon_button(ui, br, crate::icons::Icon::ChevronDown, false, &t, egui::Id::unique("char-stroke-width-presets")) + .on_hover_text(tr("Stroke width presets")) + .clicked() + && enabled + { + widgets::open_popup(ui, pop); + } + app.auto.add("character.strokeWidth.presets", br, "Stroke width presets"); + let labels: Vec = PRESETS.iter().map(|v| format!("{v} px")).collect(); + if let Some(v) = widgets::popup_menu(ui, pop, br.left_bottom(), &labels, None).and_then(|i| PRESETS.get(i)) { + actions.push(json!({"strokeWidth": v})); + } +} + /// Kerning: Metrics / Optical / a manual value (grid row 1, left). fn kerning_field(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, g: Grid, doc: &TextDoc, actions: &mut Vec) { let t = app.tokens; diff --git a/crates/ui-egui/tests/it/ui_text_edit.rs b/crates/ui-egui/tests/it/ui_text_edit.rs index ea4b56d26..8af1a8e90 100644 --- a/crates/ui-egui/tests/it/ui_text_edit.rs +++ b/crates/ui-egui/tests/it/ui_text_edit.rs @@ -8,6 +8,7 @@ use effectcraft_ui_egui::EffectcraftApp; use effectcraft_ui_egui::dock::PanelKind; use egui::{Event, Key, Modifiers, PointerButton, Pos2}; use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; use serde_json::json; fn app() -> EffectcraftApp { @@ -1750,3 +1751,19 @@ fn character_panel_line_join_buttons_set_the_join() { click(&mut h, bevel, Modifiers::NONE); assert_eq!(join(&h), effectcraft_engine::keyframe::LineJoin::Bevel); } + +/// #2075: the Character panel's stroke width has a presets menu (0.25 … 100 px) beside its value. +#[test] +fn character_panel_stroke_width_presets_set_the_width() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Type", "width": 640, "height": 360, "duration": 2})).unwrap(); + let lid = s.execute("layer.newText", json!({"text": "Stroke", "size": 80, "applyStroke": true, "strokeWidth": 8})).unwrap()["layer"].as_u64().unwrap(); + let mut h = harness(EffectcraftApp::new(s)); + h.state_mut().show_panel(PanelKind::Character); + h.run_steps(3); + let arrow = rect(&h, "character.strokeWidth.presets").center(); + click(&mut h, arrow, Modifiers::NONE); + let entry = h.get_by_label("0.5 px").rect().center(); + click(&mut h, entry, Modifiers::NONE); + assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().stroke_width, 0.5); +}