openpencil/crates/op-editor-ui/src/widgets/mod.rs
Kayshen-X 130a9b14a3 fix(editor): guard flex children on drag + property-panel and desktop chrome fixes
Canvas / layout:
- Guard flex flow-children from free-drag and add a 4px drag threshold,
  so a first click no longer materializes x/y on a flow child and
  collapses the auto-layout frame.

Property panel:
- Gap/justify radios disable the gap input on space-between/around;
  compact gap rows with vertically-centered input text.
- Padding-mode gear popover hover; the pin is anchor-scoped so it no
  longer leaks into the next selection.
- Font-weight dropdown: full 100-900 numeric options + row hover.
- Stroke hex seeds the displayed swatch colour (not #000000); stroke
  width now persists on commit.

Desktop:
- Traffic-light inset now applies on launch (casement windowDidBecomeKey).
- File-drop overlay; git commit caret blink.

Refactor:
- Split property_dispatch / git_panel / property-panel tests under the
  800-line cap; reorganise panel tests by topic.
2026-05-31 01:04:12 +08:00

277 lines
10 KiB
Rust

//! Widget facade for shell-core (Step 1b §1.4 — widget logic lives here so
//! both shell-native and shell-web reuse it; only RenderBackend / DOM event
//! mapping / accesskit DOM mirror are platform-owned).
//!
//! Two layers in one module:
//!
//! - **Primitives** (Phase B1+B2): the four reusable building blocks
//! `TreeWidget` / `PropertyRow` / `Dropdown` / `TextInput`.
//! - **Compositions** (Step 2): `LayerPanel` / `PropertyPanel` /
//! `Toolbar` — view models built from an `op_editor_core::EditorState`
//! that compose the primitives into the actual editor UI surface.
//! These were briefly housed in a `chrome/` submodule; the name
//! collided with the higher-level "OP chrome = openpencil-shell"
//! architectural term, so they live alongside the primitives now
//! (every entry here `impl Widget`, primitives + compositions
//! alike).
use crate::{Point2D, Rect, RenderBackend};
/// Minimum width (in CSS / physical px) below which the editor-UI
/// host paints the Toolbar only and skips the LayerPanel /
/// CanvasViewport / PropertyPanel rails. Single canonical
/// definition consumed by both `openpencil-shell-web::WidgetHost`
/// and `openpencil-shell-native::WidgetHostNative` so they stay in
/// lock-step (codex Step 3 R1 BLOCK fix — was duplicated as
/// `const MIN_RAIL_WIDTH` in each host).
pub const MIN_RAIL_WIDTH: f32 = 80.0;
// Phase B primitives.
pub mod dropdown;
pub mod prop_row;
pub mod text_input;
pub mod tree;
// Step 2 compositions (built on top of the primitives, driven by
// `op_editor_core::EditorState`).
pub mod layer_context_menu;
pub mod layer_panel;
mod layer_panel_paint;
#[cfg(test)]
mod layer_panel_tests;
mod layer_panel_walkers;
pub mod property_panel;
pub mod property_panel_action;
pub mod property_panel_code;
pub mod property_panel_effects;
pub mod property_panel_export;
pub mod property_panel_fill;
pub mod property_panel_flex;
pub mod property_panel_icon;
#[cfg(test)]
mod property_panel_icon_tests;
pub mod property_panel_image_fill;
#[cfg(test)]
mod property_panel_image_fill_tests;
pub mod property_panel_image_node;
mod property_panel_image_preview;
pub mod property_panel_input_layout;
pub mod property_panel_inputs;
pub mod property_panel_layer;
pub mod property_panel_layout;
#[cfg(test)]
mod property_panel_multi_select_tests;
pub mod property_panel_sections;
pub mod property_panel_snapshot;
#[cfg(test)]
mod property_panel_test_support;
#[cfg(test)]
mod property_panel_tests;
pub mod property_panel_text;
pub mod property_panel_visibility;
pub mod toolbar;
// Step 3 — center canvas that renders document nodes as actual
// visual primitives (frame fills, rect strokes, text strings).
pub mod canvas_viewport;
mod canvas_viewport_image;
pub mod canvas_viewport_overlay;
pub mod canvas_viewport_paint;
// Phase 6 — shell-core-side `theme()` / `t()` derivations over
// `op_editor_core::EditorUiState` for widgets ported off `Document`.
pub mod editor_state_ext;
// Step 4 — icon glyph drawer for editor chrome (lucide-flavored line art).
pub mod icon_catalog;
pub mod icons;
#[cfg(test)]
mod icons_tests;
// Lucide d-string data — extracted as a sibling so `icons.rs` stays
// under the 800-line cap as more first-party glyphs are added.
mod icons_data;
// Step 5 — brand logos for agent provider cards (filled SVG paths,
// not lucide stroke art). Sourced verbatim from `apps/web/src/
// components/icons/*-logo.tsx`.
pub mod brand_icons;
// Step 4 — extra editor-chrome widgets (TS app parity).
pub mod agent_settings_acp;
pub mod agent_settings_builtin;
pub mod agent_settings_i18n;
pub mod agent_settings_images;
pub mod agent_settings_mcp;
pub mod agent_settings_panel;
#[cfg(test)]
mod agent_settings_panel_tests;
pub mod agent_settings_system;
mod ai_chat_checklist;
pub mod ai_chat_model_picker;
pub mod ai_chat_panel;
pub mod ai_chat_panel_controls;
pub mod ai_chat_panel_paint;
pub mod ai_chat_transcript;
pub(crate) mod ai_chat_transcript_steps;
pub(crate) mod ai_chat_transcript_tools;
pub mod align_toolbar;
pub mod color_picker;
pub mod component_browser_panel;
pub mod design_md_markdown;
pub mod design_md_panel;
pub mod export_dialog;
pub mod figma_import;
pub mod figma_import_progress;
pub mod file_drop_overlay;
pub mod file_menu;
pub mod git_panel;
mod git_panel_diff;
mod git_panel_empty;
mod git_panel_hit;
mod git_panel_menus;
mod git_panel_ready;
mod git_panel_remotes;
mod git_panel_resolve;
mod git_panel_status;
#[cfg(test)]
mod git_panel_tests;
mod git_panel_text;
pub mod icon_picker_panel;
pub mod locale_picker;
pub mod shape_picker;
pub mod status_bar;
pub mod top_bar;
mod top_bar_paint;
pub mod variables_panel;
pub use dropdown::{Dropdown, DropdownState};
pub use prop_row::PropertyRow;
pub use text_input::{TextInput, TextInputState};
pub use tree::{TreeItem, TreeWidget};
pub use layer_panel::{LayerItem, LayerPanel};
pub use property_panel::{FontWeightChoice, PropertyPanel, PropertyPanelAction};
pub use toolbar::Toolbar;
pub use canvas_viewport::{
arc_handle_positions, path_handle_positions, rotate_point, rotation_corner_at_point,
selection_handle_at_point, ArcHandle, CanvasViewport, SelectionHandle,
};
pub use icons::{draw_icon, draw_icon_catalog_entry, draw_icon_data, Icon, IconPathData};
pub use ai_chat_panel::{
AIChatHit, AIChatPlaceholder, AI_CHAT_COLLAPSED_HEIGHT, AI_CHAT_COLLAPSED_WIDTH,
AI_CHAT_HEIGHT, AI_CHAT_WIDTH,
};
pub use align_toolbar::{AlignToolbar, ALIGN_TOOLBAR_HEIGHT, ALIGN_TOOLBAR_WIDTH};
pub use component_browser_panel::{
ComponentBrowserHit, ComponentBrowserPanel, COMPONENT_BROWSER_PANEL_H,
COMPONENT_BROWSER_PANEL_W,
};
pub use design_md_panel::{DesignMdHit, DesignMdPanel, DESIGN_MD_PANEL_H, DESIGN_MD_PANEL_W};
pub use export_dialog::{ExportDialog, ExportDialogHit, ExportFormat};
pub use git_panel::{GitPanel, GitPanelHit, GIT_PANEL_INSET, GIT_PANEL_WIDTH};
pub use icon_picker_panel::{
IconPickerHit, IconPickerPanel, ICONIFY_LOAD_MORE_LIMIT, ICON_PICKER_PANEL_H,
ICON_PICKER_PANEL_W,
};
pub use locale_picker::{LocalePicker, LOCALE_PICKER_WIDTH};
pub use shape_picker::{ShapeChoice, ShapePicker, SHAPE_PICKER_WIDTH};
pub use status_bar::{StatusBar, STATUS_BAR_HEIGHT, STATUS_BAR_WIDTH};
pub use top_bar::{TopBar, TopBarHit, WindowControl, TOP_BAR_HEIGHT};
// Re-export panel/toolbar width constants + hit enums so the host
// can size them consistently and route hits.
pub use layer_panel::{DropPosition, DropTarget, LayerPanelHit, LAYER_PANEL_WIDTH};
pub use property_panel::PROPERTY_PANEL_WIDTH;
pub use toolbar::{ToolbarAction, ToolbarHit, TOOLBAR_WIDTH};
/// Stable identifier assigned by the widget host. Used by accesskit
/// (`accesskit::NodeId(WidgetId.0)`), the DOM mirror, and event routing.
///
/// `WidgetId(0)` is reserved for the root host node — see
/// [`ROOT_WIDGET_ID`]. Use [`WidgetId::new`] to construct non-root ids
/// with a debug-time check; the tuple constructor stays public so
/// `const`-context callers (e.g. test fixtures) and pattern matches keep
/// working.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WidgetId(pub u64);
/// The reserved root-host id. Phase C tree routing skips this id when
/// dispatching to widgets (the root is the implicit host frame). Made a
/// named constant so the convention is compiler-visible — see codex
/// Phase B1 review NIT-7.
pub const ROOT_WIDGET_ID: WidgetId = WidgetId(0);
impl WidgetId {
/// Constructs a non-root `WidgetId`. In debug builds, panics if the
/// caller tries to allocate id 0 (reserved for [`ROOT_WIDGET_ID`]);
/// in release the value is accepted as-is so production paths are
/// not punished for a host bug. Phase C tree routing should use this
/// constructor for any id derived from widget allocation.
#[inline]
pub const fn new(id: u64) -> Self {
debug_assert!(
id != 0,
"WidgetId::new(0) — id 0 is reserved for ROOT_WIDGET_ID"
);
Self(id)
}
}
/// Result of a `Widget::layout` call — the absolute rectangle the widget
/// occupies in its parent frame. Phase B keeps this minimal (no taffy yet);
/// Phase C / D may extend with taffy-style intrinsic sizing once the four
/// inspector widgets shake out their layout needs.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LayoutBox {
pub rect: Rect,
}
/// Frame-scoped paint context. Holds the active `RenderBackend` so widgets
/// can issue draw calls; the `&mut dyn` indirection lets shell-native +
/// shell-web share the same widget code without monomorphising over the
/// concrete backend type.
pub struct PaintCx<'a> {
pub backend: &'a mut dyn RenderBackend,
}
/// Layout-time context. Phase B passes the available width + the host's
/// dpi scale; later phases may add font metrics / theme tokens.
#[derive(Debug, Clone, Copy)]
pub struct LayoutCx {
pub available_width: f32,
pub dpi: f32,
}
/// The widget facade. Step 1b widgets are static — `paint` takes `&self`
/// and only sees the host-provided rect; mutable per-widget state lives in
/// dedicated `*State` structs (see B2). Future phases may add a `&mut self`
/// `event` method for input handling; Phase C wires DOM events in
/// shell-web and the trait surface gets extended in lockstep.
pub trait Widget {
/// Stable identifier (assigned by the host).
fn id(&self) -> WidgetId;
/// Compute the widget's layout in the given context. Pure — no
/// rendering side effects.
fn layout(&self, cx: &LayoutCx) -> LayoutBox;
/// Paint the widget into `rect` via `cx.backend`. The host is
/// responsible for placing the rect; the widget only paints relative
/// to it.
fn paint(&self, cx: &mut PaintCx<'_>, rect: Rect);
/// Generate the accesskit Node for this widget. Used by both the
/// shell-native accesskit_winit adapter (Step 1a) and the shell-web
/// DOM mirror (Phase D). The host assigns NodeIds from `WidgetId`.
fn access_node(&self) -> accesskit::Node;
}
/// Convenience constructor used by tests + B2 widget impls.
pub fn rect(x: f32, y: f32, width: f32, height: f32) -> Rect {
Rect {
origin: Point2D::new(x, y),
size: Point2D::new(width, height),
}
}