Character panel: the stroke width shows its icon and a presets menu (#2075)

The stroke width was a field labelled W, which read as the width of the
text, not its stroke. It now shows the stroke-width icon (three bars,
thin to thick), sits beside the stroke order menu as before, and has a
presets menu (0.25 to 100 px) next to its value, as in After Effects;
fractional widths show their decimals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-30 10:34:01 +07:00
1 parent a934abff07
commit 5085a1c873
3 files changed
+58 -9

No files matched your search

+7
View File
@@ -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)]);
+34 -9
View File
@@ -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<serde_json::Value>) {
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<String> = 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<serde_json::Value>) {
let t = app.tokens;
+17
View File
@@ -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);
}