feat(panels): PropertyPanel Export section click opens ExportDialog (Task #52)
Closes the "P3. PropertyPanel Export section: real buttons" task that
was left preview-only by the export chain (P1 raster export + P2
ExportDialog modal). The Export section paints two pill dropdowns
(scale + format) but no clicks were wired — opening the dialog
required Cmd+Shift+P or the File menu.
`property_panel_layout.rs`:
- `VisibleSections` grew `effects: bool` + `export: bool`, both
required for the action-button walker to compute the y-position
of the Export section. `ALL` const + every construction site
updated.
- `action_button_rects_with_fill_picker` now continues its y-walk
past Fill through Stroke + Effects sections (consuming the
paint-side heights so the rect math stays aligned), then emits
one `OpenExportDialog` rect spanning the full Export row.
Clicking anywhere on the row resolves to OpenExportDialog —
splitting scale + format into separate hit zones isn't worth
it because the ExportDialog modal owns both pickers.
- Fill-section walker now explicitly consumes the head row +
body + divider gap heights it had been implicit about. Without
this any section after Fill (Stroke / Effects / Export) sat at
the wrong y in the hit-test space.
`property_panel.rs`:
- The three `VisibleSections` construction sites (action hit-test,
input hit-test, fill-picker overlay paint) now thread
`caps.effects` + `caps.export`. Mechanical sed-based patch since
the three sites are identical.
The downstream wiring was already in place — `widget_host/
property_dispatch.rs` handles `A::OpenExportDialog` by queueing
`FileAction::ExportImage` so the desktop binary's save-dialog +
real raster export kicks in. P3 was the missing piece between user
intent (click the section) and the existing pipeline.
Tests (1 added):
- `hit_test_action_export_section_returns_open_dialog` — selects
the sample Frame (NodeId 10), walks `action_button_rects_with_
fill_picker` to find the OpenExportDialog rect, clicks its
center, asserts hit_test_action returns OpenExportDialog. This
is the rect-walker contract that paint + hit-test stay in sync
even when sections after Fill are visible.
Total shell-core tests: 251 (was 250).
This commit is contained in:
parent
c3d0bb30cd
commit
3adaad0ae6
|
|
@ -381,6 +381,8 @@ impl PropertyPanel {
|
|||
opacity: caps.opacity,
|
||||
fill: caps.fill,
|
||||
stroke: caps.stroke,
|
||||
effects: caps.effects,
|
||||
export: caps.export,
|
||||
fill_type: self.fill_type,
|
||||
};
|
||||
let rects = sections::action_button_rects_with_fill_picker(
|
||||
|
|
@ -416,6 +418,8 @@ impl PropertyPanel {
|
|||
opacity: caps.opacity,
|
||||
fill: caps.fill,
|
||||
stroke: caps.stroke,
|
||||
effects: caps.effects,
|
||||
export: caps.export,
|
||||
fill_type: self.fill_type,
|
||||
};
|
||||
for (focus, rect) in sections::editable_input_rects(panel_rect, visible) {
|
||||
|
|
@ -563,6 +567,8 @@ impl Widget for PropertyPanel {
|
|||
opacity: caps.opacity,
|
||||
fill: caps.fill,
|
||||
stroke: caps.stroke,
|
||||
effects: caps.effects,
|
||||
export: caps.export,
|
||||
fill_type: self.fill_type,
|
||||
};
|
||||
sections::paint_fill_type_picker(cx, &self.theme, rect, visible, self.fill_type);
|
||||
|
|
@ -637,6 +643,58 @@ mod tests {
|
|||
assert!(panel.snapshot.height > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_test_action_export_section_returns_open_dialog() {
|
||||
// Single-frame selection paints every section + Export.
|
||||
// The walker now extends through Stroke + Effects so the
|
||||
// Export row's hit-test rect resolves to OpenExportDialog.
|
||||
let mut doc = Document::sample();
|
||||
// NodeId 10 is the Frame at the root of the sample document
|
||||
// (mutators.rs::sample). Frame paints every section including
|
||||
// Stroke + Effects + Export so the walker has to advance past
|
||||
// all of them to reach the Export row.
|
||||
doc.set_single_selection(NodeId::new(10));
|
||||
let panel = PropertyPanel::for_selection(&doc).expect("frame panel");
|
||||
// Tall panel rect so every section fits without clipping.
|
||||
let rect = Rect {
|
||||
origin: Point2D::new(0.0, 0.0),
|
||||
size: Point2D::new(280.0, 1600.0),
|
||||
};
|
||||
// The Export section is the last section painted. Walk
|
||||
// the action-button rects looking for the OpenExportDialog
|
||||
// rect, then click its center and assert we get back the
|
||||
// OpenExportDialog action.
|
||||
let caps = SectionCapabilities::for_kind(&panel.snapshot.kind_variant);
|
||||
let visible = sections::VisibleSections {
|
||||
flex_layout: caps.flex_layout,
|
||||
size_options: caps.size_options,
|
||||
opacity: caps.opacity,
|
||||
fill: caps.fill,
|
||||
stroke: caps.stroke,
|
||||
effects: caps.effects,
|
||||
export: caps.export,
|
||||
fill_type: panel.fill_type,
|
||||
};
|
||||
let rects = sections::action_button_rects_with_fill_picker(rect, visible, false);
|
||||
let export_rect = rects
|
||||
.iter()
|
||||
.find(|(action, _)| {
|
||||
matches!(action, PropertyPanelAction::OpenExportDialog)
|
||||
})
|
||||
.map(|(_, r)| *r)
|
||||
.expect("export section must emit an OpenExportDialog rect");
|
||||
let center = Point2D::new(
|
||||
export_rect.origin.x + export_rect.size.x / 2.0,
|
||||
export_rect.origin.y + export_rect.size.y / 2.0,
|
||||
);
|
||||
let hit = panel.hit_test_action(rect, center);
|
||||
assert!(
|
||||
matches!(hit, Some(PropertyPanelAction::OpenExportDialog)),
|
||||
"click in Export section should resolve to OpenExportDialog, got {:?}",
|
||||
hit
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_color_hex_pads_to_six_chars() {
|
||||
use crate::widgets::property_panel_inputs::format_color_hex;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ pub struct VisibleSections {
|
|||
pub fill: bool,
|
||||
/// `StrokeHex` + `StrokeWidth` from the Stroke section.
|
||||
pub stroke: bool,
|
||||
/// Effects section paints (header + add chip, no inputs yet).
|
||||
/// Tracked because the export-rect walker needs to know
|
||||
/// whether it consumed vertical space ahead of the Export
|
||||
/// section.
|
||||
pub effects: bool,
|
||||
/// Export section paints — `OpenExportDialog` action emits
|
||||
/// only when this is true.
|
||||
pub export: bool,
|
||||
/// Active fill type — affects fill-section body height so
|
||||
/// the walk past Fill stays aligned with paint when the user
|
||||
/// flips Solid / Gradient / Image.
|
||||
|
|
@ -40,6 +48,8 @@ impl VisibleSections {
|
|||
opacity: true,
|
||||
fill: true,
|
||||
stroke: true,
|
||||
effects: true,
|
||||
export: true,
|
||||
fill_type: FillType::Solid,
|
||||
};
|
||||
}
|
||||
|
|
@ -216,7 +226,44 @@ pub fn action_button_rects_with_fill_picker(
|
|||
));
|
||||
}
|
||||
}
|
||||
// Consume the rest of the Fill section so subsequent
|
||||
// sections' y math stays aligned with paint. Mirrors the
|
||||
// y-walk in `paint_fill_section`: head row + body + divider.
|
||||
y += INPUT_HEIGHT + 6.0; // head row (swatch + dropdown + opacity + X)
|
||||
y += fill_body_height(visible.fill_type) - 6.0 + 12.0; // body + divider gap
|
||||
y += SECTION_GAP;
|
||||
}
|
||||
if visible.stroke {
|
||||
// Mirrors paint_stroke_section: header + hex/width row.
|
||||
y += SECTION_HEADER_HEIGHT;
|
||||
y += INPUT_HEIGHT + 12.0;
|
||||
y += SECTION_GAP;
|
||||
}
|
||||
if visible.effects {
|
||||
// Mirrors paint_effects_section: header + 8 px filler.
|
||||
y += SECTION_HEADER_HEIGHT;
|
||||
y += 8.0;
|
||||
y += SECTION_GAP;
|
||||
}
|
||||
if visible.export {
|
||||
// Mirrors paint_export_section: header + a single dropdown
|
||||
// row. We emit one `OpenExportDialog` rect covering both
|
||||
// the scale + format pills + the full label strip so
|
||||
// clicking anywhere in the section opens the dialog. The
|
||||
// ExportDialog modal owns the format/scale picker UI, so
|
||||
// segmenting the row into two separate pills isn't worth
|
||||
// the extra hit-test complexity yet.
|
||||
y += SECTION_HEADER_HEIGHT;
|
||||
let row = Rect {
|
||||
origin: Point2D::new(x0 + PAD_X, y),
|
||||
size: Point2D::new(usable_w, INPUT_HEIGHT),
|
||||
};
|
||||
out.push((PropertyPanelAction::OpenExportDialog, row));
|
||||
// No further sections after Export today, but maintain the
|
||||
// y advance for symmetry / future additions.
|
||||
y += INPUT_HEIGHT + 12.0;
|
||||
}
|
||||
let _ = y; // suppress unused-write lint if export is last
|
||||
|
||||
out
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue