Skip to main content

proof_engine/editor/
ui_canvas.rs

1
2//! UI canvas editor — widget hierarchy, layout engine, event system, theming.
3
4use glam::{Vec2, Vec4};
5use std::collections::HashMap;
6
7// ---------------------------------------------------------------------------
8// Layout primitives
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum Axis { Horizontal, Vertical }
13
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum FlexJustify { Start, End, Center, SpaceBetween, SpaceAround, SpaceEvenly }
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum FlexAlign { Start, End, Center, Stretch, Baseline }
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum FlexWrap { NoWrap, Wrap, WrapReverse }
20
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub enum PositionType { Relative, Absolute, Fixed, Sticky }
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum SizeUnit { Pixel, Percent, Auto, MinContent, MaxContent, FitContent }
26
27#[derive(Debug, Clone, Copy)]
28pub struct SizeValue {
29    pub value: f32,
30    pub unit: SizeUnit,
31}
32
33impl SizeValue {
34    pub fn px(v: f32) -> Self { Self { value: v, unit: SizeUnit::Pixel } }
35    pub fn pct(v: f32) -> Self { Self { value: v, unit: SizeUnit::Percent } }
36    pub fn auto() -> Self { Self { value: 0.0, unit: SizeUnit::Auto } }
37    pub fn resolve(&self, parent: f32) -> f32 {
38        match self.unit {
39            SizeUnit::Pixel => self.value,
40            SizeUnit::Percent => parent * self.value / 100.0,
41            _ => 0.0,
42        }
43    }
44}
45
46#[derive(Debug, Clone, Copy, Default)]
47pub struct Edges {
48    pub top: f32,
49    pub right: f32,
50    pub bottom: f32,
51    pub left: f32,
52}
53
54impl Edges {
55    pub fn all(v: f32) -> Self { Self { top: v, right: v, bottom: v, left: v } }
56    pub fn xy(x: f32, y: f32) -> Self { Self { top: y, right: x, bottom: y, left: x } }
57    pub fn horizontal(&self) -> f32 { self.left + self.right }
58    pub fn vertical(&self) -> f32 { self.top + self.bottom }
59}
60
61// ---------------------------------------------------------------------------
62// Style
63// ---------------------------------------------------------------------------
64
65#[derive(Debug, Clone, Copy, PartialEq)]
66pub enum Display { Flex, Grid, Block, Inline, InlineBlock, None }
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub enum Overflow { Visible, Hidden, Scroll, Auto }
69#[derive(Debug, Clone, Copy, PartialEq)]
70pub enum TextOverflow { Clip, Ellipsis, Scroll }
71#[derive(Debug, Clone, Copy, PartialEq, Default)]
72pub enum BorderStyle { #[default] None, Solid, Dashed, Dotted, Double, Groove, Ridge, Inset, Outset }
73#[derive(Debug, Clone, Copy, PartialEq)]
74pub enum BackgroundType { None, Solid, LinearGradient, RadialGradient, Image, Video }
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum CursorStyle { Default, Pointer, Text, Move, Crosshair, Wait, Help, EResize, NotAllowed }
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub enum Visibility { Visible, Hidden }
79
80#[derive(Debug, Clone)]
81pub struct Background {
82    pub background_type: BackgroundType,
83    pub color: Vec4,
84    pub gradient_from: Vec4,
85    pub gradient_to: Vec4,
86    pub gradient_angle: f32,
87    pub image_id: Option<u64>,
88    pub size_x: f32,
89    pub size_y: f32,
90    pub repeat: bool,
91}
92
93impl Default for Background {
94    fn default() -> Self {
95        Self {
96            background_type: BackgroundType::None,
97            color: Vec4::ZERO,
98            gradient_from: Vec4::ZERO,
99            gradient_to: Vec4::ONE,
100            gradient_angle: 0.0,
101            image_id: None,
102            size_x: 0.0,
103            size_y: 0.0,
104            repeat: false,
105        }
106    }
107}
108
109impl Background {
110    pub fn solid(color: Vec4) -> Self { Self { background_type: BackgroundType::Solid, color, ..Default::default() } }
111}
112
113#[derive(Debug, Clone, Default)]
114pub struct Border {
115    pub style: BorderStyle,
116    pub color: Vec4,
117    pub width: Edges,
118    pub radius: [f32; 4],
119}
120
121#[derive(Debug, Clone)]
122pub struct WidgetStyle {
123    pub display: Display,
124    pub position_type: PositionType,
125    pub overflow: Overflow,
126    pub overflow_x: Overflow,
127    pub overflow_y: Overflow,
128    pub visibility: Visibility,
129    pub opacity: f32,
130    pub z_index: i32,
131    pub width: SizeValue,
132    pub height: SizeValue,
133    pub min_width: SizeValue,
134    pub max_width: SizeValue,
135    pub min_height: SizeValue,
136    pub max_height: SizeValue,
137    pub margin: Edges,
138    pub padding: Edges,
139    pub top: SizeValue,
140    pub right: SizeValue,
141    pub bottom: SizeValue,
142    pub left: SizeValue,
143    pub flex_direction: Axis,
144    pub flex_wrap: FlexWrap,
145    pub justify_content: FlexJustify,
146    pub align_items: FlexAlign,
147    pub align_content: FlexAlign,
148    pub flex_grow: f32,
149    pub flex_shrink: f32,
150    pub flex_basis: SizeValue,
151    pub gap: Vec2,
152    pub background: Background,
153    pub border: Border,
154    pub cursor: CursorStyle,
155    pub font_size: f32,
156    pub font_family: String,
157    pub font_weight: u16,
158    pub font_italic: bool,
159    pub color: Vec4,
160    pub text_align: TextAlignH,
161    pub line_height: f32,
162    pub letter_spacing: f32,
163    pub word_spacing: f32,
164    pub text_decoration: TextDeco,
165    pub pointer_events: bool,
166    pub user_select: bool,
167    pub filter_blur: f32,
168    pub filter_brightness: f32,
169    pub transform_translate: Vec2,
170    pub transform_rotate: f32,
171    pub transform_scale: Vec2,
172    pub transition_duration: f32,
173    pub transition_easing: String,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq)]
177pub enum TextAlignH { Left, Center, Right, Justify }
178#[derive(Debug, Clone, Copy, PartialEq)]
179pub enum TextDeco { None, Underline, Overline, LineThrough }
180
181impl Default for WidgetStyle {
182    fn default() -> Self {
183        Self {
184            display: Display::Flex,
185            position_type: PositionType::Relative,
186            overflow: Overflow::Visible,
187            overflow_x: Overflow::Visible,
188            overflow_y: Overflow::Visible,
189            visibility: Visibility::Visible,
190            opacity: 1.0,
191            z_index: 0,
192            width: SizeValue::auto(),
193            height: SizeValue::auto(),
194            min_width: SizeValue::px(0.0),
195            max_width: SizeValue::px(f32::INFINITY),
196            min_height: SizeValue::px(0.0),
197            max_height: SizeValue::px(f32::INFINITY),
198            margin: Edges::all(0.0),
199            padding: Edges::all(0.0),
200            top: SizeValue::auto(),
201            right: SizeValue::auto(),
202            bottom: SizeValue::auto(),
203            left: SizeValue::auto(),
204            flex_direction: Axis::Horizontal,
205            flex_wrap: FlexWrap::NoWrap,
206            justify_content: FlexJustify::Start,
207            align_items: FlexAlign::Stretch,
208            align_content: FlexAlign::Start,
209            flex_grow: 0.0,
210            flex_shrink: 1.0,
211            flex_basis: SizeValue::auto(),
212            gap: Vec2::ZERO,
213            background: Background::default(),
214            border: Border::default(),
215            cursor: CursorStyle::Default,
216            font_size: 14.0,
217            font_family: "Inter".into(),
218            font_weight: 400,
219            font_italic: false,
220            color: Vec4::ONE,
221            text_align: TextAlignH::Left,
222            line_height: 1.4,
223            letter_spacing: 0.0,
224            word_spacing: 0.0,
225            text_decoration: TextDeco::None,
226            pointer_events: true,
227            user_select: true,
228            filter_blur: 0.0,
229            filter_brightness: 1.0,
230            transform_translate: Vec2::ZERO,
231            transform_rotate: 0.0,
232            transform_scale: Vec2::ONE,
233            transition_duration: 0.0,
234            transition_easing: "ease".into(),
235        }
236    }
237}
238
239// ---------------------------------------------------------------------------
240// Widgets
241// ---------------------------------------------------------------------------
242
243#[derive(Debug, Clone, Copy, PartialEq)]
244pub enum WidgetKind {
245    Container, Button, Label, Image, Input, Checkbox, Radio, Slider, Toggle,
246    Dropdown, Combobox, ProgressBar, Spinner, Tooltip, Modal, Drawer, Tabs,
247    ScrollView, ListView, GridView, TreeView, Table, Card, Divider,
248    Icon, Badge, Avatar, Chip, Tag, Rating, ColorPicker, DatePicker, TimePicker,
249    Breadcrumb, Pagination, Stepper, Accordion, Popover, Notification, Toast,
250    Sidebar, Navbar, Footer, Hero, Grid, Flex, Stack, Spacer, Placeholder,
251}
252
253impl WidgetKind {
254    pub fn label(self) -> &'static str {
255        match self {
256            WidgetKind::Container => "Container",
257            WidgetKind::Button => "Button",
258            WidgetKind::Label => "Label",
259            WidgetKind::Image => "Image",
260            WidgetKind::Input => "Input",
261            WidgetKind::Checkbox => "Checkbox",
262            WidgetKind::Radio => "Radio",
263            WidgetKind::Slider => "Slider",
264            WidgetKind::Toggle => "Toggle",
265            WidgetKind::Dropdown => "Dropdown",
266            WidgetKind::Combobox => "Combobox",
267            WidgetKind::ProgressBar => "Progress Bar",
268            WidgetKind::Spinner => "Spinner",
269            WidgetKind::Modal => "Modal",
270            WidgetKind::Tabs => "Tabs",
271            WidgetKind::ScrollView => "Scroll View",
272            WidgetKind::ListView => "List View",
273            WidgetKind::GridView => "Grid View",
274            WidgetKind::TreeView => "Tree View",
275            WidgetKind::Table => "Table",
276            WidgetKind::ColorPicker => "Color Picker",
277            WidgetKind::Accordion => "Accordion",
278            WidgetKind::Sidebar => "Sidebar",
279            WidgetKind::Navbar => "Navbar",
280            _ => "Widget",
281        }
282    }
283
284    pub fn category(self) -> &'static str {
285        match self {
286            WidgetKind::Container | WidgetKind::Grid | WidgetKind::Flex | WidgetKind::Stack => "Layout",
287            WidgetKind::Button | WidgetKind::Checkbox | WidgetKind::Radio | WidgetKind::Toggle | WidgetKind::Slider | WidgetKind::Dropdown | WidgetKind::Input => "Form",
288            WidgetKind::Label | WidgetKind::Icon | WidgetKind::Badge | WidgetKind::Avatar => "Display",
289            WidgetKind::ListView | WidgetKind::GridView | WidgetKind::TreeView | WidgetKind::Table | WidgetKind::ScrollView => "Data",
290            WidgetKind::Modal | WidgetKind::Tooltip | WidgetKind::Popover | WidgetKind::Notification | WidgetKind::Toast => "Overlay",
291            WidgetKind::Navbar | WidgetKind::Sidebar | WidgetKind::Footer | WidgetKind::Breadcrumb => "Navigation",
292            _ => "Other",
293        }
294    }
295}
296
297#[derive(Debug, Clone)]
298pub struct Widget {
299    pub id: u64,
300    pub name: String,
301    pub kind: WidgetKind,
302    pub style: WidgetStyle,
303    pub children: Vec<u64>,
304    pub parent: Option<u64>,
305    pub computed_rect: [Vec2; 2],
306    pub visible: bool,
307    pub enabled: bool,
308    pub focused: bool,
309    pub hovered: bool,
310    pub pressed: bool,
311    pub text: String,
312    pub placeholder: String,
313    pub value_float: f32,
314    pub value_min: f32,
315    pub value_max: f32,
316    pub value_checked: bool,
317    pub value_string: String,
318    pub image_id: Option<u64>,
319    pub icon_name: String,
320    pub variant: String,
321    pub custom_data: HashMap<String, String>,
322    pub event_handlers: Vec<String>,
323    pub animation_id: Option<u32>,
324}
325
326impl Widget {
327    pub fn new(id: u64, name: impl Into<String>, kind: WidgetKind) -> Self {
328        Self {
329            id, name: name.into(), kind,
330            style: WidgetStyle::default(),
331            children: Vec::new(), parent: None,
332            computed_rect: [Vec2::ZERO; 2],
333            visible: true, enabled: true, focused: false, hovered: false, pressed: false,
334            text: String::new(), placeholder: String::new(),
335            value_float: 0.0, value_min: 0.0, value_max: 1.0,
336            value_checked: false, value_string: String::new(),
337            image_id: None, icon_name: String::new(),
338            variant: "default".into(),
339            custom_data: HashMap::new(),
340            event_handlers: Vec::new(),
341            animation_id: None,
342        }
343    }
344
345    pub fn button(id: u64, name: impl Into<String>, label: impl Into<String>) -> Self {
346        let mut w = Widget::new(id, name, WidgetKind::Button);
347        w.text = label.into();
348        w.style.padding = Edges::xy(16.0, 8.0);
349        w.style.background = Background::solid(Vec4::new(0.2, 0.5, 1.0, 1.0));
350        w.style.border.radius = [4.0; 4];
351        w
352    }
353
354    pub fn label(id: u64, name: impl Into<String>, text: impl Into<String>) -> Self {
355        let mut w = Widget::new(id, name, WidgetKind::Label);
356        w.text = text.into();
357        w
358    }
359
360    pub fn width_px(&self) -> f32 { self.computed_rect[1].x - self.computed_rect[0].x }
361    pub fn height_px(&self) -> f32 { self.computed_rect[1].y - self.computed_rect[0].y }
362
363    pub fn contains(&self, p: Vec2) -> bool {
364        p.x >= self.computed_rect[0].x && p.x < self.computed_rect[1].x &&
365        p.y >= self.computed_rect[0].y && p.y < self.computed_rect[1].y
366    }
367
368    pub fn center(&self) -> Vec2 {
369        (self.computed_rect[0] + self.computed_rect[1]) * 0.5
370    }
371}
372
373// ---------------------------------------------------------------------------
374// Canvas (root document)
375// ---------------------------------------------------------------------------
376
377#[derive(Debug, Clone, Copy, PartialEq)]
378pub enum RenderMode { Screen, WorldSpace, CameraSpace }
379#[derive(Debug, Clone, Copy, PartialEq)]
380pub enum ScaleMode { ConstantPixelSize, ScaleWithScreenSize, ConstantPhysicalSize }
381
382#[derive(Debug, Clone)]
383pub struct UiCanvas {
384    pub widgets: Vec<Widget>,
385    pub root_ids: Vec<u64>,
386    pub width: f32,
387    pub height: f32,
388    pub render_mode: RenderMode,
389    pub scale_mode: ScaleMode,
390    pub reference_width: f32,
391    pub reference_height: f32,
392    pub match_width_or_height: f32,
393    pub pixels_per_unit: f32,
394    pub sort_order: i32,
395    pub next_id: u64,
396    pub theme: UiTheme,
397}
398
399impl UiCanvas {
400    pub fn new(width: f32, height: f32) -> Self {
401        let mut canvas = Self {
402            widgets: Vec::new(),
403            root_ids: Vec::new(),
404            width, height,
405            render_mode: RenderMode::Screen,
406            scale_mode: ScaleMode::ScaleWithScreenSize,
407            reference_width: 1920.0,
408            reference_height: 1080.0,
409            match_width_or_height: 0.5,
410            pixels_per_unit: 1.0,
411            sort_order: 0,
412            next_id: 1,
413            theme: UiTheme::default_dark(),
414        };
415        canvas.build_demo();
416        canvas
417    }
418
419    fn build_demo(&mut self) {
420        let canvas_w = self.width;
421        let canvas_h = self.height;
422        // Root container
423        let root_id = self.add_widget(Widget::new(0, "Root", WidgetKind::Container));
424        if let Some(w) = self.find_widget_mut(root_id) {
425            w.style.width = SizeValue::px(canvas_w);
426            w.style.height = SizeValue::px(canvas_h);
427            w.style.flex_direction = Axis::Vertical;
428            w.style.background = Background::solid(Vec4::new(0.1, 0.1, 0.12, 1.0));
429        }
430        // Navbar
431        let navbar = self.add_child_widget(root_id, Widget::new(0, "Navbar", WidgetKind::Navbar));
432        if let Some(w) = self.find_widget_mut(navbar) {
433            w.style.width = SizeValue::pct(100.0);
434            w.style.height = SizeValue::px(48.0);
435            w.style.background = Background::solid(Vec4::new(0.15, 0.15, 0.18, 1.0));
436            w.style.padding = Edges::xy(16.0, 0.0);
437            w.style.align_items = FlexAlign::Center;
438        }
439        // Main area
440        let main = self.add_child_widget(root_id, Widget::new(0, "Main", WidgetKind::Flex));
441        if let Some(w) = self.find_widget_mut(main) {
442            w.style.flex_grow = 1.0;
443            w.style.flex_direction = Axis::Horizontal;
444        }
445        // Sidebar
446        let sidebar = self.add_child_widget(main, Widget::new(0, "Sidebar", WidgetKind::Sidebar));
447        if let Some(w) = self.find_widget_mut(sidebar) {
448            w.style.width = SizeValue::px(240.0);
449            w.style.height = SizeValue::pct(100.0);
450            w.style.background = Background::solid(Vec4::new(0.13, 0.13, 0.15, 1.0));
451        }
452        // Content
453        let content = self.add_child_widget(main, Widget::new(0, "Content", WidgetKind::Container));
454        if let Some(w) = self.find_widget_mut(content) {
455            w.style.flex_grow = 1.0;
456            w.style.padding = Edges::all(24.0);
457            w.style.flex_direction = Axis::Vertical;
458            w.style.gap = Vec2::new(16.0, 16.0);
459        }
460        // Some buttons
461        let btn1 = Widget::button(0, "PrimaryButton", "Save Project");
462        self.add_child_widget(content, btn1);
463        let btn2 = Widget::button(0, "SecondaryButton", "Export");
464        self.add_child_widget(content, btn2);
465        let lbl = Widget::label(0, "TitleLabel", "Properties");
466        self.add_child_widget(content, lbl);
467    }
468
469    pub fn add_widget(&mut self, mut widget: Widget) -> u64 {
470        let id = self.next_id;
471        self.next_id += 1;
472        widget.id = id;
473        if widget.parent.is_none() {
474            self.root_ids.push(id);
475        }
476        self.widgets.push(widget);
477        id
478    }
479
480    pub fn add_child_widget(&mut self, parent_id: u64, mut widget: Widget) -> u64 {
481        let id = self.next_id;
482        self.next_id += 1;
483        widget.id = id;
484        widget.parent = Some(parent_id);
485        self.widgets.push(widget);
486        if let Some(parent) = self.find_widget_mut(parent_id) {
487            parent.children.push(id);
488        }
489        id
490    }
491
492    pub fn find_widget(&self, id: u64) -> Option<&Widget> {
493        self.widgets.iter().find(|w| w.id == id)
494    }
495
496    pub fn find_widget_mut(&mut self, id: u64) -> Option<&mut Widget> {
497        self.widgets.iter_mut().find(|w| w.id == id)
498    }
499
500    pub fn hit_test(&self, p: Vec2) -> Option<u64> {
501        // Test in reverse order (last = on top)
502        for w in self.widgets.iter().rev() {
503            if w.visible && w.enabled && w.style.pointer_events && w.contains(p) {
504                return Some(w.id);
505            }
506        }
507        None
508    }
509
510    pub fn widget_count(&self) -> usize { self.widgets.len() }
511}
512
513// ---------------------------------------------------------------------------
514// Theme
515// ---------------------------------------------------------------------------
516
517#[derive(Debug, Clone)]
518pub struct UiTheme {
519    pub name: String,
520    pub primary: Vec4,
521    pub secondary: Vec4,
522    pub success: Vec4,
523    pub warning: Vec4,
524    pub error: Vec4,
525    pub background: Vec4,
526    pub surface: Vec4,
527    pub on_background: Vec4,
528    pub on_surface: Vec4,
529    pub on_primary: Vec4,
530    pub border_color: Vec4,
531    pub border_radius: f32,
532    pub font_family: String,
533    pub font_size_sm: f32,
534    pub font_size_md: f32,
535    pub font_size_lg: f32,
536    pub spacing_unit: f32,
537    pub shadow_color: Vec4,
538    pub focus_ring_color: Vec4,
539    pub disabled_opacity: f32,
540}
541
542impl UiTheme {
543    pub fn default_dark() -> Self {
544        Self {
545            name: "Dark".into(),
546            primary: Vec4::new(0.24, 0.52, 1.0, 1.0),
547            secondary: Vec4::new(0.6, 0.4, 1.0, 1.0),
548            success: Vec4::new(0.2, 0.8, 0.4, 1.0),
549            warning: Vec4::new(1.0, 0.7, 0.2, 1.0),
550            error: Vec4::new(0.9, 0.3, 0.3, 1.0),
551            background: Vec4::new(0.1, 0.1, 0.12, 1.0),
552            surface: Vec4::new(0.15, 0.15, 0.18, 1.0),
553            on_background: Vec4::new(0.95, 0.95, 0.95, 1.0),
554            on_surface: Vec4::new(0.9, 0.9, 0.9, 1.0),
555            on_primary: Vec4::ONE,
556            border_color: Vec4::new(0.3, 0.3, 0.35, 1.0),
557            border_radius: 6.0,
558            font_family: "Inter".into(),
559            font_size_sm: 12.0,
560            font_size_md: 14.0,
561            font_size_lg: 18.0,
562            spacing_unit: 8.0,
563            shadow_color: Vec4::new(0.0, 0.0, 0.0, 0.4),
564            focus_ring_color: Vec4::new(0.24, 0.52, 1.0, 0.6),
565            disabled_opacity: 0.38,
566        }
567    }
568
569    pub fn default_light() -> Self {
570        let mut t = Self::default_dark();
571        t.name = "Light".into();
572        t.background = Vec4::new(0.97, 0.97, 0.97, 1.0);
573        t.surface = Vec4::ONE;
574        t.on_background = Vec4::new(0.1, 0.1, 0.1, 1.0);
575        t.on_surface = Vec4::new(0.15, 0.15, 0.15, 1.0);
576        t.border_color = Vec4::new(0.8, 0.8, 0.8, 1.0);
577        t
578    }
579
580    pub fn spacing(&self, n: f32) -> f32 { self.spacing_unit * n }
581}
582
583// ---------------------------------------------------------------------------
584// UI canvas editor
585// ---------------------------------------------------------------------------
586
587#[derive(Debug, Clone, Copy, PartialEq)]
588pub enum UiEditorTool { Select, Pan, Rect, Text, Image, InsertWidget }
589#[derive(Debug, Clone, Copy, PartialEq)]
590pub enum UiEditorPanel { Canvas, Properties, Hierarchy, Components, Theme, Preview }
591
592#[derive(Debug, Clone)]
593pub struct UiCanvasEditor {
594    pub canvas: UiCanvas,
595    pub selected_widgets: Vec<u64>,
596    pub tool: UiEditorTool,
597    pub active_panel: UiEditorPanel,
598    pub zoom: f32,
599    pub pan: Vec2,
600    pub show_grid: bool,
601    pub grid_size: f32,
602    pub snap_to_grid: bool,
603    pub show_ruler: bool,
604    pub show_guides: bool,
605    pub guides: Vec<(bool, f32)>,  // (horizontal, position)
606    pub theme_names: Vec<String>,
607    pub active_theme: usize,
608    pub themes: Vec<UiTheme>,
609    pub history: Vec<Vec<Widget>>,
610    pub history_pos: usize,
611    pub search_query: String,
612    pub preview_responsive: bool,
613    pub preview_width: f32,
614    pub preview_height: f32,
615    pub show_layout_debug: bool,
616    pub animate_transitions: bool,
617}
618
619impl UiCanvasEditor {
620    pub fn new() -> Self {
621        let canvas = UiCanvas::new(1920.0, 1080.0);
622        let themes = vec![UiTheme::default_dark(), UiTheme::default_light()];
623        let theme_names: Vec<String> = themes.iter().map(|t| t.name.clone()).collect();
624        Self {
625            canvas,
626            selected_widgets: Vec::new(),
627            tool: UiEditorTool::Select,
628            active_panel: UiEditorPanel::Canvas,
629            zoom: 1.0,
630            pan: Vec2::ZERO,
631            show_grid: true,
632            grid_size: 8.0,
633            snap_to_grid: false,
634            show_ruler: true,
635            show_guides: true,
636            guides: Vec::new(),
637            theme_names,
638            active_theme: 0,
639            themes,
640            history: Vec::new(),
641            history_pos: 0,
642            search_query: String::new(),
643            preview_responsive: false,
644            preview_width: 1920.0,
645            preview_height: 1080.0,
646            show_layout_debug: false,
647            animate_transitions: true,
648        }
649    }
650
651    pub fn select(&mut self, id: u64) { self.selected_widgets = vec![id]; }
652    pub fn add_to_selection(&mut self, id: u64) {
653        if !self.selected_widgets.contains(&id) { self.selected_widgets.push(id); }
654    }
655    pub fn deselect_all(&mut self) { self.selected_widgets.clear(); }
656
657    pub fn selected_widget(&self) -> Option<&Widget> {
658        self.selected_widgets.first().and_then(|&id| self.canvas.find_widget(id))
659    }
660
661    pub fn snapshot(&mut self) {
662        self.history.truncate(self.history_pos);
663        self.history.push(self.canvas.widgets.clone());
664        self.history_pos = self.history.len();
665    }
666
667    pub fn undo(&mut self) {
668        if self.history_pos > 1 {
669            self.history_pos -= 1;
670            self.canvas.widgets = self.history[self.history_pos - 1].clone();
671        }
672    }
673
674    pub fn redo(&mut self) {
675        if self.history_pos < self.history.len() {
676            self.canvas.widgets = self.history[self.history_pos].clone();
677            self.history_pos += 1;
678        }
679    }
680
681    pub fn current_theme(&self) -> &UiTheme {
682        &self.themes[self.active_theme.min(self.themes.len() - 1)]
683    }
684
685    pub fn screen_to_canvas(&self, screen: Vec2) -> Vec2 {
686        (screen - self.pan) / self.zoom
687    }
688
689    pub fn canvas_to_screen(&self, canvas: Vec2) -> Vec2 {
690        canvas * self.zoom + self.pan
691    }
692
693    pub fn hit_test_canvas(&self, screen: Vec2) -> Option<u64> {
694        let canvas_pos = self.screen_to_canvas(screen);
695        self.canvas.hit_test(canvas_pos)
696    }
697
698    pub fn search_widgets(&self, query: &str) -> Vec<&Widget> {
699        let q = query.to_lowercase();
700        self.canvas.widgets.iter().filter(|w| {
701            w.name.to_lowercase().contains(&q) ||
702            w.kind.label().to_lowercase().contains(&q) ||
703            w.text.to_lowercase().contains(&q)
704        }).collect()
705    }
706
707    pub fn duplicate_selected(&mut self) {
708        if self.selected_widgets.is_empty() { return; }
709        self.snapshot();
710        let id = self.selected_widgets[0];
711        if let Some(widget) = self.canvas.find_widget(id).cloned() {
712            let parent_id = widget.parent;
713            let mut new_w = widget.clone();
714            let new_id = self.canvas.next_id;
715            new_w.id = new_id;
716            new_w.name = format!("{}_copy", new_w.name);
717            new_w.children.clear();
718            if let Some(pid) = parent_id {
719                self.canvas.add_child_widget(pid, new_w);
720            } else {
721                self.canvas.add_widget(new_w);
722            }
723            self.selected_widgets = vec![new_id];
724        }
725    }
726}
727
728// ---------------------------------------------------------------------------
729// Tests
730// ---------------------------------------------------------------------------
731#[cfg(test)]
732mod tests {
733    use super::*;
734
735    #[test]
736    fn test_size_value() {
737        let pct = SizeValue::pct(50.0);
738        assert!((pct.resolve(200.0) - 100.0).abs() < 1e-5);
739        let px = SizeValue::px(42.0);
740        assert!((px.resolve(100.0) - 42.0).abs() < 1e-5);
741    }
742
743    #[test]
744    fn test_canvas_creation() {
745        let canvas = UiCanvas::new(1920.0, 1080.0);
746        assert!(canvas.widget_count() > 0);
747    }
748
749    #[test]
750    fn test_widget_hierarchy() {
751        let mut canvas = UiCanvas::new(800.0, 600.0);
752        let parent = canvas.add_widget(Widget::new(0, "Parent", WidgetKind::Container));
753        let child = canvas.add_child_widget(parent, Widget::new(0, "Child", WidgetKind::Button));
754        assert_eq!(canvas.find_widget(child).unwrap().parent, Some(parent));
755        assert!(canvas.find_widget(parent).unwrap().children.contains(&child));
756    }
757
758    #[test]
759    fn test_theme() {
760        let theme = UiTheme::default_dark();
761        assert!((theme.spacing(2.0) - 16.0).abs() < 1e-5);
762    }
763
764    #[test]
765    fn test_editor() {
766        let mut ed = UiCanvasEditor::new();
767        assert!(ed.canvas.widget_count() > 0);
768        ed.snapshot();
769        ed.undo();
770    }
771}