mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 15:48:33 +00:00
Character panel: All Fills Over All Strokes and All Strokes Over All Fills in the stroke order menu
This commit is contained in:
1 parent
9250a30b37
commit
3cd7a19710
3 files changed
+141
-7
No files matched your search
@@ -173,21 +173,43 @@ fn kerning_field(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke Over Fill / Fill Over Stroke (grid row 2, right).
|
||||
fn stroke_order_field(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: Grid, doc: &TextDoc, actions: &mut Vec<serde_json::Value>) {
|
||||
/// Stroke order (grid row 2, right): per character (Fill Over Stroke / Stroke Over Fill), or for
|
||||
/// the whole layer, which is the layer's More Options > Fill & Stroke (`fill_stroke`, see
|
||||
/// [`layer_fill_stroke`]). Returns the layer order picked, if any.
|
||||
fn stroke_order_field(
|
||||
app: &mut EffectcraftApp,
|
||||
ui: &mut egui::Ui,
|
||||
g: Grid,
|
||||
doc: &TextDoc,
|
||||
fill_stroke: u32,
|
||||
actions: &mut Vec<serde_json::Value>,
|
||||
) -> Option<u32> {
|
||||
let t = app.tokens;
|
||||
let Grid { x0, y, w, enabled } = g;
|
||||
let fx = x0 + w / 2.0;
|
||||
let fy = y + 2.0 * 28.0;
|
||||
let dr = Rect::from_min_size(pos2(fx, fy), vec2(w / 2.0, 20.0));
|
||||
let labels = vec!["Fill Over Stroke".to_string(), "Stroke Over Fill".to_string()];
|
||||
let labels: Vec<String> =
|
||||
["Fill Over Stroke", "Stroke Over Fill", "-", "All Fills Over All Strokes", "All Strokes Over All Fills"].iter().map(|s| s.to_string()).collect();
|
||||
let current = match fill_stroke {
|
||||
1 => 3,
|
||||
2 => 4,
|
||||
_ => doc.stroke_over_fill as usize,
|
||||
};
|
||||
let pop = egui::Id::unique("char-stroke-order-pop");
|
||||
if widgets::dropdown(ui, dr, &labels[doc.stroke_over_fill as usize], &t, egui::Id::unique("char-stroke-order")).clicked() && enabled {
|
||||
if widgets::dropdown(ui, dr, &labels[current], &t, egui::Id::unique("char-stroke-order")).clicked() && enabled {
|
||||
widgets::open_popup(ui, pop);
|
||||
}
|
||||
app.auto.add("character.strokeOverFill", dr, "Stroke order");
|
||||
if let Some(i) = widgets::popup_menu(ui, pop, dr.left_bottom(), &labels, Some(doc.stroke_over_fill as usize)) {
|
||||
actions.push(json!({"strokeOverFill": i == 1}));
|
||||
match widgets::popup_menu(ui, pop, dr.left_bottom(), &labels, Some(current)) {
|
||||
Some(i @ (3 | 4)) => Some(i as u32 - 2),
|
||||
Some(i) => {
|
||||
actions.push(json!({"strokeOverFill": i == 1}));
|
||||
// A per-character order puts the layer back on the Per Character Palette, or it
|
||||
// would have no visible effect.
|
||||
(fill_stroke != 0).then_some(0)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +227,36 @@ fn line_join_field(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the layer's More Options > Fill & Stroke to `v` together with the per-character order
|
||||
/// it changes, as one undo step.
|
||||
fn apply_layer_fill_stroke(app: &mut EffectcraftApp, ctx: &egui::Context, tt: &TextTarget, v: u32, actions: Vec<serde_json::Value>) {
|
||||
let set = json!({"command": "prop.set", "params": {"layer": tt.layer, "path": "text/moreOptions/fillStroke", "value": v}});
|
||||
let steps: Vec<serde_json::Value> =
|
||||
std::iter::once(set).chain(actions.into_iter().map(|a| json!({"command": "layer.setText", "params": tt.params(a)}))).collect();
|
||||
if let Err(e) = crate::menus::invoke(app, ctx, "engine.batch", json!({"steps": steps, "label": "Stroke Order"})) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
}
|
||||
|
||||
/// A text layer's More Options > Fill & Stroke in the active comp: 0 per character palette,
|
||||
/// 1 all fills over all strokes, 2 all strokes over all fills.
|
||||
fn layer_fill_stroke(app: &EffectcraftApp, layer: u64) -> u32 {
|
||||
let (Some(comp), Some(cid)) = (app.session.active_comp(), app.session.active_comp_id()) else { return 0 };
|
||||
let Some(l) = comp.layer(effectcraft_engine::project::LayerId(layer)) else { return 0 };
|
||||
let Some(more) = l.props.sub("text").and_then(|t| t.sub("moreOptions")) else { return 0 };
|
||||
let ectx = EvalCtx {
|
||||
project: &app.session.project,
|
||||
comp_id: cid,
|
||||
comp,
|
||||
time: app.session.time(),
|
||||
expr: app.session.expr.as_deref(),
|
||||
footage: None,
|
||||
property_overrides: &[],
|
||||
media_overrides: &[],
|
||||
};
|
||||
ectx.e(l, more, "fillStroke")
|
||||
}
|
||||
|
||||
/// The Character panel for the text being edited (or the selected text layer): font family and
|
||||
/// style, size, leading, tracking, stroke, scale, baseline shift and other numeric fields, kerning,
|
||||
/// fill/stroke colours with eyedroppers, style toggles, OpenType features and variable-font axes.
|
||||
@@ -246,7 +298,8 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let grid = Grid { x0, y, w, enabled };
|
||||
numeric_fields(app, ui, &p, grid, &doc, &mut actions);
|
||||
kerning_field(app, ui, &p, grid, &doc, &mut actions);
|
||||
stroke_order_field(app, ui, grid, &doc, &mut actions);
|
||||
let fill_stroke = target.as_ref().map_or(0, |tt| layer_fill_stroke(app, tt.layer));
|
||||
let layer_fill_stroke_pick = stroke_order_field(app, ui, grid, &doc, fill_stroke, &mut actions);
|
||||
line_join_field(app, ui, &p, grid, &doc, &mut actions);
|
||||
y += 6.0 * 28.0 + 8.0;
|
||||
// Fill / stroke swatches with their enable boxes.
|
||||
@@ -383,6 +436,9 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
_ => {}
|
||||
}
|
||||
let Some(tt) = target else { return };
|
||||
if let Some(v) = layer_fill_stroke_pick {
|
||||
apply_layer_fill_stroke(app, &ctx, &tt, v, std::mem::take(&mut actions));
|
||||
}
|
||||
for a in actions {
|
||||
if let Err(e) = crate::menus::invoke(app, &ctx, "layer.setText", tt.params(a)) {
|
||||
app.ui.status = e;
|
||||
|
||||
@@ -13,6 +13,7 @@ mod ui_batch_switches;
|
||||
mod ui_blank_context_menus;
|
||||
mod ui_camera_tracker;
|
||||
mod ui_character_eyedropper;
|
||||
mod ui_character_stroke_order;
|
||||
mod ui_color_hex;
|
||||
mod ui_comp_presets;
|
||||
mod ui_control_modifiers;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//! The Character panel's stroke order menu: the two per-character orders and the two layer-wide
|
||||
//! ones (All Fills Over All Strokes, All Strokes Over All Fills), which set the text layer's
|
||||
//! More Options ▸ Fill & Stroke.
|
||||
use effectcraft_engine::{Session, commands::text_edit::layer_doc, project::LayerId};
|
||||
use effectcraft_ui_egui::{EffectcraftApp, dock::PanelKind};
|
||||
use egui::{
|
||||
Event, Modifiers, PointerButton, Pos2,
|
||||
accesskit::{Role, Toggled},
|
||||
pos2, vec2,
|
||||
};
|
||||
use egui_kittest::{
|
||||
Harness,
|
||||
kittest::{NodeT, Queryable},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
fn click(h: &mut Harness<'_, EffectcraftApp>, pos: Pos2) {
|
||||
h.event(Event::PointerMoved(pos));
|
||||
h.step();
|
||||
for pressed in [true, false] {
|
||||
h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
}
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn open_menu(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
let e = h.state().auto.find("character.strokeOverFill").unwrap_or_else(|| panic!("missing character.strokeOverFill")).clone();
|
||||
click(h, pos2(e.rect[0] + e.rect[2] * 0.5, e.rect[1] + e.rect[3] * 0.5));
|
||||
}
|
||||
|
||||
fn pick(h: &mut Harness<'_, EffectcraftApp>, label: &str) {
|
||||
open_menu(h);
|
||||
// A menu row: the dropdown shows the current order under the same label.
|
||||
h.get_by_role_and_label(Role::Button, label).click();
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn fill_stroke(s: &mut Session, layer: u64) -> serde_json::Value {
|
||||
s.execute("prop.get", json!({"layer": layer, "path": "text/moreOptions/fillStroke"})).unwrap()["value"].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_stroke_order_menu_sets_layer_fill_and_stroke() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"width": 320, "height": 180, "duration": 2})).unwrap();
|
||||
let text = s.execute("layer.newText", json!({"text": "Caption", "font": "Inter"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setText", json!({"layer": text, "applyStroke": true, "strokeWidth": 8.0, "strokeOverFill": false})).unwrap();
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
app.set_workspace("Text");
|
||||
let builder = Harness::builder().with_size(vec2(1600.0, 1400.0));
|
||||
let mut h = if std::env::var_os("EC_SNAPSHOT_DIR").is_some() { builder.wgpu().build_eframe(|_| app) } else { builder.build_eframe(|_| app) };
|
||||
h.state_mut().show_panel(PanelKind::Character);
|
||||
h.run_steps(4);
|
||||
|
||||
pick(&mut h, "All Fills Over All Strokes");
|
||||
assert_eq!(fill_stroke(&mut h.state_mut().session, text), json!(1));
|
||||
pick(&mut h, "All Strokes Over All Fills");
|
||||
assert_eq!(fill_stroke(&mut h.state_mut().session, text), json!(2));
|
||||
// The menu shows the layer's order as the current one.
|
||||
open_menu(&mut h);
|
||||
assert_eq!(h.get_by_role_and_label(Role::Button, "All Strokes Over All Fills").accesskit_node().toggled(), Some(Toggled::True));
|
||||
assert_eq!(h.get_by_role_and_label(Role::Button, "Fill Over Stroke").accesskit_node().toggled(), Some(Toggled::False));
|
||||
if let Ok(dir) = std::env::var("EC_SNAPSHOT_DIR") {
|
||||
h.render().unwrap().save(format!("{dir}/character-stroke-order.png")).unwrap();
|
||||
}
|
||||
h.key_press(egui::Key::Escape);
|
||||
h.run_steps(2);
|
||||
|
||||
// A per-character order goes back to the Per Character Palette, in one undo step.
|
||||
pick(&mut h, "Stroke Over Fill");
|
||||
assert_eq!(fill_stroke(&mut h.state_mut().session, text), json!(0));
|
||||
assert!(layer_doc(&h.state().session, LayerId(text)).unwrap().stroke_over_fill);
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(fill_stroke(&mut h.state_mut().session, text), json!(2));
|
||||
assert!(!layer_doc(&h.state().session, LayerId(text)).unwrap().stroke_over_fill);
|
||||
}
|
||||
Reference in new issue
Block a user