diff --git a/crates/openpencil-shell-core/src/widgets/icons.rs b/crates/openpencil-shell-core/src/widgets/icons.rs index 9d14cb526..01b2ccc26 100644 --- a/crates/openpencil-shell-core/src/widgets/icons.rs +++ b/crates/openpencil-shell-core/src/widgets/icons.rs @@ -77,6 +77,24 @@ pub enum Icon { ChevronUp, /// MessageSquare — chat bubble icon (collapsed AI chat pill). MessageSquare, + /// LayoutGrid — 2×2 grid layout-mode (弹性布局 / RightPanel). + LayoutGrid, + /// Rows3 — horizontal rows layout-mode. + Rows3, + /// Columns3 — vertical columns layout-mode. + Columns3, + /// RotateCw — rotation handle for 位置 section. + RotateCw, + /// Diamond — "create component" button glyph. + Diamond, + /// Component — component instance indicator. + Component, + /// Unlink — detach component instance. + Unlink, + /// Check — checkbox tick. + Check, + /// ArrowUpRight — link / external nav indicator. + ArrowUpRight, } impl Icon { @@ -109,6 +127,15 @@ impl Icon { Icon::Close => CLOSE, Icon::ChevronUp => CHEVRON_UP, Icon::MessageSquare => MESSAGE_SQUARE, + Icon::LayoutGrid => LAYOUT_GRID, + Icon::Rows3 => ROWS_3, + Icon::Columns3 => COLUMNS_3, + Icon::RotateCw => ROTATE_CW, + Icon::Diamond => DIAMOND, + Icon::Component => COMPONENT, + Icon::Unlink => UNLINK, + Icon::Check => CHECK, + Icon::ArrowUpRight => ARROW_UP_RIGHT, } } } @@ -245,6 +272,63 @@ const MESSAGE_SQUARE: &[&str] = &[ "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z", ]; +// Lucide `layout-grid.svg` — 4 rounded-rect cells. +const LAYOUT_GRID: &[&str] = &[ + "M4 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z", + "M15 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z", + "M15 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z", + "M4 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z", +]; + +// Lucide `rows-3.svg` — round-rect with 2 horizontal dividers. +const ROWS_3: &[&str] = &[ + "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z", + "M21 9H3", + "M21 15H3", +]; + +// Lucide `columns-3.svg` — round-rect with 2 vertical dividers. +const COLUMNS_3: &[&str] = &[ + "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z", + "M9 3v18", + "M15 3v18", +]; + +// Lucide `rotate-cw.svg`. +const ROTATE_CW: &[&str] = &[ + "M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8", + "M21 3v5h-5", +]; + +// Lucide `diamond.svg`. +const DIAMOND: &[&str] = &[ + "M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41l-7.59-7.59a2.41 2.41 0 0 0-3.41 0Z", +]; + +// Lucide `component.svg`. +const COMPONENT: &[&str] = &[ + "M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z", + "M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z", + "M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z", + "M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z", +]; + +// Lucide `unlink.svg`. +const UNLINK: &[&str] = &[ + "m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71", + "m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71", + "M8 2L8 5", + "M2 8L5 8", + "M16 19L16 22", + "M19 16L22 16", +]; + +// Lucide `check.svg`. +const CHECK: &[&str] = &["M20 6 9 17l-5-5"]; + +// Lucide `arrow-up-right.svg`. +const ARROW_UP_RIGHT: &[&str] = &["M7 7h10v10", "M7 17 17 7"]; + /// Paint `icon` at `top_left`, scaled to `size × size` pixels, in /// `color` strokes of width `stroke_width`. The icon's lucide /// path data is rendered via `RenderBackend::stroke_svg_path`. @@ -351,6 +435,15 @@ mod tests { Icon::Close, Icon::ChevronUp, Icon::MessageSquare, + Icon::LayoutGrid, + Icon::Rows3, + Icon::Columns3, + Icon::RotateCw, + Icon::Diamond, + Icon::Component, + Icon::Unlink, + Icon::Check, + Icon::ArrowUpRight, ] { let b = paint_one(icon); assert!(b.paths > 0, "{:?} drew nothing", icon); diff --git a/crates/openpencil-shell-core/src/widgets/property_panel_sections.rs b/crates/openpencil-shell-core/src/widgets/property_panel_sections.rs index 187350086..0d24ea3e2 100644 --- a/crates/openpencil-shell-core/src/widgets/property_panel_sections.rs +++ b/crates/openpencil-shell-core/src/widgets/property_panel_sections.rs @@ -100,9 +100,12 @@ pub fn paint_create_component( cx.backend.fill_round_rect(btn_rect, 8.0, theme.muted); cx.backend .stroke_round_rect(btn_rect, 8.0, theme.border, 1.0); + // TS uses Diamond icon for the "create component" affordance + // (apps/web/src/components/panels/property-panel.tsx imports + // Diamond from lucide-react). Sparkles was a stand-in. draw_icon( cx.backend, - Icon::Sparkles, + Icon::Diamond, Point2D::new(btn_rect.origin.x + 12.0, btn_rect.origin.y + 9.0), 18.0, theme.foreground, @@ -160,6 +163,7 @@ pub fn paint_position_section( &snapshot.y.to_string(), ); y += INPUT_HEIGHT + 6.0; + // TS uses RotateCw for the rotation input (layout-section.tsx). paint_input_with_icon( cx, theme, @@ -167,7 +171,7 @@ pub fn paint_position_section( origin: Point2D::new(x + PAD_X, y), size: Point2D::new(half_w, INPUT_HEIGHT), }, - Icon::Redo, + Icon::RotateCw, "0", Some("°"), ); @@ -190,40 +194,40 @@ pub fn paint_position_section( pub fn paint_flex_section(cx: &mut PaintCx<'_>, theme: &Theme, x: f32, y: f32, width: f32) -> f32 { let mut y = paint_section_label(cx, theme, "弹性布局", x, y, width); + // TS layout-section.tsx uses Columns3 / Rows3 / LayoutGrid for + // the three flex modes; LayoutGrid is the default-active mode + // (Free / 自由布局). let btn_w = 56.0; let gap = 8.0; let row_x = x + PAD_X; - let active = Rect { - origin: Point2D::new(row_x, y), - size: Point2D::new(btn_w, 32.0), - }; - cx.backend.fill_round_rect(active, 6.0, theme.primary); - draw_icon( - cx.backend, - Icon::Square, - Point2D::new(active.origin.x + (btn_w - 18.0) / 2.0, active.origin.y + 7.0), - 18.0, - theme.primary_foreground, - 1.4, - ); - let mut bx = row_x + btn_w + gap; - for icon in [Icon::Frame, Icon::FolderOpen] { + let icons = [Icon::LayoutGrid, Icon::Rows3, Icon::Columns3]; + for (i, icon) in icons.iter().enumerate() { + let bx = row_x + i as f32 * (btn_w + gap); let rect = Rect { origin: Point2D::new(bx, y), size: Point2D::new(btn_w, 32.0), }; - cx.backend.fill_round_rect(rect, 6.0, theme.muted); - cx.backend - .stroke_round_rect(rect, 6.0, theme.border, 1.0); + let is_active = i == 0; + if is_active { + cx.backend.fill_round_rect(rect, 6.0, theme.primary); + } else { + cx.backend.fill_round_rect(rect, 6.0, theme.muted); + cx.backend + .stroke_round_rect(rect, 6.0, theme.border, 1.0); + } + let icon_color = if is_active { + theme.primary_foreground + } else { + theme.muted_foreground + }; draw_icon( cx.backend, - icon, + *icon, Point2D::new(rect.origin.x + (btn_w - 18.0) / 2.0, rect.origin.y + 7.0), 18.0, - theme.muted_foreground, + icon_color, 1.4, ); - bx += btn_w + gap; } y += 32.0 + 12.0; paint_section_divider(cx, theme, x, y, width);