feat(panels): shadow/blur parameter steppers in the Effects section
Completes the Effects-controls gap — each effect row now exposes its editable scalar parameters: - op-editor-core: `EffectField` enum + `EditorCommand::SetEffectParam` + `cmd_set_effect_param` — writes one shadow param (offset X/Y, blur, spread) or a blur/background-blur radius; blur values clamp to >= 0, field/effect mismatches reject. - panels: each effect block paints a parameter row per field — `<label> <value> [-] [+]`; the "-"/"+" steppers emit `AdjustEffectParam` (the walker computes the post-step value from the current one). `effects_section_height` / the action-rect walker now take the effects slice so the variable per-kind block height stays aligned with paint (`VisibleSections.effect_count` retired in favour of the slice). - both hosts dispatch `AdjustEffectParam` via `SetEffectParam`, history-committed. op-editor-core 233 / op-editor-ui 140 / op-host-desktop 64 tests green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
parent
e30f3b12b4
commit
722445b017
|
|
@ -40,6 +40,18 @@ pub enum NodeFlag {
|
|||
Collapsed,
|
||||
}
|
||||
|
||||
/// Which scalar parameter [`EditorCommand::SetEffectParam`] writes.
|
||||
/// `OffsetX` / `OffsetY` / `Blur` / `Spread` target a Shadow effect;
|
||||
/// `Radius` targets a Blur / BackgroundBlur effect.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EffectField {
|
||||
OffsetX,
|
||||
OffsetY,
|
||||
Blur,
|
||||
Spread,
|
||||
Radius,
|
||||
}
|
||||
|
||||
/// Wire-friendly value payload for [`EditorCommand::SetVariableScalar`]
|
||||
/// — a non-color scalar variable's new value.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
|
@ -208,6 +220,13 @@ pub enum EditorCommand {
|
|||
AddNodeEffect { node_id: NodeId, kind: String },
|
||||
/// Remove the effect at `index` from a node's effect list.
|
||||
RemoveNodeEffect { node_id: NodeId, index: u32 },
|
||||
/// Set one scalar parameter of the effect at `index` on a node.
|
||||
SetEffectParam {
|
||||
node_id: NodeId,
|
||||
index: u32,
|
||||
field: EffectField,
|
||||
value: f32,
|
||||
},
|
||||
/// Set the active canvas tool.
|
||||
SetActiveTool { tool: String },
|
||||
/// Undo the last change.
|
||||
|
|
|
|||
|
|
@ -180,6 +180,12 @@ impl EditorState {
|
|||
EditorCommand::RemoveNodeEffect { node_id, index } => {
|
||||
self.cmd_remove_node_effect(&node_id, index)
|
||||
}
|
||||
EditorCommand::SetEffectParam {
|
||||
node_id,
|
||||
index,
|
||||
field,
|
||||
value,
|
||||
} => self.cmd_set_effect_param(&node_id, index, field, value),
|
||||
|
||||
// --- Variables + themes --------------------------------
|
||||
EditorCommand::SetVariableColor { name, hex } => self.set_variable_color(&name, &hex),
|
||||
|
|
|
|||
|
|
@ -167,3 +167,63 @@ fn remove_node_effect_rejects_out_of_range() {
|
|||
index: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_effect_param_writes_shadow_offset() {
|
||||
let mut s = state_with(vec![rect("n1", "r", 0.0, 0.0, 10.0, 10.0)]);
|
||||
assert!(s.apply(EditorCommand::AddNodeEffect {
|
||||
node_id: id("n1"),
|
||||
kind: "shadow".into(),
|
||||
}));
|
||||
assert!(s.apply(EditorCommand::SetEffectParam {
|
||||
node_id: id("n1"),
|
||||
index: 0,
|
||||
field: crate::EffectField::OffsetX,
|
||||
value: 12.0,
|
||||
}));
|
||||
match find_node(s.active_children(), &id("n1")).unwrap() {
|
||||
PenNode::Rectangle(r) => match &r.container.effects.as_ref().unwrap()[0] {
|
||||
jian_ops_schema::style::PenEffect::Shadow(sh) => assert_eq!(sh.offset_x, 12.0),
|
||||
other => panic!("expected shadow, got {other:?}"),
|
||||
},
|
||||
other => panic!("expected rect, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_effect_param_clamps_blur_radius_to_non_negative() {
|
||||
let mut s = state_with(vec![rect("n1", "r", 0.0, 0.0, 10.0, 10.0)]);
|
||||
assert!(s.apply(EditorCommand::AddNodeEffect {
|
||||
node_id: id("n1"),
|
||||
kind: "blur".into(),
|
||||
}));
|
||||
assert!(s.apply(EditorCommand::SetEffectParam {
|
||||
node_id: id("n1"),
|
||||
index: 0,
|
||||
field: crate::EffectField::Radius,
|
||||
value: -9.0,
|
||||
}));
|
||||
match find_node(s.active_children(), &id("n1")).unwrap() {
|
||||
PenNode::Rectangle(r) => match &r.container.effects.as_ref().unwrap()[0] {
|
||||
jian_ops_schema::style::PenEffect::Blur(b) => assert_eq!(b.radius, 0.0),
|
||||
other => panic!("expected blur, got {other:?}"),
|
||||
},
|
||||
other => panic!("expected rect, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_effect_param_rejects_field_effect_mismatch() {
|
||||
let mut s = state_with(vec![rect("n1", "r", 0.0, 0.0, 10.0, 10.0)]);
|
||||
assert!(s.apply(EditorCommand::AddNodeEffect {
|
||||
node_id: id("n1"),
|
||||
kind: "blur".into(),
|
||||
}));
|
||||
// OffsetX is a Shadow-only field — rejected on a Blur effect.
|
||||
assert!(!s.apply(EditorCommand::SetEffectParam {
|
||||
node_id: id("n1"),
|
||||
index: 0,
|
||||
field: crate::EffectField::OffsetX,
|
||||
value: 5.0,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
//! Each helper keeps the validate-then-mutate discipline: kind / range
|
||||
//! / hex checks happen BEFORE the mutable borrow + write.
|
||||
|
||||
use crate::command::NodeFlag;
|
||||
use crate::command::{EffectField, NodeFlag};
|
||||
use crate::fills::{set_primary_fill_hex, set_primary_stroke_hex};
|
||||
use crate::node_id::NodeId;
|
||||
use crate::pen_node_ext::PenNodeExt;
|
||||
|
|
@ -408,4 +408,42 @@ impl EditorState {
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// `SetEffectParam` — write one scalar param of the effect at
|
||||
/// `index`. Blur values are clamped to ≥ 0. Rejects a non-finite
|
||||
/// value, an out-of-range index, or a field that doesn't match
|
||||
/// the effect variant (e.g. `Radius` on a Shadow).
|
||||
pub(crate) fn cmd_set_effect_param(
|
||||
&mut self,
|
||||
node_id: &NodeId,
|
||||
index: u32,
|
||||
field: EffectField,
|
||||
value: f32,
|
||||
) -> bool {
|
||||
if !node_id.is_real() || !value.is_finite() {
|
||||
return false;
|
||||
}
|
||||
let Some(node) = find_node_mut(self.active_children_mut(), node_id) else {
|
||||
return false;
|
||||
};
|
||||
let Some(slot) = node_effects_slot(node) else {
|
||||
return false;
|
||||
};
|
||||
let Some(effects) = slot.as_mut() else {
|
||||
return false;
|
||||
};
|
||||
let Some(effect) = effects.get_mut(index as usize) else {
|
||||
return false;
|
||||
};
|
||||
match (effect, field) {
|
||||
(PenEffect::Shadow(s), EffectField::OffsetX) => s.offset_x = value,
|
||||
(PenEffect::Shadow(s), EffectField::OffsetY) => s.offset_y = value,
|
||||
(PenEffect::Shadow(s), EffectField::Blur) => s.blur = value.max(0.0),
|
||||
(PenEffect::Shadow(s), EffectField::Spread) => s.spread = value,
|
||||
(PenEffect::Blur(b), EffectField::Radius)
|
||||
| (PenEffect::BackgroundBlur(b), EffectField::Radius) => b.radius = value.max(0.0),
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ pub use agent_settings::{
|
|||
pub use align::AlignAction;
|
||||
pub use chat::{AgentProvider, ChatAnchor, ChatMessage, ChatRole, ChatState, ModelEntry};
|
||||
pub use color_picker::{hsv_to_rgb, parse_hex_rgb, rgb_to_hex, rgb_to_hsv};
|
||||
pub use command::{BatchInsertItem, EditorCommand, NodeFlag, VariableScalarPayload};
|
||||
pub use command::{BatchInsertItem, EditorCommand, EffectField, NodeFlag, VariableScalarPayload};
|
||||
pub use components::{Component, ComponentLibrary};
|
||||
pub use editor_ui_state::{
|
||||
BooleanOp, EditorUiState, ExportFormat, FileAction, FileMenuChoice, FillType, FlexLayout,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ pub const PROPERTY_PANEL_WIDTH: f32 = 280.0;
|
|||
/// Button / checkbox actions in the property panel that don't
|
||||
/// map to a text input. The host dispatches these in `apply_press`
|
||||
/// after the text-input hit-test misses.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
///
|
||||
/// `PartialEq` only (not `Eq`) — `AdjustEffectParam` carries an `f32`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum PropertyPanelAction {
|
||||
SetFlexLayout(op_editor_core::FlexLayout),
|
||||
ToggleSizeFillWidth,
|
||||
|
|
@ -93,6 +95,15 @@ pub enum PropertyPanelAction {
|
|||
/// User clicked the "✕" on an effect row — host removes the
|
||||
/// effect at this index from the selected node.
|
||||
RemoveEffect(usize),
|
||||
/// User clicked a "−" / "+" stepper on an effect parameter row.
|
||||
/// `new_value` is the post-step value (the walker computed it
|
||||
/// from the current value ± the step); the host writes it via
|
||||
/// `EditorCommand::SetEffectParam`.
|
||||
AdjustEffectParam {
|
||||
effect: usize,
|
||||
field: op_editor_core::EffectField,
|
||||
new_value: f32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Per-NodeKind toggles for which property-panel sections render.
|
||||
|
|
@ -243,6 +254,19 @@ pub struct EffectSummary {
|
|||
}
|
||||
|
||||
impl EffectSummary {
|
||||
/// Current value of one editable parameter — Blur / BackgroundBlur
|
||||
/// keep their radius in `blur`, so `Blur` and `Radius` both read
|
||||
/// that field.
|
||||
pub fn param_value(&self, field: op_editor_core::EffectField) -> f32 {
|
||||
use op_editor_core::EffectField as F;
|
||||
match field {
|
||||
F::OffsetX => self.offset_x,
|
||||
F::OffsetY => self.offset_y,
|
||||
F::Blur | F::Radius => self.blur,
|
||||
F::Spread => self.spread,
|
||||
}
|
||||
}
|
||||
|
||||
/// Summarise a canonical `PenEffect` for the panel.
|
||||
fn from_pen_effect(e: &jian_ops_schema::style::PenEffect) -> Self {
|
||||
use jian_ops_schema::style::PenEffect;
|
||||
|
|
@ -532,13 +556,13 @@ impl PropertyPanel {
|
|||
fill: caps.fill,
|
||||
stroke: caps.stroke,
|
||||
effects: caps.effects,
|
||||
effect_count: self.snapshot.effects.len(),
|
||||
export: caps.export,
|
||||
fill_type: self.fill_type,
|
||||
};
|
||||
let rects = sections::action_button_rects_with_fill_picker(
|
||||
panel_rect,
|
||||
visible,
|
||||
&self.snapshot.effects,
|
||||
self.fill_type_picker_open,
|
||||
);
|
||||
// Picker rows live in `rects` AFTER the dropdown rect, so
|
||||
|
|
@ -570,7 +594,6 @@ impl PropertyPanel {
|
|||
fill: caps.fill,
|
||||
stroke: caps.stroke,
|
||||
effects: caps.effects,
|
||||
effect_count: self.snapshot.effects.len(),
|
||||
export: caps.export,
|
||||
fill_type: self.fill_type,
|
||||
};
|
||||
|
|
@ -733,7 +756,6 @@ impl Widget for PropertyPanel {
|
|||
fill: caps.fill,
|
||||
stroke: caps.stroke,
|
||||
effects: caps.effects,
|
||||
effect_count: self.snapshot.effects.len(),
|
||||
export: caps.export,
|
||||
fill_type: self.fill_type,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
//! Pulled out of `property_panel_sections.rs` to keep that file
|
||||
//! under the 800-line ceiling.
|
||||
|
||||
use crate::widgets::property_panel::PropertyPanelAction;
|
||||
use crate::widgets::property_panel::{EffectKind, EffectSummary, PropertyPanelAction};
|
||||
use crate::widgets::property_panel_inputs::{
|
||||
HEADER_HEIGHT, INPUT_HEIGHT, PAD_X, SECTION_GAP, SECTION_HEADER_HEIGHT, TAB_HEIGHT,
|
||||
};
|
||||
use crate::{Point2D, Rect};
|
||||
use op_editor_core::{FillType, FlexLayout, PropertyFocus};
|
||||
use op_editor_core::{EffectField, FillType, FlexLayout, PropertyFocus};
|
||||
|
||||
/// Whether each section currently paints — drives the layout
|
||||
/// walk so when per-kind filtering hides a section, the rects
|
||||
|
|
@ -26,15 +26,12 @@ pub struct VisibleSections {
|
|||
pub fill: bool,
|
||||
/// `StrokeHex` + `StrokeWidth` from the Stroke section.
|
||||
pub stroke: bool,
|
||||
/// Effects section paints (header + add chip + one row per
|
||||
/// Effects section paints (header + add chip + one block per
|
||||
/// effect). Tracked because the export-rect walker needs to know
|
||||
/// whether it consumed vertical space ahead of the Export
|
||||
/// section.
|
||||
/// section. The per-effect geometry is driven by the `effects`
|
||||
/// slice the walker takes alongside this struct.
|
||||
pub effects: bool,
|
||||
/// Number of effect rows the Effects section paints — drives the
|
||||
/// section's variable height + the per-row "✕" hit rects. 0 when
|
||||
/// the section is hidden or the node has no effects.
|
||||
pub effect_count: usize,
|
||||
/// Export section paints — `OpenExportDialog` action emits
|
||||
/// only when this is true.
|
||||
pub export: bool,
|
||||
|
|
@ -53,27 +50,52 @@ impl VisibleSections {
|
|||
fill: true,
|
||||
stroke: true,
|
||||
effects: true,
|
||||
effect_count: 0,
|
||||
export: true,
|
||||
fill_type: FillType::Solid,
|
||||
};
|
||||
}
|
||||
|
||||
/// Height (px) of one effect row in the Effects section — the type
|
||||
/// label + the "✕" remove button.
|
||||
/// Height (px) of an effect's header row — the type label + "✕".
|
||||
pub const EFFECT_ROW_HEIGHT: f32 = INPUT_HEIGHT + 4.0;
|
||||
|
||||
/// Total vertical space the Effects section consumes for
|
||||
/// `effect_count` effects: the section header plus one row per
|
||||
/// effect, or an 8 px filler when the node has none. Paint
|
||||
/// (`paint_effects_section`) and the layout walker both consult this
|
||||
/// so their y-math can never drift.
|
||||
pub fn effects_section_height(effect_count: usize) -> f32 {
|
||||
/// Height (px) of one effect-parameter row — label + value + the
|
||||
/// "−" / "+" steppers.
|
||||
pub const EFFECT_PARAM_ROW_HEIGHT: f32 = INPUT_HEIGHT + 2.0;
|
||||
|
||||
/// Doc-px a single "−" / "+" stepper click moves an effect parameter.
|
||||
pub const EFFECT_PARAM_STEP: f32 = 1.0;
|
||||
|
||||
/// The editable scalar params an effect kind exposes, in row order,
|
||||
/// each paired with its short row label. Shadow exposes four; the
|
||||
/// blur kinds expose a single radius.
|
||||
pub fn effect_param_fields(kind: EffectKind) -> &'static [(EffectField, &'static str)] {
|
||||
match kind {
|
||||
EffectKind::Shadow => &[
|
||||
(EffectField::OffsetX, "X"),
|
||||
(EffectField::OffsetY, "Y"),
|
||||
(EffectField::Blur, "Blur"),
|
||||
(EffectField::Spread, "Spread"),
|
||||
],
|
||||
EffectKind::Blur | EffectKind::BackgroundBlur => &[(EffectField::Radius, "Radius")],
|
||||
}
|
||||
}
|
||||
|
||||
/// Total height one effect block consumes — its header row plus one
|
||||
/// row per editable parameter.
|
||||
pub fn effect_block_height(kind: EffectKind) -> f32 {
|
||||
EFFECT_ROW_HEIGHT + effect_param_fields(kind).len() as f32 * EFFECT_PARAM_ROW_HEIGHT
|
||||
}
|
||||
|
||||
/// Total vertical space the Effects section consumes: the section
|
||||
/// header plus one block per effect (or an 8 px filler when the node
|
||||
/// has none). Paint (`paint_effects_section`) and the action-rect
|
||||
/// walker both consult this so their y-math can never drift.
|
||||
pub fn effects_section_height(effects: &[EffectSummary]) -> f32 {
|
||||
SECTION_HEADER_HEIGHT
|
||||
+ if effect_count == 0 {
|
||||
+ if effects.is_empty() {
|
||||
8.0
|
||||
} else {
|
||||
effect_count as f32 * EFFECT_ROW_HEIGHT
|
||||
effects.iter().map(|e| effect_block_height(e.kind)).sum()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,16 +131,19 @@ pub fn fill_body_height(fill_type: FillType) -> f32 {
|
|||
pub fn action_button_rects(
|
||||
panel_rect: Rect,
|
||||
visible: VisibleSections,
|
||||
effects: &[EffectSummary],
|
||||
) -> Vec<(PropertyPanelAction, Rect)> {
|
||||
action_button_rects_with_fill_picker(panel_rect, visible, false)
|
||||
action_button_rects_with_fill_picker(panel_rect, visible, effects, false)
|
||||
}
|
||||
|
||||
/// Same as `action_button_rects` but `fill_picker_open == true`
|
||||
/// emits hit-rects for the 4 picker rows that overlay the Fill
|
||||
/// section.
|
||||
/// section. `effects` drives the Effects section's per-effect "✕"
|
||||
/// and parameter-stepper rects + that section's variable height.
|
||||
pub fn action_button_rects_with_fill_picker(
|
||||
panel_rect: Rect,
|
||||
visible: VisibleSections,
|
||||
effects: &[EffectSummary],
|
||||
fill_picker_open: bool,
|
||||
) -> Vec<(PropertyPanelAction, Rect)> {
|
||||
let x0 = panel_rect.origin.x;
|
||||
|
|
@ -263,30 +288,57 @@ pub fn action_button_rects_with_fill_picker(
|
|||
y += SECTION_GAP;
|
||||
}
|
||||
if visible.effects {
|
||||
// Mirrors `paint_effects_section`: header + one row per
|
||||
// effect. The header's "+" button (drawn by
|
||||
// `paint_section_label_with_add` at the right edge) maps to
|
||||
// an `AddEffect` action.
|
||||
// Mirrors `paint_effects_section`: header + one block per
|
||||
// effect (header row + parameter rows). The header's "+"
|
||||
// button maps to `AddEffect`.
|
||||
let plus = Rect {
|
||||
origin: Point2D::new(x0 + w - PAD_X - 22.0, y),
|
||||
size: Point2D::new(28.0, SECTION_HEADER_HEIGHT),
|
||||
};
|
||||
out.push((PropertyPanelAction::AddEffect, plus));
|
||||
y += SECTION_HEADER_HEIGHT;
|
||||
// One right-aligned "✕" per effect row → RemoveEffect(i).
|
||||
for i in 0..visible.effect_count {
|
||||
let row_y = y + i as f32 * EFFECT_ROW_HEIGHT;
|
||||
for (ei, eff) in effects.iter().enumerate() {
|
||||
// "✕" on the effect's header row → RemoveEffect.
|
||||
out.push((
|
||||
PropertyPanelAction::RemoveEffect(i),
|
||||
PropertyPanelAction::RemoveEffect(ei),
|
||||
Rect {
|
||||
origin: Point2D::new(x0 + w - PAD_X - 20.0, row_y + 2.0),
|
||||
origin: Point2D::new(x0 + w - PAD_X - 20.0, y + 2.0),
|
||||
size: Point2D::new(20.0, INPUT_HEIGHT),
|
||||
},
|
||||
));
|
||||
// One "−"/"+" stepper pair per editable parameter row.
|
||||
let mut py = y + EFFECT_ROW_HEIGHT;
|
||||
for &(field, _) in effect_param_fields(eff.kind) {
|
||||
let cur = eff.param_value(field);
|
||||
out.push((
|
||||
PropertyPanelAction::AdjustEffectParam {
|
||||
effect: ei,
|
||||
field,
|
||||
new_value: cur - EFFECT_PARAM_STEP,
|
||||
},
|
||||
Rect {
|
||||
origin: Point2D::new(x0 + w - PAD_X - 48.0, py + 3.0),
|
||||
size: Point2D::new(22.0, INPUT_HEIGHT - 6.0),
|
||||
},
|
||||
));
|
||||
out.push((
|
||||
PropertyPanelAction::AdjustEffectParam {
|
||||
effect: ei,
|
||||
field,
|
||||
new_value: cur + EFFECT_PARAM_STEP,
|
||||
},
|
||||
Rect {
|
||||
origin: Point2D::new(x0 + w - PAD_X - 22.0, py + 3.0),
|
||||
size: Point2D::new(22.0, INPUT_HEIGHT - 6.0),
|
||||
},
|
||||
));
|
||||
py += EFFECT_PARAM_ROW_HEIGHT;
|
||||
}
|
||||
y += effect_block_height(eff.kind);
|
||||
}
|
||||
if effects.is_empty() {
|
||||
y += 8.0;
|
||||
}
|
||||
// Advance past the rows — identical to effects_section_height
|
||||
// minus the header already consumed above.
|
||||
y += effects_section_height(visible.effect_count) - SECTION_HEADER_HEIGHT;
|
||||
y += SECTION_GAP;
|
||||
}
|
||||
if visible.export {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ use crate::widgets::property_panel_inputs::{
|
|||
paint_section_label, paint_section_label_with_add, to_jian_color, HEADER_HEIGHT, INPUT_HEIGHT,
|
||||
INPUT_RADIUS, PAD_X, SECTION_GAP, TAB_HEIGHT,
|
||||
};
|
||||
use crate::widgets::property_panel_layout::EFFECT_ROW_HEIGHT;
|
||||
use crate::widgets::property_panel_layout::{
|
||||
effect_param_fields, EFFECT_PARAM_ROW_HEIGHT, EFFECT_ROW_HEIGHT,
|
||||
};
|
||||
use crate::widgets::PaintCx;
|
||||
use crate::{Color, Point2D, Rect, TextLayout};
|
||||
use op_editor_core::PropertyFocus;
|
||||
|
|
@ -710,12 +712,67 @@ pub fn paint_effects_section(
|
|||
for eff in effects {
|
||||
paint_effect_row(cx, theme, eff, x, row_y, width);
|
||||
row_y += EFFECT_ROW_HEIGHT;
|
||||
for &(field, label) in effect_param_fields(eff.kind) {
|
||||
paint_effect_param_row(cx, theme, label, eff.param_value(field), x, row_y, width);
|
||||
row_y += EFFECT_PARAM_ROW_HEIGHT;
|
||||
}
|
||||
}
|
||||
}
|
||||
paint_section_divider(cx, theme, x, row_y, width);
|
||||
row_y + SECTION_GAP
|
||||
}
|
||||
|
||||
/// Paint one effect-parameter row: `<label> <value> [−] [+]`. The
|
||||
/// "−"/"+" stepper rects must match `action_button_rects`'s
|
||||
/// `AdjustEffectParam` rects exactly so paint + hit-test agree.
|
||||
fn paint_effect_param_row(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
label: &str,
|
||||
value: f32,
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
) {
|
||||
let label_layout = TextLayout::single_run(
|
||||
label,
|
||||
"system-ui",
|
||||
11.0,
|
||||
to_jian_color(theme.muted_foreground),
|
||||
Point2D::new(0.0, 0.0),
|
||||
);
|
||||
cx.backend
|
||||
.draw_text(&label_layout, Point2D::new(x + PAD_X + 4.0, y + 15.0));
|
||||
let value_text = format!("{value:.0}");
|
||||
let value_layout = TextLayout::single_run(
|
||||
&value_text,
|
||||
"system-ui",
|
||||
12.0,
|
||||
to_jian_color(theme.foreground),
|
||||
Point2D::new(0.0, 0.0),
|
||||
);
|
||||
cx.backend.draw_text(
|
||||
&value_layout,
|
||||
Point2D::new(x + width - PAD_X - 78.0, y + 15.0),
|
||||
);
|
||||
// "−" then "+" — geometry mirrors the `AdjustEffectParam` rects.
|
||||
for (icon, off) in [(Icon::Minus, 48.0_f32), (Icon::Plus, 22.0_f32)] {
|
||||
let r = Rect {
|
||||
origin: Point2D::new(x + width - PAD_X - off, y + 3.0),
|
||||
size: Point2D::new(22.0, INPUT_HEIGHT - 6.0),
|
||||
};
|
||||
cx.backend.fill_round_rect(r, 6.0, theme.muted);
|
||||
draw_icon(
|
||||
cx.backend,
|
||||
icon,
|
||||
Point2D::new(r.origin.x + 5.0, r.origin.y + (r.size.y - 12.0) / 2.0),
|
||||
12.0,
|
||||
theme.foreground,
|
||||
1.4,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint one effect row — the effect-type label on the left + a
|
||||
/// right-aligned "✕" remove glyph. The "✕" hit rect is emitted by
|
||||
/// `action_button_rects` as `RemoveEffect(index)`, so the glyph
|
||||
|
|
|
|||
|
|
@ -85,11 +85,15 @@ fn hit_test_action_export_section_returns_open_dialog() {
|
|||
fill: caps.fill,
|
||||
stroke: caps.stroke,
|
||||
effects: caps.effects,
|
||||
effect_count: panel.snapshot.effects.len(),
|
||||
export: caps.export,
|
||||
fill_type: panel.fill_type,
|
||||
};
|
||||
let rects = sections::action_button_rects_with_fill_picker(rect, visible, false);
|
||||
let rects = sections::action_button_rects_with_fill_picker(
|
||||
rect,
|
||||
visible,
|
||||
&panel.snapshot.effects,
|
||||
false,
|
||||
);
|
||||
let export_rect = rects
|
||||
.iter()
|
||||
.find(|(action, _)| matches!(action, PropertyPanelAction::OpenExportDialog))
|
||||
|
|
|
|||
|
|
@ -72,6 +72,24 @@ impl WidgetHostNative {
|
|||
});
|
||||
}
|
||||
}
|
||||
A::AdjustEffectParam {
|
||||
effect,
|
||||
field,
|
||||
new_value,
|
||||
} => {
|
||||
let id = self.editor_state.selection.anchor.clone();
|
||||
if id.is_real() {
|
||||
self.editor_state.commit_history();
|
||||
let _ =
|
||||
self.editor_state
|
||||
.apply(op_editor_core::EditorCommand::SetEffectParam {
|
||||
node_id: id,
|
||||
index: effect as u32,
|
||||
field,
|
||||
value: new_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,24 @@ impl WidgetHost {
|
|||
});
|
||||
}
|
||||
}
|
||||
A::AdjustEffectParam {
|
||||
effect,
|
||||
field,
|
||||
new_value,
|
||||
} => {
|
||||
let id = self.editor_state.selection.anchor.clone();
|
||||
if id.is_real() {
|
||||
self.editor_state.commit_history();
|
||||
let _ =
|
||||
self.editor_state
|
||||
.apply(op_editor_core::EditorCommand::SetEffectParam {
|
||||
node_id: id,
|
||||
index: effect as u32,
|
||||
field,
|
||||
value: new_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue