git.lucas.co / cce-layout-interface
page layout tool
git clone https://git.lucas.co/cce-layout-interface.git

src/main.rs (193.7K)

   1 use cce_ui::widget::ScrollRegion;
   2 use cce_ui::widget::scroll_motion::{Bounds, ScrollMotion, LINE_PX};
   3 use wayland_client::QueueHandle;
   4 use cce_ui::cosmic_text::FontSystem;
   5 use serde::{Serialize, Deserialize};
   6 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings, LineCap};
   7 use cce_ui::widget::{
   8     MouseButton, ElementState, MouseScrollDelta, KeyEvent, WidgetHost as UiElement,
   9     TextBox, Slider, TextLabel, Paginator, Button, Dropdown, Toggle, ColorSelector,
  10     Label, Spinbox, Key, FontSelector, PageSelector, MenuController
  11 };
  12 use cce_ui::layout::{plate_gap, plate_padding, root_plate_gap, root_plate_inset, RenderTarget, Section, UiFrame};
  13 
  14 /// The sidebar's width — a size, not a spacing, and the one place it is
  15 /// named: the paginator's rect, the canvas's left edge and the pointer
  16 /// handlers' sidebar/canvas split all derive from it (`sidebar_rect`,
  17 /// `canvas_rect`).
  18 const SIDEBAR_W: f32 = 280.0;
  19 
  20 pub struct PageContent {
  21     pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
  22     pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
  23     /// Scoped clip `[l, t, r, b]`: while set, recorded rects are CLAMPED to it
  24     /// (exact for this flat pipeline — every rect is a plain quad) and texts
  25     /// get it intersected into their bounds. Set around the recent-files row
  26     /// loop so partially visible rows render cut at the list viewport instead
  27     /// of bleeding out of the well (the toolkit ScrollRegion's intersection
  28     /// contract returns partial rows).
  29     pub active_clip: Option<[f32; 4]>,
  30 }
  31 
  32 impl PageContent {
  33     pub fn new() -> Self {
  34         Self {
  35             rects: Vec::new(),
  36             texts: Vec::new(),
  37             active_clip: None,
  38         }
  39     }
  40 
  41     fn clipped_bounds(&self, bounds: Option<[f32; 4]>) -> Option<[f32; 4]> {
  42         match (self.active_clip, bounds) {
  43             (Some(c), Some(b)) => Some([b[0].max(c[0]), b[1].max(c[1]), b[2].min(c[2]), b[3].min(c[3])]),
  44             (Some(c), None) => Some(c),
  45             (None, b) => b,
  46         }
  47     }
  48 }
  49 
  50 impl RenderTarget for PageContent {
  51     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
  52         let (mut x0, mut y0, mut x1, mut y1) = (x, y, x + w, y + h);
  53         if let Some(c) = self.active_clip {
  54             x0 = x0.max(c[0]);
  55             y0 = y0.max(c[1]);
  56             x1 = x1.min(c[2]);
  57             y1 = y1.min(c[3]);
  58             if x1 <= x0 || y1 <= y0 {
  59                 return;
  60             }
  61         }
  62         self.rects.push((color, x0, y0, x1 - x0, y1 - y0));
  63     }
  64 
  65     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
  66         let b = self.clipped_bounds(None);
  67         self.texts.push((content.to_string(), size, x, y, color, None, b));
  68     }
  69 
  70     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
  71         let b = self.clipped_bounds(None);
  72         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), b));
  73     }
  74 
  75     fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
  76         let b = self.clipped_bounds(bounds);
  77         self.texts.push((content.to_string(), size, x, y, color, None, b));
  78     }
  79 
  80     fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
  81         let b = self.clipped_bounds(bounds);
  82         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), b));
  83     }
  84 }
  85 
  86 
  87 
  88 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
  89 pub enum TextAlignH {
  90     Left,
  91     Center,
  92     Right,
  93 }
  94 
  95 impl Default for TextAlignH {
  96     fn default() -> Self {
  97         TextAlignH::Left
  98     }
  99 }
 100 
 101 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
 102 pub enum TextAlignV {
 103     Top,
 104     Middle,
 105     Bottom,
 106 }
 107 
 108 impl Default for TextAlignV {
 109     fn default() -> Self {
 110         TextAlignV::Top
 111     }
 112 }
 113 
 114 fn point_to_line_segment_distance(px: f32, py: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
 115     let dx = x2 - x1;
 116     let dy = y2 - y1;
 117     let len_sq = dx * dx + dy * dy;
 118     if len_sq < 0.001 {
 119         let dx_p = px - x1;
 120         let dy_p = py - y1;
 121         return (dx_p * dx_p + dy_p * dy_p).sqrt();
 122     }
 123     let t = (((px - x1) * dx + (py - y1) * dy) / len_sq).clamp(0.0, 1.0);
 124     let proj_x = x1 + t * dx;
 125     let proj_y = y1 + t * dy;
 126     let dx_p = px - proj_x;
 127     let dy_p = py - proj_y;
 128     (dx_p * dx_p + dy_p * dy_p).sqrt()
 129 }
 130 
 131 #[derive(Debug, Clone)]
 132 enum AppMessage {
 133     Exit,
 134     NewDocument,
 135     Open,
 136     Save,
 137     SaveAs,
 138     AddText,
 139     AddRectangle,
 140     AddBanner,
 141     AddVector,
 142 }
 143 
 144 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
 145 enum ShapeType {
 146     Rectangle,
 147     Banner,
 148 }
 149 
 150 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 151 #[serde(tag = "type")]
 152 enum Element {
 153     Text {
 154         text: String,
 155         x: f32,
 156         y: f32,
 157         w: f32,
 158         h: f32,
 159         font_size: f32,
 160         color: [f32; 4],
 161         font_family: String,
 162         #[serde(default, rename = "align")]
 163         align_h: TextAlignH,
 164         #[serde(default)]
 165         align_v: TextAlignV,
 166         #[serde(default = "default_multiline")]
 167         multiline: bool,
 168     },
 169     Shape {
 170         shape_type: ShapeType,
 171         x: f32,
 172         y: f32,
 173         w: f32,
 174         h: f32,
 175         color: [f32; 4],
 176     },
 177     Vector {
 178         x1: f32,
 179         y1: f32,
 180         x2: f32,
 181         y2: f32,
 182         stroke_width: f32,
 183         color: [f32; 4],
 184         #[serde(default = "default_line_cap")]
 185         line_cap: LineCap,
 186     },
 187 }
 188 
 189 fn default_line_cap() -> LineCap {
 190     LineCap::Arrow
 191 }
 192 
 193 fn default_grid_color() -> [f32; 4] {
 194     [0.20, 0.35, 0.60, 0.8]
 195 }
 196 
 197 fn default_grid_size() -> f32 {
 198     1.5
 199 }
 200 
 201 fn default_multiline() -> bool {
 202     true
 203 }
 204 
 205 fn default_margin_color() -> [f32; 4] {
 206     [0.20, 0.50, 0.85, 0.35]
 207 }
 208 
 209 fn default_margin_thickness() -> f32 {
 210     1.0
 211 }
 212 
 213 fn default_grid_enabled() -> bool {
 214     true
 215 }
 216 
 217 fn default_grid_units() -> usize {
 218     0
 219 }
 220 
 221 fn default_zoom() -> f32 {
 222     0.33333334
 223 }
 224 
 225 fn default_rulers_enabled() -> bool {
 226     false
 227 }
 228 
 229 fn default_ruler_units() -> usize {
 230     0
 231 }
 232 
 233 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 234 struct LayoutDocument {
 235     page_w: f32,
 236     page_h: f32,
 237     page_color: [f32; 4],
 238     margin_enabled: bool,
 239     margin_x: f32,
 240     margin_y: f32,
 241     word_processor_enabled: bool,
 242     wp_text: String,
 243     elements: Vec<Element>,
 244     #[serde(default = "default_grid_color")]
 245     grid_color: [f32; 4],
 246     #[serde(default = "default_grid_size")]
 247     grid_size: f32,
 248     #[serde(default = "default_margin_color")]
 249     margin_color: [f32; 4],
 250     #[serde(default = "default_margin_thickness")]
 251     margin_thickness: f32,
 252     #[serde(default = "default_grid_enabled")]
 253     grid_enabled: bool,
 254     #[serde(default = "default_grid_units")]
 255     grid_units: usize,
 256     #[serde(default = "default_zoom")]
 257     zoom: f32,
 258     #[serde(default = "default_rulers_enabled")]
 259     rulers_enabled: bool,
 260     #[serde(default = "default_ruler_units")]
 261     ruler_units: usize,
 262 }
 263 
 264 struct PagePreset {
 265     name: &'static str,
 266     w: f32,
 267     h: f32,
 268 }
 269 
 270 const PRESETS: &[PagePreset] = &[
 271     PagePreset { name: "Letter (8.5\" x 11\")", w: 510.0, h: 660.0 },
 272     PagePreset { name: "Legal (8.5\" x 14\")", w: 510.0, h: 840.0 },
 273     PagePreset { name: "Tabloid (11\" x 17\")", w: 660.0, h: 1020.0 },
 274     PagePreset { name: "Executive (7.25\" x 10.5\")", w: 435.0, h: 630.0 },
 275     PagePreset { name: "A3 (297 x 420 mm)", w: 701.0, h: 992.0 },
 276     PagePreset { name: "A4 (210 x 297 mm)", w: 496.0, h: 701.0 },
 277     PagePreset { name: "A5 (148 x 210 mm)", w: 350.0, h: 496.0 },
 278     PagePreset { name: "A6 (105 x 148 mm)", w: 248.0, h: 350.0 },
 279     PagePreset { name: "B5 (JIS) (182 x 257 mm)", w: 430.0, h: 607.0 },
 280     PagePreset { name: "Business Card (3.5\" x 2\")", w: 210.0, h: 120.0 },
 281     PagePreset { name: "Presentation (16:9)", w: 800.0, h: 450.0 },
 282     PagePreset { name: "Square Poster (12\" x 12\")", w: 720.0, h: 720.0 },
 283     PagePreset { name: "Custom", w: 510.0, h: 660.0 },
 284 ];
 285 
 286 /// App shortcuts, resolved once at startup from input.kdl
 287 /// (`cce-layout-interface` domain → `cce-ui` domain).
 288 struct LayoutKeys {
 289     save_document: String,
 290     delete_element: String,
 291 }
 292 
 293 impl LayoutKeys {
 294     fn load() -> Self {
 295         Self {
 296             save_document: cce_ui::input::app_chord("save_document", "ctrl+s"),
 297             delete_element: cce_ui::input::app_chord("delete_element", "delete"),
 298         }
 299     }
 300 }
 301 
 302 struct LayoutApp {
 303     keys: LayoutKeys,
 304     btn_new_doc: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 305     btn_open: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 306     recent_files: Vec<std::path::PathBuf>,
 307     recent_files_list: ScrollRegion,
 308     recent_files_buttons: Vec<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
 309     btn_exit: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 310     btn_save: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 311     btn_save_as: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 312     paginator: cce_ui::widget::Adapted<Paginator>,
 313     layer_buttons: Vec<Box<cce_ui::widget::Adapted<cce_ui::widget::Button>>>,
 314     elements: Vec<Element>,
 315     selected_idx: Option<usize>,
 316     dragging: Option<(usize, f32, f32)>, // Index, offset_x, offset_y
 317     grid_enabled: bool,
 318     width: u32,
 319     current_file_path: Option<std::path::PathBuf>,
 320     last_saved_document: LayoutDocument,
 321     height: u32,
 322     scale_factor: f64,
 323     // (text, size, x, y, color_u8, font, bounds, layout) — the frame's text as prim data.
 324     text_prims: Vec<(String, f32, f32, f32, [u8; 3], Option<String>, Option<[f32; 4]>, Option<cce_ui::scene::paint::TextLayout>)>,
 325     font_system: FontSystem,
 326     needs_rebuild: bool,
 327 
 328     // Page 0: Layout properties controls
 329     dropdown_presets: cce_ui::widget::Adapted<Dropdown>,
 330     slider_page_x: cce_ui::widget::Adapted<cce_ui::widget::Slider>,
 331     slider_page_y: cce_ui::widget::Adapted<cce_ui::widget::Slider>,
 332     page_color_selector: cce_ui::widget::Adapted<ColorSelector>,
 333     page_color: [f32; 4],
 334     toggle_margin: cce_ui::widget::Adapted<Toggle>,
 335     dropdown_margin_units: cce_ui::widget::Adapted<Dropdown>,
 336     slider_margin_x: cce_ui::widget::Adapted<cce_ui::widget::Slider>,
 337     slider_margin_y: cce_ui::widget::Adapted<cce_ui::widget::Slider>,
 338     margin_enabled: bool,
 339     margin_x: f32,
 340     margin_y: f32,
 341     toggle_word_processor: cce_ui::widget::Adapted<Toggle>,
 342     word_processor_enabled: bool,
 343     wp_text_box: cce_ui::widget::Adapted<TextBox>,
 344     sidebar_x: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
 345     sidebar_y: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
 346     sidebar_w: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
 347     sidebar_h: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
 348     sidebar_text: cce_ui::widget::Adapted<TextBox>,
 349     sidebar_size: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
 350     font_selector: cce_ui::widget::Adapted<FontSelector>,
 351     dropdown_text_align_h: cce_ui::widget::Adapted<Dropdown>,
 352     dropdown_text_align_v: cce_ui::widget::Adapted<Dropdown>,
 353     toggle_multiline: cce_ui::widget::Adapted<Toggle>,
 354     dropdown_align_h: cce_ui::widget::Adapted<Dropdown>,
 355     dropdown_align_v: cce_ui::widget::Adapted<Dropdown>,
 356     dropdown_line_cap: cce_ui::widget::Adapted<Dropdown>,
 357     wp_base_font_size: f32,
 358 
 359     slider_r: cce_ui::widget::Adapted<cce_ui::widget::Slider>,
 360     slider_g: cce_ui::widget::Adapted<cce_ui::widget::Slider>,
 361     slider_b: cce_ui::widget::Adapted<cce_ui::widget::Slider>,
 362     slider_zoom: cce_ui::widget::Adapted<cce_ui::widget::Slider>,
 363 
 364     // Page 1: Canvas settings controls
 365     toggle_grid: cce_ui::widget::Adapted<Toggle>,
 366     grid_color_selector: cce_ui::widget::Adapted<ColorSelector>,
 367     grid_color: [f32; 4],
 368     grid_size: f32,
 369     spinbox_grid_size: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
 370     dropdown_grid_units: cce_ui::widget::Adapted<Dropdown>,
 371     margin_color_selector: cce_ui::widget::Adapted<ColorSelector>,
 372     margin_color: [f32; 4],
 373     spinbox_margin_thickness: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
 374     margin_thickness: f32,
 375     btn_add_text: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 376     btn_add_rect: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 377     btn_add_banner: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 378     btn_add_vector: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 379 
 380     toggle_rulers: cce_ui::widget::Adapted<Toggle>,
 381     dropdown_units: cce_ui::widget::Adapted<Dropdown>,
 382 
 383     label_sel_status: cce_ui::widget::Adapted<cce_ui::widget::Label>,
 384     label_sel_desc1: cce_ui::widget::Adapted<cce_ui::widget::Label>,
 385     label_sel_desc2: cce_ui::widget::Adapted<cce_ui::widget::Label>,
 386     label_grid_snap: cce_ui::widget::Adapted<Toggle>,
 387     label_total_elements: cce_ui::widget::Adapted<cce_ui::widget::Label>,
 388 
 389     last_selected: Option<usize>,
 390     page_w: f32,
 391     page_h: f32,
 392     pan_x: f32,
 393     pan_y: f32,
 394     /// Drives `pan_x`/`pan_y` (the drawn values) from the wheel: notches
 395     /// glide, fingers track 1:1 and fling on the lift. Unbounded — the
 396     /// canvas pans freely. `tick_pan` carries the drawn values after it.
 397     pan_motion: ScrollMotion,
 398     sidebar_quads: Vec<(f32, f32, f32, f32, [f32; 4])>,
 399     ui_context: cce_ui::context::UiContext,
 400 }
 401 
 402 fn get_monitor_ppi() -> f32 {
 403     let fallback_ppi = 96.0;
 404     let Ok(entries) = std::fs::read_dir("/sys/class/drm") else {
 405         return fallback_ppi;
 406     };
 407     for entry in entries.flatten() {
 408         let path = entry.path();
 409         if path.is_dir() {
 410             let status_path = path.join("status");
 411             let edid_path = path.join("edid");
 412             let modes_path = path.join("modes");
 413             if status_path.exists() && edid_path.exists() && modes_path.exists() {
 414                 if let Ok(status) = std::fs::read_to_string(&status_path) {
 415                     if status.trim() == "connected" {
 416                         if let (Ok(edid_bytes), Ok(modes_str)) = (std::fs::read(&edid_path), std::fs::read_to_string(&modes_path)) {
 417                             if edid_bytes.len() >= 23 {
 418                                 let w_cm = edid_bytes[21] as f32;
 419                                 if w_cm > 0.0 {
 420                                     // Parse resolution from the first mode line (e.g., "3840x2400")
 421                                     if let Some(first_mode) = modes_str.lines().next() {
 422                                         if let Some(w_str) = first_mode.split('x').next() {
 423                                             if let Ok(w_px) = w_str.parse::<f32>() {
 424                                                 let ppi = (w_px * 2.54) / w_cm;
 425                                                 if ppi > 30.0 && ppi < 600.0 {
 426                                                     return ppi;
 427                                                 }
 428                                             }
 429                                         }
 430                                     }
 431                                 }
 432                             }
 433                         }
 434                     }
 435                 }
 436             }
 437         }
 438     }
 439     fallback_ppi
 440 }
 441 
 442 #[allow(dead_code)]
 443 fn get_system_fonts() -> Vec<String> {
 444     let mut fonts = Vec::new();
 445     if let Ok(output) = std::process::Command::new("fc-list")
 446         .arg(":")
 447         .arg("family")
 448         .output()
 449     {
 450         if let Ok(stdout) = String::from_utf8(output.stdout) {
 451             for line in stdout.lines() {
 452                 for part in line.split(',') {
 453                     let cleaned = part.replace('\\', "").trim().to_string();
 454                     if !cleaned.is_empty() {
 455                         fonts.push(cleaned);
 456                     }
 457                 }
 458             }
 459         }
 460     }
 461     fonts.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
 462     fonts.dedup();
 463     if fonts.is_empty() {
 464         fonts = vec![
 465             "Monospace".to_string(),
 466             "Sans-Serif".to_string(),
 467             "Serif".to_string(),
 468         ];
 469     }
 470     fonts
 471 }
 472 
 473 impl LayoutApp {
 474     fn load_recent_files() -> Vec<std::path::PathBuf> {
 475         cce_ui::config::load_recent_files()
 476             .into_iter()
 477             .map(std::path::PathBuf::from)
 478             .collect()
 479     }
 480 
 481     fn save_recent_files(files: &[std::path::PathBuf]) {
 482         let string_files: Vec<String> = files.iter().map(|p| p.to_string_lossy().to_string()).collect();
 483         cce_ui::config::save_recent_files(&string_files);
 484     }
 485 
 486     fn add_recent_file(&mut self, path: std::path::PathBuf) {
 487         self.recent_files.retain(|p| p != &path);
 488         self.recent_files.insert(0, path);
 489         self.recent_files.truncate(10);
 490         Self::save_recent_files(&self.recent_files);
 491         self.rebuild_recent_buttons();
 492     }
 493 
 494     fn rebuild_recent_buttons(&mut self) {
 495         // Widget ids are globally monotonic and never reused, so the buttons pushed below
 496         // register under NEW ids; the dropped ones would stay in the registry pointing at
 497         // freed memory, and the engine derefs the whole registry on every left press
 498         // (`close_popovers_missed_by_press`). Drop their registrations first.
 499         let stale: Vec<_> = self.recent_files_buttons.iter().map(|b| b.id()).collect();
 500         for id in stale {
 501             self.ui_context.unregister_widget(id);
 502         }
 503         self.recent_files_buttons.clear();
 504         for file in &self.recent_files {
 505             let label = file.file_name()
 506                 .map(|n| n.to_string_lossy().to_string())
 507                 .unwrap_or_else(|| file.to_string_lossy().to_string());
 508             let btn = Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(&label);
 509             self.recent_files_buttons.push(btn);
 510         }
 511     }
 512 
 513     fn save_document(&self, path: &std::path::Path) -> std::io::Result<()> {
 514         let doc = self.get_current_document();
 515         let file = std::fs::File::create(path)?;
 516         serde_json::to_writer_pretty(file, &doc)?;
 517         Ok(())
 518     }
 519 
 520     fn perform_save_as(&mut self) {
 521         let path_opt = std::process::Command::new("/home/lsgalante/.local/bin/cce-files")
 522             .arg("--save")
 523             .output()
 524             .or_else(|_| {
 525                 std::process::Command::new("cce-files")
 526                     .arg("--save")
 527                     .output()
 528             })
 529             .ok()
 530             .and_then(|output| {
 531                 if output.status.success() {
 532                     let stdout = String::from_utf8_lossy(&output.stdout);
 533                     let trimmed = stdout.trim();
 534                     if !trimmed.is_empty() {
 535                         Some(std::path::PathBuf::from(trimmed))
 536                     } else {
 537                         None
 538                     }
 539                 } else {
 540                     None
 541                 }
 542             });
 543 
 544         if let Some(path) = path_opt {
 545             if let Err(e) = self.save_document(&path) {
 546                 log::error!("Failed to save document: {}", e);
 547             } else {
 548                 log::info!("Successfully saved layout to {:?}", path);
 549                 self.current_file_path = Some(path.clone());
 550                 self.last_saved_document = self.get_current_document();
 551                 self.add_recent_file(path);
 552             }
 553         }
 554     }
 555 
 556     fn load_document(&mut self, path: &std::path::Path) -> std::io::Result<()> {
 557         let file = std::fs::File::open(path)?;
 558         let doc: LayoutDocument = serde_json::from_reader(file)?;
 559 
 560         self.page_w = doc.page_w;
 561         self.page_h = doc.page_h;
 562         self.page_color = doc.page_color;
 563         self.margin_enabled = doc.margin_enabled;
 564         self.margin_x = doc.margin_x;
 565         self.margin_y = doc.margin_y;
 566         self.word_processor_enabled = doc.word_processor_enabled;
 567         self.wp_text_box.text = doc.wp_text.clone();
 568         self.wp_text_box.edit_buffer = doc.wp_text;
 569         self.elements = doc.elements;
 570 
 571         self.toggle_margin.set_toggled(self.margin_enabled);
 572         self.toggle_word_processor.set_toggled(self.word_processor_enabled);
 573 
 574         let r = (self.page_color[0] * 255.0).clamp(0.0, 255.0) as u8;
 575         let g = (self.page_color[1] * 255.0).clamp(0.0, 255.0) as u8;
 576         let b = (self.page_color[2] * 255.0).clamp(0.0, 255.0) as u8;
 577         self.page_color_selector.color = [r, g, b];
 578 
 579         self.grid_color = doc.grid_color;
 580         self.grid_size = doc.grid_size;
 581         self.margin_color = doc.margin_color;
 582 
 583         let gr = (self.grid_color[0] * 255.0).clamp(0.0, 255.0) as u8;
 584         let gg = (self.grid_color[1] * 255.0).clamp(0.0, 255.0) as u8;
 585         let gb = (self.grid_color[2] * 255.0).clamp(0.0, 255.0) as u8;
 586         self.grid_color_selector.color = [gr, gg, gb];
 587         self.spinbox_grid_size.value = (self.grid_size * 10.0).round() as i32;
 588 
 589         let mr = (self.margin_color[0] * 255.0).clamp(0.0, 255.0) as u8;
 590         let mg = (self.margin_color[1] * 255.0).clamp(0.0, 255.0) as u8;
 591         let mb = (self.margin_color[2] * 255.0).clamp(0.0, 255.0) as u8;
 592         self.margin_color_selector.color = [mr, mg, mb];
 593 
 594         self.margin_thickness = doc.margin_thickness;
 595         self.spinbox_margin_thickness.value = self.margin_thickness.round() as i32;
 596 
 597         self.grid_enabled = doc.grid_enabled;
 598         self.toggle_grid.set_toggled(doc.grid_enabled);
 599         self.label_grid_snap.set_toggled(doc.grid_enabled);
 600         self.dropdown_grid_units.selected = doc.grid_units;
 601         self.slider_zoom.set_value(doc.zoom);
 602         self.toggle_rulers.set_toggled(doc.rulers_enabled);
 603         self.dropdown_units.selected = doc.ruler_units;
 604 
 605         let mut preset_idx = PRESETS.len() - 1; // Custom
 606         for (i, preset) in PRESETS.iter().enumerate() {
 607             if (preset.w - self.page_w).abs() < 1.0 && (preset.h - self.page_h).abs() < 1.0 {
 608                 preset_idx = i;
 609                 break;
 610             }
 611         }
 612         self.dropdown_presets.selected = preset_idx;
 613 
 614         self.sync_page_units();
 615         self.rebuild_layers_tab_widgets();
 616         self.sync_sidebar_fields();
 617         self.needs_rebuild = true;
 618 
 619         Ok(())
 620     }
 621 
 622     fn perform_open(&mut self) {
 623         let path_opt = std::process::Command::new("/home/lsgalante/.local/bin/cce-files")
 624             .arg("--select")
 625             .output()
 626             .or_else(|_| {
 627                 std::process::Command::new("cce-files")
 628                     .arg("--select")
 629                     .output()
 630             })
 631             .ok()
 632             .and_then(|output| {
 633                 if output.status.success() {
 634                     let stdout = String::from_utf8_lossy(&output.stdout);
 635                     let trimmed = stdout.trim();
 636                     if !trimmed.is_empty() {
 637                         Some(std::path::PathBuf::from(trimmed))
 638                     } else {
 639                         None
 640                     }
 641                 } else {
 642                     None
 643                 }
 644             });
 645 
 646         if let Some(path) = path_opt {
 647             self.selected_idx = None;
 648             self.dragging = None;
 649             self.pan_x = 0.0;
 650             self.pan_y = 0.0;
 651             if let Err(e) = self.load_document(&path) {
 652                 log::error!("Failed to load document: {}", e);
 653             } else {
 654                 log::info!("Successfully loaded layout from {:?}", path);
 655                 self.current_file_path = Some(path.clone());
 656                 self.last_saved_document = self.get_current_document();
 657                 self.add_recent_file(path);
 658             }
 659         }
 660     }
 661 
 662     fn sync_page_units(&mut self) {
 663         let unit_idx = self.dropdown_margin_units.selected;
 664         let px_per_unit = match unit_idx {
 665             0 => 1.0,           // Pixels
 666             1 => 60.0,          // Inches
 667             2 => 23.622047,     // Centimeters
 668             3 => 2.3622047,     // Millimeters
 669             _ => 1.0,
 670         };
 671         let max_val = 100.0 / px_per_unit;
 672         self.slider_margin_x.set_range(0.0, max_val);
 673         self.slider_margin_x.set_value(self.margin_x / 100.0);
 674 
 675         self.slider_margin_y.set_range(0.0, max_val);
 676         self.slider_margin_y.set_value(self.margin_y / 100.0);
 677 
 678         self.slider_page_x.set_range(100.0 / px_per_unit, 1500.0 / px_per_unit);
 679         self.slider_page_x.set_value((self.page_w - 100.0) / (1500.0 - 100.0));
 680 
 681         self.slider_page_y.set_range(100.0 / px_per_unit, 1500.0 / px_per_unit);
 682         self.slider_page_y.set_value((self.page_h - 100.0) / (1500.0 - 100.0));
 683 
 684         let unit_suffix = match unit_idx {
 685             0 => "px",
 686             1 => "in",
 687             2 => "cm",
 688             3 => "mm",
 689             _ => "px",
 690         };
 691         self.slider_margin_x.set_label(&format!("Margin Width (X) ({})", unit_suffix));
 692         self.slider_margin_y.set_label(&format!("Margin Height (Y) ({})", unit_suffix));
 693         self.slider_page_x.set_label(&format!("Page Width (X) ({})", unit_suffix));
 694         self.slider_page_y.set_label(&format!("Page Height (Y) ({})", unit_suffix));
 695     }
 696 
 697     fn render_zoom(&self) -> f32 {
 698         let ppi = get_monitor_ppi();
 699         let scale_factor = self.scale_factor as f32;
 700         let zoom_multiplier = ppi / (60.0 * scale_factor);
 701         self.slider_zoom.get_scaled_value() * zoom_multiplier
 702     }
 703 
 704     /// The sidebar's rect `(x, y, w, h)`: standing on the root plate, inset
 705     /// from the window edge on its three outer sides, `SIDEBAR_W` wide.
 706     fn sidebar_rect(&self) -> (f32, f32, f32, f32) {
 707         let inset = root_plate_inset();
 708         (inset, inset, SIDEBAR_W, self.height as f32 - 2.0 * inset)
 709     }
 710 
 711     /// The canvas's rect `(x, y, w, h)`: the sidebar's sibling on the root
 712     /// plate, one root gap to its right and inset from the other three edges.
 713     /// Paint and hit-testing both read it, so the two cannot drift.
 714     fn canvas_rect(&self) -> (f32, f32, f32, f32) {
 715         let inset = root_plate_inset();
 716         let x = inset + SIDEBAR_W + root_plate_gap();
 717         (x, inset, self.width as f32 - x - inset, self.height as f32 - 2.0 * inset)
 718     }
 719 
 720     /// Top-left of the page sheet: centred on the canvas at the render zoom,
 721     /// then panned.
 722     fn page_origin(&self) -> (f32, f32) {
 723         let zoom = self.render_zoom();
 724         let (cx, cy, cw, ch) = self.canvas_rect();
 725         (
 726             cx + (cw - self.page_w * zoom) / 2.0 + self.pan_x,
 727             cy + (ch - self.page_h * zoom) / 2.0 + self.pan_y,
 728         )
 729     }
 730 
 731     fn rebuild_text_items(&mut self) {
 732         let font_system = &mut self.font_system;
 733         let selected_page = self.paginator.selected_page();
 734         let word_processor_enabled = self.word_processor_enabled;
 735         let _ = cce_ui::scale::scale_factor();
 736 
 737         match selected_page {
 738             0 => {
 739                 self.btn_new_doc.prepare_text(font_system);
 740                 self.btn_open.prepare_text(font_system);
 741                 for btn in &mut self.recent_files_buttons {
 742                     btn.prepare_text(font_system);
 743                 }
 744                 self.btn_save.prepare_text(font_system);
 745                 self.btn_save_as.prepare_text(font_system);
 746                 self.btn_exit.prepare_text(font_system);
 747             }
 748             1 => {
 749                 self.dropdown_margin_units.prepare_text(font_system);
 750                 self.dropdown_presets.prepare_text(font_system);
 751                 self.slider_page_x.prepare_text(font_system);
 752                 self.slider_page_y.prepare_text(font_system);
 753                 self.page_color_selector.prepare_text(font_system);
 754                 self.toggle_margin.prepare_text(font_system);
 755                 self.toggle_word_processor.prepare_text(font_system);
 756                 self.slider_margin_x.prepare_text(font_system);
 757                 self.slider_margin_y.prepare_text(font_system);
 758             }
 759             2 => {
 760                 if word_processor_enabled {
 761                     self.font_selector.prepare_text(font_system);
 762                     self.sidebar_size.prepare_text(font_system);
 763                     self.slider_r.prepare_text(font_system);
 764                     self.slider_g.prepare_text(font_system);
 765                     self.slider_b.prepare_text(font_system);
 766                 } else if self.selected_idx.is_some() {
 767                     self.dropdown_align_h.prepare_text(font_system);
 768                     self.dropdown_align_v.prepare_text(font_system);
 769 
 770                     let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
 771                         Some(Element::Text { .. }) => (true, false),
 772                         Some(Element::Vector { .. }) => (false, true),
 773                         _ => (false, false),
 774                     };
 775                     if is_text {
 776                         self.sidebar_x.prepare_text(font_system);
 777                         self.sidebar_y.prepare_text(font_system);
 778                         self.sidebar_w.prepare_text(font_system);
 779                         self.sidebar_h.prepare_text(font_system);
 780                         self.sidebar_text.prepare_text(font_system);
 781                         self.font_selector.prepare_text(font_system);
 782                         self.toggle_multiline.prepare_text(font_system);
 783                         self.dropdown_text_align_h.prepare_text(font_system);
 784                         self.dropdown_text_align_v.prepare_text(font_system);
 785                         self.sidebar_size.prepare_text(font_system);
 786                         self.slider_r.prepare_text(font_system);
 787                         self.slider_g.prepare_text(font_system);
 788                         self.slider_b.prepare_text(font_system);
 789                     } else if is_vector {
 790                         self.sidebar_x.prepare_text(font_system);
 791                         self.sidebar_y.prepare_text(font_system);
 792                         self.sidebar_w.prepare_text(font_system);
 793                         self.sidebar_h.prepare_text(font_system);
 794                         self.sidebar_size.prepare_text(font_system);
 795                         self.dropdown_line_cap.prepare_text(font_system);
 796                         self.slider_r.prepare_text(font_system);
 797                         self.slider_g.prepare_text(font_system);
 798                         self.slider_b.prepare_text(font_system);
 799                     } else {
 800                         self.sidebar_x.prepare_text(font_system);
 801                         self.sidebar_y.prepare_text(font_system);
 802                         self.sidebar_w.prepare_text(font_system);
 803                         self.sidebar_h.prepare_text(font_system);
 804                         self.slider_r.prepare_text(font_system);
 805                         self.slider_g.prepare_text(font_system);
 806                         self.slider_b.prepare_text(font_system);
 807                     }
 808                 }
 809             }
 810             3 => {
 811                 self.btn_add_text.prepare_text(font_system);
 812                 self.btn_add_rect.prepare_text(font_system);
 813                 self.btn_add_banner.prepare_text(font_system);
 814                 self.btn_add_vector.prepare_text(font_system);
 815             }
 816             4 => {
 817                 self.toggle_grid.prepare_text(font_system);
 818                 self.grid_color_selector.prepare_text(font_system);
 819                 self.spinbox_grid_size.prepare_text(font_system);
 820                 self.label_grid_snap.prepare_text(font_system);
 821                 self.slider_zoom.prepare_text(font_system);
 822                 self.toggle_rulers.prepare_text(font_system);
 823                 self.dropdown_units.prepare_text(font_system);
 824                 self.margin_color_selector.prepare_text(font_system);
 825                 self.spinbox_margin_thickness.prepare_text(font_system);
 826                 self.dropdown_grid_units.prepare_text(font_system);
 827             }
 828             5 => {
 829                 for btn in &mut self.layer_buttons {
 830                     btn.prepare_text(font_system);
 831                 }
 832             }
 833             _ => {}
 834         }
 835 
 836         self.wp_text_box.prepare_text(font_system);
 837 
 838         self.sidebar_quads.clear();
 839         self.text_prims.clear();
 840 
 841         // 1. Populate sidebar page views into PageContent
 842         let mut pc = PageContent::new();
 843         let ui_frame = UiFrame::start(0.0);
 844 
 845         // The tab strip takes the left of the sidebar and the selected page's
 846         // sections the rest, starting one pane padding below the sidebar's top.
 847         let (side_x, side_y, side_w, _) = self.sidebar_rect();
 848         let strip_w = self.paginator.sidebar_w();
 849         let cx = side_x + strip_w;
 850         let cy = side_y + plate_padding();
 851         let cw = side_w - strip_w;
 852 
 853         match self.paginator.selected_page() {
 854             0 => {
 855                 let mut sec = Section::new(&mut pc, cx, cy, cw, "File Operations");
 856                 let col_w = sec.content_width();
 857                 sec.widget(&mut pc, &mut self.btn_new_doc, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 858                 sec.spacing(plate_gap());
 859                 sec.widget(&mut pc, &mut self.btn_open, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 860                 sec.spacing(plate_gap());
 861 
 862                 sec.text(&mut pc, "Recent Files", Section::DEFAULT_MARGIN_X, 0.0, 11.0, [0.83, 0.83, 0.83, 1.0]);
 863                 // TODO(style): the heading's line height, not a gap — the 11px
 864                 // label plus the room under it before the well.
 865                 sec.spacing(16.0);
 866 
 867                 let list_h = 100.0;
 868                 let list_x = sec.content_left();
 869                 let list_y = sec.ay();
 870                 self.recent_files_list.set_rect(list_x, list_y, col_w, list_h);
 871                 self.recent_files_list.push_prims(&mut pc);
 872                 sec.spacing(list_h);
 873 
 874                 self.recent_files_list.update_bounds(self.recent_files.len(), list_y, list_h);
 875 
 876                 let btn_h = 22.0;
 877                 // Rows hug the well's border by the same inset its scrollbar
 878                 // keeps from the frame, and leave the bar's lane on the right.
 879                 let hug = self.recent_files_list.edge_inset;
 880                 let inner_x = list_x + hug;
 881                 let inner_w = col_w - hug - (cce_ui::layout::scrollbar_width() + hug);
 882 
 883                 // `get_item_draw_y` returns PARTIALLY visible rows (toolkit
 884                 // ScrollRegion intersection contract); the scoped clip clamps
 885                 // their quads and bounds their labels to the list viewport, so
 886                 // an edge row renders cut instead of bleeding out of the well.
 887                 pc.active_clip = Some([list_x, list_y, list_x + col_w, list_y + list_h]);
 888                 for (idx, btn) in self.recent_files_buttons.iter_mut().enumerate() {
 889                     if let Some(draw_y) = self.recent_files_list.get_item_draw_y(idx, 0.0) {
 890                         cce_ui::layout::render_widget(&mut pc, btn, inner_x, draw_y, inner_w, btn_h, &mut self.ui_context);
 891                         // Hit rect = the VISIBLE sliver: the drawn geometry is
 892                         // already recorded (clamped by the clip above), so
 893                         // re-clamping the stored rect only affects hit-testing
 894                         // — without it a straddling row's hidden part would
 895                         // shadow the controls below the well.
 896                         let cy = draw_y.max(list_y);
 897                         let cb = (draw_y + btn_h).min(list_y + list_h);
 898                         if cb - cy < btn_h {
 899                             btn.set_rect(inner_x, cy, inner_w, (cb - cy).max(0.0));
 900                         }
 901                     } else {
 902                         btn.set_rect(-9999.0, -9999.0, 0.0, 0.0);
 903                     }
 904                 }
 905                 pc.active_clip = None;
 906 
 907                 if self.recent_files.is_empty() {
 908                     pc.text("No recent files", list_x + plate_padding(), list_y + plate_padding(), 11.0, [0.55, 0.55, 0.60, 1.0]);
 909                 }
 910 
 911                 sec.spacing(plate_gap());
 912                 sec.widget(&mut pc, &mut self.btn_save, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 913                 sec.spacing(plate_gap());
 914                 sec.widget(&mut pc, &mut self.btn_save_as, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 915                 sec.spacing(plate_gap());
 916                 sec.widget(&mut pc, &mut self.btn_exit, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 917                 sec.spacing(plate_gap());
 918                 sec.finish(&mut pc);
 919             }
 920             1 => {
 921                 let mut sec = Section::new(&mut pc, cx, cy, cw, "Document Size");
 922                 let col_w = sec.content_width();
 923                 sec.widget(&mut pc, &mut self.dropdown_presets, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 924                 sec.spacing(plate_gap());
 925                 sec.widget(&mut pc, &mut self.slider_page_x, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
 926                 sec.spacing(plate_gap());
 927                 sec.widget(&mut pc, &mut self.slider_page_y, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
 928                 sec.spacing(plate_gap());
 929                 sec.widget(&mut pc, &mut self.page_color_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 930                 sec.spacing(plate_gap());
 931                 let next_y = sec.finish(&mut pc);
 932 
 933                 let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Margins & Mode");
 934                 sec2.widget(&mut pc, &mut self.toggle_margin, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 935                 sec2.spacing(plate_gap());
 936                 sec2.widget(&mut pc, &mut self.dropdown_margin_units, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 937                 sec2.spacing(plate_gap());
 938                 sec2.widget(&mut pc, &mut self.slider_margin_x, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
 939                 sec2.spacing(plate_gap());
 940                 sec2.widget(&mut pc, &mut self.slider_margin_y, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
 941                 sec2.spacing(plate_gap());
 942                 sec2.widget(&mut pc, &mut self.toggle_word_processor, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 943                 sec2.spacing(plate_gap());
 944                 sec2.finish(&mut pc);
 945             }
 946             2 => {
 947                 if self.word_processor_enabled {
 948                     let mut sec = Section::new(&mut pc, cx, cy, cw, "Word Processor");
 949                     let col_w = sec.content_width();
 950                     sec.widget(&mut pc, &mut self.font_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 951                     sec.spacing(plate_gap());
 952                     sec.widget(&mut pc, &mut self.sidebar_size, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 953                     sec.spacing(plate_gap());
 954                     sec.widget(&mut pc, &mut self.slider_r, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
 955                     sec.spacing(plate_gap());
 956                     sec.widget(&mut pc, &mut self.slider_g, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
 957                     sec.spacing(plate_gap());
 958                     sec.widget(&mut pc, &mut self.slider_b, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
 959                     sec.spacing(plate_gap());
 960                     sec.finish(&mut pc);
 961                 } else if let Some(idx) = self.selected_idx {
 962                     let (is_text, is_vector) = match &self.elements[idx] {
 963                         Element::Text { .. } => (true, false),
 964                         Element::Vector { .. } => (false, true),
 965                         _ => (false, false),
 966                     };
 967 
 968                     let mut sec = Section::new(&mut pc, cx, cy, cw, "Geometry");
 969                     let col_w = sec.content_width();
 970                     sec.widget(&mut pc, &mut self.sidebar_x, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 971                     sec.spacing(plate_gap());
 972                     sec.widget(&mut pc, &mut self.sidebar_y, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 973                     sec.spacing(plate_gap());
 974                     sec.widget(&mut pc, &mut self.sidebar_w, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 975                     sec.spacing(plate_gap());
 976                     sec.widget(&mut pc, &mut self.sidebar_h, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 977                     sec.spacing(plate_gap());
 978                     let next_y = sec.finish(&mut pc);
 979 
 980                     let mut sec_align = Section::new(&mut pc, cx, next_y, cw, "Alignment");
 981                     sec_align.widget(&mut pc, &mut self.dropdown_align_h, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 982                     sec_align.spacing(plate_gap());
 983                     sec_align.widget(&mut pc, &mut self.dropdown_align_v, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 984                     let next_y = sec_align.finish(&mut pc);
 985 
 986                     if is_text {
 987                         let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Text Properties");
 988                         sec2.widget(&mut pc, &mut self.sidebar_text, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 989                         sec2.spacing(plate_gap());
 990                         sec2.widget(&mut pc, &mut self.toggle_multiline, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 991                         sec2.spacing(plate_gap());
 992                         sec2.widget(&mut pc, &mut self.font_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 993                         sec2.spacing(plate_gap());
 994                         sec2.widget(&mut pc, &mut self.sidebar_size, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
 995                         sec2.spacing(plate_gap());
 996                         sec2.widget(&mut pc, &mut self.slider_r, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
 997                         sec2.spacing(plate_gap());
 998                         sec2.widget(&mut pc, &mut self.slider_g, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
 999                         sec2.spacing(plate_gap());
1000                         sec2.widget(&mut pc, &mut self.slider_b, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1001                         sec2.spacing(plate_gap());
1002                         let next_y_align = sec2.finish(&mut pc);
1003 
1004                         let mut sec_text_align = Section::new(&mut pc, cx, next_y_align, cw, "Text Alignment");
1005                         sec_text_align.widget(&mut pc, &mut self.dropdown_text_align_h, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1006                         sec_text_align.spacing(plate_gap());
1007                         sec_text_align.widget(&mut pc, &mut self.dropdown_text_align_v, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1008                         sec_text_align.finish(&mut pc);
1009                     } else if is_vector {
1010                         let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Line Properties");
1011                         sec2.widget(&mut pc, &mut self.sidebar_size, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1012                         sec2.spacing(plate_gap());
1013                         sec2.widget(&mut pc, &mut self.dropdown_line_cap, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1014                         sec2.spacing(plate_gap());
1015                         sec2.widget(&mut pc, &mut self.slider_r, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1016                         sec2.spacing(plate_gap());
1017                         sec2.widget(&mut pc, &mut self.slider_g, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1018                         sec2.spacing(plate_gap());
1019                         sec2.widget(&mut pc, &mut self.slider_b, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1020                         sec2.spacing(plate_gap());
1021                         sec2.finish(&mut pc);
1022                     } else {
1023                         let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Fill Color");
1024                         sec2.widget(&mut pc, &mut self.slider_r, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1025                         sec2.spacing(plate_gap());
1026                         sec2.widget(&mut pc, &mut self.slider_g, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1027                         sec2.spacing(plate_gap());
1028                         sec2.widget(&mut pc, &mut self.slider_b, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1029                         sec2.spacing(plate_gap());
1030                         sec2.finish(&mut pc);
1031                     }
1032                 } else {
1033                     let mut sec = Section::new(&mut pc, cx, cy, cw, "Properties");
1034                     let col_w = sec.content_width();
1035                     sec.widget(&mut pc, &mut self.label_sel_status, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1036                     // style: deliberate — the lines of one caption, set tight,
1037                     // not siblings on the pane.
1038                     sec.spacing(8.0);
1039                     sec.widget(&mut pc, &mut self.label_sel_desc1, Section::DEFAULT_MARGIN_X, col_w, 14.0, &mut self.ui_context);
1040                     sec.spacing(6.0);
1041                     sec.widget(&mut pc, &mut self.label_sel_desc2, Section::DEFAULT_MARGIN_X, col_w, 14.0, &mut self.ui_context);
1042                     sec.spacing(plate_gap());
1043                     sec.finish(&mut pc);
1044                 }
1045             }
1046             3 => {
1047                 let mut sec = Section::new(&mut pc, cx, cy, cw, "Add Elements");
1048                 let col_w = sec.content_width();
1049                 sec.widget(&mut pc, &mut self.btn_add_text, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1050                 sec.spacing(plate_gap());
1051                 sec.widget(&mut pc, &mut self.btn_add_rect, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1052                 sec.spacing(plate_gap());
1053                 sec.widget(&mut pc, &mut self.btn_add_banner, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1054                 sec.spacing(plate_gap());
1055                 sec.widget(&mut pc, &mut self.btn_add_vector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1056                 sec.spacing(plate_gap());
1057                 sec.widget(&mut pc, &mut self.label_total_elements, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1058                 sec.spacing(plate_gap());
1059                 sec.finish(&mut pc);
1060             }
1061             4 => {
1062                 let mut sec = Section::new(&mut pc, cx, cy, cw, "Grid");
1063                 let col_w = sec.content_width();
1064                 sec.widget(&mut pc, &mut self.toggle_grid, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1065                 sec.spacing(plate_gap());
1066                 sec.widget(&mut pc, &mut self.grid_color_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1067                 sec.spacing(plate_gap());
1068                 sec.widget(&mut pc, &mut self.spinbox_grid_size, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1069                 sec.spacing(plate_gap());
1070                 sec.widget(&mut pc, &mut self.label_grid_snap, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1071                 sec.spacing(plate_gap());
1072                 sec.widget(&mut pc, &mut self.dropdown_grid_units, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1073                 sec.spacing(plate_gap());
1074                 let next_y = sec.finish(&mut pc);
1075 
1076                 let mut sec_zoom = Section::new(&mut pc, cx, next_y, cw, "Zoom");
1077                 sec_zoom.widget(&mut pc, &mut self.slider_zoom, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1078                 sec_zoom.spacing(plate_gap());
1079                 let next_y = sec_zoom.finish(&mut pc);
1080 
1081                 let mut sec_margins = Section::new(&mut pc, cx, next_y, cw, "Margins");
1082                 sec_margins.widget(&mut pc, &mut self.margin_color_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1083                 sec_margins.spacing(plate_gap());
1084                 sec_margins.widget(&mut pc, &mut self.spinbox_margin_thickness, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1085                 sec_margins.spacing(plate_gap());
1086                 let next_y = sec_margins.finish(&mut pc);
1087 
1088                 let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Rulers");
1089                 sec2.widget(&mut pc, &mut self.toggle_rulers, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1090                 sec2.spacing(plate_gap());
1091                 sec2.widget(&mut pc, &mut self.dropdown_units, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1092                 sec2.spacing(plate_gap());
1093                 sec2.finish(&mut pc);
1094             }
1095             5 => {
1096                 let mut sec = Section::new(&mut pc, cx, cy, cw, "Layers List");
1097                 let col_w = sec.content_width();
1098                 if self.layer_buttons.is_empty() {
1099                     let mut label_no_layers = Label::new("No elements found").with_color([0x83, 0x83, 0x8a]);
1100                     sec.widget(&mut pc, &mut label_no_layers, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
1101                     sec.spacing(plate_gap());
1102                 } else {
1103                     for btn in &mut self.layer_buttons {
1104                         sec.widget(&mut pc, &mut **btn, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
1105                         sec.spacing(plate_gap());
1106                     }
1107                 }
1108                 sec.finish(&mut pc);
1109             }
1110             _ => {}
1111         }
1112 
1113         ui_frame.finish(&mut pc);
1114 
1115         // Store the sidebar quads
1116         for (c, x, y, w, h) in pc.rects {
1117             self.sidebar_quads.push((x, y, w, h, c));
1118         }
1119 
1120         // Sidebar widget text as prims (shaped by the engine at render).
1121         for (text, size, x, y, color, font, bounds) in pc.texts {
1122             let c = [
1123                 (color[0] * 255.0).clamp(0.0, 255.0) as u8,
1124                 (color[1] * 255.0).clamp(0.0, 255.0) as u8,
1125                 (color[2] * 255.0).clamp(0.0, 255.0) as u8,
1126             ];
1127             self.text_prims.push((text, size, x, y, c, font, bounds, None));
1128         }
1129 
1130         if let Some(idx) = self.selected_idx {
1131             self.label_sel_status.set_text(&format!("Selected Element #{}", idx + 1));
1132             self.label_sel_status.set_color([0x3b, 0x82, 0xf6]);
1133             self.label_sel_desc1.set_text("");
1134             self.label_sel_desc2.set_text("");
1135         } else {
1136             self.label_sel_status.set_text("No Selection");
1137             self.label_sel_status.set_color([0x83, 0x83, 0x8a]);
1138             self.label_sel_desc1.set_text("Click canvas elements");
1139             self.label_sel_desc2.set_text("to edit properties.");
1140         }
1141 
1142         let snap_suffix = match self.dropdown_grid_units.selected {
1143             0 => "20px",
1144             1 => "0.25in",
1145             2 => "0.5cm",
1146             3 => "5mm",
1147             _ => "20px",
1148         };
1149         self.label_grid_snap.set_label(&format!(
1150             "Grid Snapping: {}",
1151             if self.grid_enabled {
1152                 format!("ON ({})", snap_suffix)
1153             } else {
1154                 "OFF".to_string()
1155             }
1156         ));
1157         self.label_total_elements.set_text(&format!("Total Elements: {}", self.elements.len()));
1158 
1159         let mut labels = Vec::new();
1160 
1161         let zoom = self.render_zoom();
1162         let (page_x, page_y) = self.page_origin();
1163 
1164         // 2. Paginator sidebar tabs: emitted in display_list via the paint walk.
1165 
1166         if self.toggle_rulers.toggled() {
1167             let unit_idx = self.dropdown_units.selected;
1168             let (px_per_unit, tick_step, label_step, precision) = match unit_idx {
1169                 0 => (1.0, 50.0, 100.0, 0),       // Pixels
1170                 1 => (60.0, 0.25, 1.0, 0),        // Inches
1171                 2 => (23.622047, 0.5, 1.0, 0),    // Centimeters
1172                 3 => (2.3622047, 5.0, 10.0, 0),   // Millimeters
1173                 _ => (1.0, 50.0, 100.0, 0),
1174             };
1175 
1176             // X Ruler Labels
1177             let max_unit_w = self.page_w / px_per_unit;
1178             let mut val = 0.0;
1179             while val <= max_unit_w + 0.001 {
1180                 let is_major = (val / label_step).round() * label_step;
1181                 if (val - is_major).abs() < 0.001 {
1182                     labels.push(TextLabel {
1183                         text: format!("{:.precision$}", val, precision = precision),
1184                         x: page_x + val * px_per_unit * zoom + 2.0,
1185                         y: page_y - 16.0,
1186                         font_size: 8.0,
1187                         color: [0xaa, 0xaa, 0xbb],
1188                     });
1189                 }
1190                 val += tick_step;
1191             }
1192 
1193             // Y Ruler Labels
1194             let max_unit_h = self.page_h / px_per_unit;
1195             let mut val = 0.0;
1196             while val <= max_unit_h + 0.001 {
1197                 let is_major = (val / label_step).round() * label_step;
1198                 if (val - is_major).abs() < 0.001 {
1199                     labels.push(TextLabel {
1200                         text: format!("{:.precision$}", val, precision = precision),
1201                         x: page_x - 18.0,
1202                         y: page_y + val * px_per_unit * zoom + 2.0,
1203                         font_size: 8.0,
1204                         color: [0xaa, 0xaa, 0xbb],
1205                     });
1206                 }
1207                 val += tick_step;
1208             }
1209         }
1210 
1211 
1212 
1213         // 5. Canvas Element Labels (drawn relative to the paper sheet)
1214         // TODO(style): the `8.0 * zoom` / `6.0 * zoom` label offsets and the
1215         // text box's `16.0`/`12.0` insets are page-space padding inside a
1216         // document element — they scale with the zoom, so they are content,
1217         // not UI spacing.
1218         if self.word_processor_enabled {
1219             // Word-processor text box: emitted in display_list via the paint walk.
1220         } else {
1221             for (idx, element) in self.elements.iter().enumerate() {
1222                 match element {
1223                     Element::Text { text, x, y, w, h, font_size, color, font_family, align_h, align_v, multiline } => {
1224                         let label_font_size = *font_size * zoom;
1225                         let label_color = [
1226                             (color[0] * 255.0).clamp(0.0, 255.0) as u8,
1227                             (color[1] * 255.0).clamp(0.0, 255.0) as u8,
1228                             (color[2] * 255.0).clamp(0.0, 255.0) as u8,
1229                         ];
1230 
1231                         // Boxed canvas text: the engine shapes it uncached with wrap +
1232                         // alignment (Phase 6aj / boxed-text prims) and applies the vertical
1233                         // offset from the shaped height.
1234                         let text_w = (*w * zoom - 16.0 * zoom).max(0.0);
1235                         let text_h = (*h * zoom - 12.0 * zoom).max(0.0);
1236                         let wrap = if *multiline { Some(text_w) } else { None };
1237                         let l_align_h = match align_h {
1238                             TextAlignH::Left => cce_ui::scene::paint::AlignH::Left,
1239                             TextAlignH::Center => cce_ui::scene::paint::AlignH::Center,
1240                             TextAlignH::Right => cce_ui::scene::paint::AlignH::Right,
1241                         };
1242                         let l_align_v = match align_v {
1243                             TextAlignV::Top => cce_ui::scene::paint::AlignV::Top,
1244                             TextAlignV::Middle => cce_ui::scene::paint::AlignV::Middle,
1245                             TextAlignV::Bottom => cce_ui::scene::paint::AlignV::Bottom,
1246                         };
1247                         let tl = cce_ui::scene::paint::TextLayout {
1248                             wrap_width: wrap,
1249                             box_height: text_h,
1250                             align_h: l_align_h,
1251                             align_v: l_align_v,
1252                         };
1253                         let label_x = page_x + *x * zoom + 8.0 * zoom;
1254                         let label_y = page_y + *y * zoom + 6.0 * zoom;
1255                         self.text_prims.push((text.clone(), label_font_size, label_x, label_y, label_color, Some(font_family.clone()), None, Some(tl)));
1256                     }
1257                     Element::Shape { shape_type, x, y, w: _, h: _, color: _ } => {
1258                         let type_str = match shape_type {
1259                             ShapeType::Rectangle => "Rectangle",
1260                             ShapeType::Banner => "Banner",
1261                         };
1262                         labels.push(TextLabel {
1263                             text: format!("{} #{}", type_str, idx + 1),
1264                             x: page_x + *x * zoom + 8.0 * zoom,
1265                             y: page_y + *y * zoom + 6.0 * zoom,
1266                             font_size: 11.0 * zoom,
1267                             color: [0xee, 0xee, 0xf5],
1268                         });
1269                     }
1270                     Element::Vector { x1, y1, .. } => {
1271                         labels.push(TextLabel {
1272                             text: format!("Line #{}", idx + 1),
1273                             x: page_x + *x1 * zoom + 8.0 * zoom,
1274                             y: page_y + *y1 * zoom + 6.0 * zoom,
1275                             font_size: 11.0 * zoom,
1276                             color: [0xee, 0xee, 0xf5],
1277                         });
1278                     }
1279                 }
1280             }
1281         }
1282 
1283         for label in labels {
1284             self.text_prims.push((label.text, label.font_size, label.x, label.y, label.color, None, None, None));
1285         }
1286     }
1287 
1288     fn rebuild_layers_tab_widgets(&mut self) {
1289         // See the note in rebuild_recent_buttons(): monotonic ids mean the outgoing buttons
1290         // must be unregistered or the registry keeps dangling pointers into freed boxes.
1291         let stale: Vec<_> = self.layer_buttons.iter().map(|b| b.id()).collect();
1292         for id in stale {
1293             self.ui_context.unregister_widget(id);
1294         }
1295         self.layer_buttons.clear();
1296         for (idx, element) in self.elements.iter().enumerate() {
1297             let label = match element {
1298                 Element::Text { text, .. } => {
1299                     let truncated: String = text.chars().take(20).collect();
1300                     format!("{}. Text: \"{}\"", idx + 1, truncated)
1301                 }
1302                 Element::Shape { shape_type, .. } => {
1303                     let type_str = match shape_type {
1304                         ShapeType::Rectangle => "Rectangle",
1305                         ShapeType::Banner => "Banner",
1306                     };
1307                     format!("{}. {}", idx + 1, type_str)
1308                 }
1309                 Element::Vector { x1, y1, x2, y2, .. } => {
1310                     format!("{}. Line: ({:.0},{:.0})->({:.0},{:.0})", idx + 1, x1, y1, x2, y2)
1311                 }
1312             };
1313 
1314             let mut btn = Box::new(Button::new_list_row(0.0, 0.0, 224.0, 26.0).with_label(&label));
1315             btn.selected = Some(idx) == self.selected_idx;
1316             self.layer_buttons.push(btn);
1317         }
1318     }
1319 
1320 
1321     fn sync_sidebar_fields(&mut self) {
1322         if self.word_processor_enabled {
1323             if !self.sidebar_size.editing {
1324                 self.sidebar_size.value = self.wp_base_font_size.round() as i32;
1325             }
1326             self.font_selector.font_family = self.wp_text_box.font_family.clone();
1327 
1328             if let Some(rgb) = self.wp_text_box.text_color {
1329                 self.slider_r.set_value(rgb[0] as f32 / 255.0);
1330                 self.slider_g.set_value(rgb[1] as f32 / 255.0);
1331                 self.slider_b.set_value(rgb[2] as f32 / 255.0);
1332             } else {
1333                 self.slider_r.set_value(0.1);
1334                 self.slider_g.set_value(0.1);
1335                 self.slider_b.set_value(0.14);
1336             }
1337         } else if let Some(idx) = self.selected_idx {
1338             if self.last_selected != Some(idx) {
1339                 self.sidebar_text.unfocus();
1340                 self.sidebar_x.unfocus();
1341                 self.sidebar_y.unfocus();
1342                 self.sidebar_w.unfocus();
1343                 self.sidebar_h.unfocus();
1344                 self.sidebar_size.unfocus();
1345                 self.last_selected = Some(idx);
1346             }
1347 
1348             let element = &self.elements[idx];
1349             match element {
1350                 Element::Text { text, x, y, w, h, font_size, color, font_family, align_h, align_v, multiline } => {
1351                     self.sidebar_x.base_mut().label = Some("X Position".to_string());
1352                     self.sidebar_y.base_mut().label = Some("Y Position".to_string());
1353                     self.sidebar_w.base_mut().label = Some("Width".to_string());
1354                     self.sidebar_h.base_mut().label = Some("Height".to_string());
1355                     self.sidebar_size.base_mut().label = Some("Text Size".to_string());
1356 
1357                     if !self.sidebar_text.editing { self.sidebar_text.text = text.clone(); }
1358                     if !self.sidebar_x.editing { self.sidebar_x.value = x.round() as i32; }
1359                     if !self.sidebar_y.editing { self.sidebar_y.value = y.round() as i32; }
1360                     if !self.sidebar_w.editing { self.sidebar_w.value = w.round() as i32; }
1361                     if !self.sidebar_h.editing { self.sidebar_h.value = h.round() as i32; }
1362                     if !self.sidebar_size.editing { self.sidebar_size.value = font_size.round() as i32; }
1363 
1364                     self.slider_r.set_value(color[0]);
1365                     self.slider_g.set_value(color[1]);
1366                     self.slider_b.set_value(color[2]);
1367 
1368                     self.font_selector.font_family = font_family.clone();
1369                     self.toggle_multiline.set_toggled(*multiline);
1370 
1371                     self.dropdown_text_align_h.selected = match align_h {
1372                         TextAlignH::Left => 0,
1373                         TextAlignH::Center => 1,
1374                         TextAlignH::Right => 2,
1375                     };
1376                     self.dropdown_text_align_v.selected = match align_v {
1377                         TextAlignV::Top => 0,
1378                         TextAlignV::Middle => 1,
1379                         TextAlignV::Bottom => 2,
1380                     };
1381                 }
1382                 Element::Shape { shape_type: _, x, y, w, h, color } => {
1383                     self.sidebar_x.base_mut().label = Some("X Position".to_string());
1384                     self.sidebar_y.base_mut().label = Some("Y Position".to_string());
1385                     self.sidebar_w.base_mut().label = Some("Width".to_string());
1386                     self.sidebar_h.base_mut().label = Some("Height".to_string());
1387 
1388                     if !self.sidebar_text.editing { self.sidebar_text.text = "Shape Mode".to_string(); }
1389                     if !self.sidebar_x.editing { self.sidebar_x.value = x.round() as i32; }
1390                     if !self.sidebar_y.editing { self.sidebar_y.value = y.round() as i32; }
1391                     if !self.sidebar_w.editing { self.sidebar_w.value = w.round() as i32; }
1392                     if !self.sidebar_h.editing { self.sidebar_h.value = h.round() as i32; }
1393 
1394                     self.slider_r.set_value(color[0]);
1395                     self.slider_g.set_value(color[1]);
1396                     self.slider_b.set_value(color[2]);
1397                 }
1398                 Element::Vector { x1, y1, x2, y2, stroke_width, color, line_cap } => {
1399                     self.sidebar_x.base_mut().label = Some("Start X".to_string());
1400                     self.sidebar_y.base_mut().label = Some("Start Y".to_string());
1401                     self.sidebar_w.base_mut().label = Some("End X".to_string());
1402                     self.sidebar_h.base_mut().label = Some("End Y".to_string());
1403                     self.sidebar_size.base_mut().label = Some("Thickness".to_string());
1404 
1405                     if !self.sidebar_text.editing { self.sidebar_text.text = "Line Mode".to_string(); }
1406                     if !self.sidebar_x.editing { self.sidebar_x.value = x1.round() as i32; }
1407                     if !self.sidebar_y.editing { self.sidebar_y.value = y1.round() as i32; }
1408                     if !self.sidebar_w.editing { self.sidebar_w.value = x2.round() as i32; }
1409                     if !self.sidebar_h.editing { self.sidebar_h.value = y2.round() as i32; }
1410                     if !self.sidebar_size.editing { self.sidebar_size.value = stroke_width.round() as i32; }
1411                     self.dropdown_line_cap.selected = match line_cap {
1412                         LineCap::Arrow => 0,
1413                         LineCap::Round => 1,
1414                         LineCap::Flat => 2,
1415                     };
1416 
1417                     self.slider_r.set_value(color[0]);
1418                     self.slider_g.set_value(color[1]);
1419                     self.slider_b.set_value(color[2]);
1420                 }
1421             }
1422 
1423             // Sync page alignment dropdowns based on current positions
1424             let (ex, ey, ew, eh) = match element {
1425                 Element::Text { x, y, w, h, .. } => (*x, *y, *w, *h),
1426                 Element::Shape { x, y, w, h, .. } => (*x, *y, *w, *h),
1427                 Element::Vector { x1, y1, x2, y2, .. } => {
1428                     let x = x1.min(*x2);
1429                     let y = y1.min(*y2);
1430                     let w = (x1 - x2).abs();
1431                     let h = (y1 - y2).abs();
1432                     (x, y, w, h)
1433                 }
1434             };
1435 
1436             let center_x = ((self.page_w - ew) / 2.0).round();
1437             let right_x = (self.page_w - ew).round();
1438             self.dropdown_align_h.selected = if (ex - 0.0).abs() < 0.1 {
1439                 1 // Left
1440             } else if (ex - center_x).abs() < 0.1 {
1441                 2 // Center
1442             } else if (ex - right_x).abs() < 0.1 {
1443                 3 // Right
1444             } else {
1445                 0 // "--" (No alignment)
1446             };
1447 
1448             let middle_y = ((self.page_h - eh) / 2.0).round();
1449             let bottom_y = (self.page_h - eh).round();
1450             self.dropdown_align_v.selected = if (ey - 0.0).abs() < 0.1 {
1451                 1 // Top
1452             } else if (ey - middle_y).abs() < 0.1 {
1453                 2 // Middle
1454             } else if (ey - bottom_y).abs() < 0.1 {
1455                 3 // Bottom
1456             } else {
1457                 0 // "--" (No alignment)
1458             };
1459         }
1460     }
1461 
1462     fn apply_sidebar_changes(&mut self) {
1463         if self.word_processor_enabled {
1464             let mut family_str = self.wp_text_box.font_family.clone();
1465             if self.font_selector.take_change() {
1466                 family_str = self.font_selector.font_family.clone();
1467             }
1468             self.wp_text_box.font_family = family_str;
1469 
1470             let size_val = if self.sidebar_size.editing {
1471                 self.sidebar_size.edit_buffer.parse::<f32>().unwrap_or(14.0)
1472             } else {
1473                 self.sidebar_size.value as f32
1474             };
1475             self.wp_base_font_size = size_val;
1476 
1477             let r = (self.slider_r.inner().value() * 255.0).clamp(0.0, 255.0) as u8;
1478             let g = (self.slider_g.inner().value() * 255.0).clamp(0.0, 255.0) as u8;
1479             let b = (self.slider_b.inner().value() * 255.0).clamp(0.0, 255.0) as u8;
1480             self.wp_text_box.text_color = Some([r, g, b]);
1481         } else if let Some(idx) = self.selected_idx {
1482             let text_val = if self.sidebar_text.editing { self.sidebar_text.edit_buffer.clone() } else { self.sidebar_text.text.clone() };
1483             let x_val = if self.sidebar_x.editing {
1484                 self.sidebar_x.edit_buffer.parse::<f32>().unwrap_or(0.0)
1485             } else {
1486                 self.sidebar_x.value as f32
1487             };
1488             let y_val = if self.sidebar_y.editing {
1489                 self.sidebar_y.edit_buffer.parse::<f32>().unwrap_or(0.0)
1490             } else {
1491                 self.sidebar_y.value as f32
1492             };
1493             let w_val = if self.sidebar_w.editing {
1494                 self.sidebar_w.edit_buffer.parse::<f32>().unwrap_or(100.0)
1495             } else {
1496                 self.sidebar_w.value as f32
1497             };
1498             let h_val = if self.sidebar_h.editing {
1499                 self.sidebar_h.edit_buffer.parse::<f32>().unwrap_or(50.0)
1500             } else {
1501                 self.sidebar_h.value as f32
1502             };
1503 
1504             let is_vector = match &self.elements[idx] {
1505                 Element::Vector { .. } => true,
1506                 _ => false,
1507             };
1508             let (v_x2, v_y2) = if is_vector {
1509                 let nx2 = if self.sidebar_w.editing {
1510                     self.sidebar_w.edit_buffer.parse::<f32>().unwrap_or(0.0)
1511                 } else {
1512                     self.sidebar_w.value as f32
1513                 };
1514                 let ny2 = if self.sidebar_h.editing {
1515                     self.sidebar_h.edit_buffer.parse::<f32>().unwrap_or(0.0)
1516                 } else {
1517                     self.sidebar_h.value as f32
1518                 };
1519                 (nx2, ny2)
1520             } else {
1521                 (0.0, 0.0)
1522             };
1523             let w_for_align = if is_vector { (v_x2 - x_val).abs() } else { w_val };
1524             let h_for_align = if is_vector { (v_y2 - y_val).abs() } else { h_val };
1525             let size_val = if self.sidebar_size.editing {
1526                 self.sidebar_size.edit_buffer.parse::<f32>().unwrap_or(16.0)
1527             } else {
1528                 self.sidebar_size.value as f32
1529             };
1530 
1531             let r_val = self.slider_r.inner().value();
1532             let g_val = self.slider_g.inner().value();
1533             let b_val = self.slider_b.inner().value();
1534 
1535             let mut family_str = String::new();
1536             if let Some(Element::Text { font_family, .. }) = self.elements.get(idx) {
1537                 family_str = font_family.clone();
1538             }
1539             if self.font_selector.take_change() {
1540                 family_str = self.font_selector.font_family.clone();
1541             }
1542 
1543             let mut page_align_h = None;
1544             if self.dropdown_align_h.take_change() {
1545                 page_align_h = Some(self.dropdown_align_h.selected);
1546             }
1547             let mut page_align_v = None;
1548             if self.dropdown_align_v.take_change() {
1549                 page_align_v = Some(self.dropdown_align_v.selected);
1550             }
1551 
1552             let mut text_align_h = None;
1553             if self.dropdown_text_align_h.take_change() {
1554                 text_align_h = Some(match self.dropdown_text_align_h.selected {
1555                     1 => TextAlignH::Center,
1556                     2 => TextAlignH::Right,
1557                     _ => TextAlignH::Left,
1558                 });
1559             }
1560             let mut text_align_v = None;
1561             if self.dropdown_text_align_v.take_change() {
1562                 text_align_v = Some(match self.dropdown_text_align_v.selected {
1563                     1 => TextAlignV::Middle,
1564                     2 => TextAlignV::Bottom,
1565                     _ => TextAlignV::Top,
1566                 });
1567             }
1568 
1569             let mut final_x = x_val;
1570             let mut final_y = y_val;
1571 
1572             if let Some(sel_h) = page_align_h {
1573                 if sel_h == 1 {
1574                     final_x = 0.0;
1575                 } else if sel_h == 2 {
1576                     final_x = ((self.page_w - w_for_align) / 2.0).round();
1577                 } else if sel_h == 3 {
1578                     final_x = (self.page_w - w_for_align).round();
1579                 }
1580             }
1581             if let Some(sel_v) = page_align_v {
1582                 if sel_v == 1 {
1583                     final_y = 0.0;
1584                 } else if sel_v == 2 {
1585                     final_y = ((self.page_h - h_for_align) / 2.0).round();
1586                 } else if sel_v == 3 {
1587                     final_y = (self.page_h - h_for_align).round();
1588                 }
1589             }
1590 
1591             let mut final_x2 = v_x2;
1592             let mut final_y2 = v_y2;
1593             if is_vector {
1594                 let dx = final_x - x_val;
1595                 let dy = final_y - y_val;
1596                 final_x2 += dx;
1597                 final_y2 += dy;
1598             }
1599 
1600             match &mut self.elements[idx] {
1601                 Element::Text { text, x, y, w, h, font_size, color, font_family, align_h, align_v, multiline } => {
1602                     *text = text_val;
1603                     *w = w_val;
1604                     *h = h_val;
1605                     *font_size = size_val;
1606                     *color = [r_val, g_val, b_val, 1.0];
1607                     if !family_str.is_empty() {
1608                         *font_family = family_str;
1609                     }
1610                     if let Some(ah) = text_align_h {
1611                         *align_h = ah;
1612                     }
1613                     if let Some(av) = text_align_v {
1614                         *align_v = av;
1615                     }
1616                     *multiline = self.toggle_multiline.toggled();
1617                     *x = final_x;
1618                     *y = final_y;
1619                 }
1620                 Element::Shape { shape_type: _, x, y, w, h, color } => {
1621                     *w = w_val;
1622                     *h = h_val;
1623                     *color = [r_val, g_val, b_val, 1.0];
1624                     *x = final_x;
1625                     *y = final_y;
1626                 }
1627                 Element::Vector { x1, y1, x2, y2, stroke_width, color, line_cap } => {
1628                     *x1 = final_x;
1629                     *y1 = final_y;
1630                     *x2 = final_x2;
1631                     *y2 = final_y2;
1632                     *stroke_width = size_val;
1633                     *color = [r_val, g_val, b_val, 1.0];
1634                     *line_cap = match self.dropdown_line_cap.selected {
1635                         0 => LineCap::Arrow,
1636                         1 => LineCap::Round,
1637                         2 => LineCap::Flat,
1638                         _ => LineCap::Arrow,
1639                     };
1640                 }
1641             }
1642         }
1643         self.rebuild_layers_tab_widgets();
1644         self.sync_sidebar_fields();
1645     }
1646 
1647 
1648 
1649     pub fn get_current_document(&self) -> LayoutDocument {
1650         LayoutDocument {
1651             page_w: self.page_w,
1652             page_h: self.page_h,
1653             page_color: self.page_color,
1654             margin_enabled: self.margin_enabled,
1655             margin_x: self.margin_x,
1656             margin_y: self.margin_y,
1657             word_processor_enabled: self.word_processor_enabled,
1658             wp_text: self.wp_text_box.text.clone(),
1659             elements: self.elements.clone(),
1660             grid_color: self.grid_color,
1661             grid_size: self.spinbox_grid_size.value as f32 / 10.0,
1662             margin_color: self.margin_color,
1663             margin_thickness: self.margin_thickness,
1664             grid_enabled: self.grid_enabled,
1665             grid_units: self.dropdown_grid_units.selected,
1666             zoom: self.slider_zoom.inner().value(),
1667             rulers_enabled: self.toggle_rulers.toggled(),
1668             ruler_units: self.dropdown_units.selected,
1669         }
1670     }
1671 
1672     pub fn has_unsaved_changes(&self) -> bool {
1673         self.get_current_document() != self.last_saved_document
1674     }
1675 
1676     pub fn grid_spacing(&self) -> f32 {
1677         match self.dropdown_grid_units.selected {
1678             0 => 20.0,                    // Pixels
1679             1 => 0.25 * 60.0,             // Inches
1680             2 => 0.5 * 23.622047,         // Centimeters
1681             3 => 5.0 * 2.3622047,         // Millimeters
1682             _ => 20.0,
1683         }
1684     }
1685 
1686     fn active_page_widgets_unfocus(&mut self) {
1687         let selected_page = self.paginator.selected_page();
1688         match selected_page {
1689             0 => {
1690                 self.btn_new_doc.unfocus();
1691                 self.btn_open.unfocus();
1692                 self.recent_files_list.focused = false;
1693                 for btn in &mut self.recent_files_buttons {
1694                     btn.unfocus();
1695                 }
1696                 self.btn_save.unfocus();
1697                 self.btn_save_as.unfocus();
1698                 self.btn_exit.unfocus();
1699             }
1700             1 => {
1701                 self.dropdown_margin_units.unfocus();
1702                 self.dropdown_presets.unfocus();
1703                 self.slider_page_x.unfocus();
1704                 self.slider_page_y.unfocus();
1705                 self.page_color_selector.unfocus();
1706                 self.toggle_margin.unfocus();
1707                 self.toggle_word_processor.unfocus();
1708                 self.slider_margin_x.unfocus();
1709                 self.slider_margin_y.unfocus();
1710             }
1711             2 => {
1712                 self.font_selector.unfocus();
1713                 self.sidebar_size.unfocus();
1714                 self.slider_r.unfocus();
1715                 self.slider_g.unfocus();
1716                 self.slider_b.unfocus();
1717                 self.sidebar_x.unfocus();
1718                 self.sidebar_y.unfocus();
1719                 self.sidebar_w.unfocus();
1720                 self.sidebar_h.unfocus();
1721                 self.sidebar_text.unfocus();
1722                 self.toggle_multiline.unfocus();
1723                 self.dropdown_text_align_h.unfocus();
1724                 self.dropdown_text_align_v.unfocus();
1725                 self.dropdown_align_h.unfocus();
1726                 self.dropdown_align_v.unfocus();
1727                 self.dropdown_line_cap.unfocus();
1728                 self.label_sel_status.unfocus();
1729                 self.label_sel_desc1.unfocus();
1730                 self.label_sel_desc2.unfocus();
1731             }
1732             3 => {
1733                 self.btn_add_text.unfocus();
1734                 self.btn_add_rect.unfocus();
1735                 self.btn_add_banner.unfocus();
1736                 self.btn_add_vector.unfocus();
1737                 self.label_total_elements.unfocus();
1738             }
1739             4 => {
1740                 self.toggle_grid.unfocus();
1741                 self.grid_color_selector.unfocus();
1742                 self.spinbox_grid_size.unfocus();
1743                 self.label_grid_snap.unfocus();
1744                 self.slider_zoom.unfocus();
1745                 self.toggle_rulers.unfocus();
1746                 self.dropdown_units.unfocus();
1747                 self.margin_color_selector.unfocus();
1748                 self.spinbox_margin_thickness.unfocus();
1749                 self.dropdown_grid_units.unfocus();
1750             }
1751             5 => {
1752                 for btn in &mut self.layer_buttons {
1753                     btn.unfocus();
1754                 }
1755             }
1756             _ => {}
1757         }
1758     }
1759 
1760     fn active_page_widgets_focus_handle(&mut self, px: f32, py: f32) {
1761         let ctx = &self.ui_context;
1762         let selected_page = self.paginator.selected_page();
1763         let mut hit_idx = None;
1764         match selected_page {
1765             0 => {
1766                 if self.btn_new_doc.hit_test(px, py, ctx) { hit_idx = Some(0); }
1767                 else if self.btn_open.hit_test(px, py, ctx) { hit_idx = Some(1); }
1768                 else if self.recent_files_list.hit(px, py) { hit_idx = Some(2); }
1769                 else {
1770                     for (i, btn) in self.recent_files_buttons.iter().enumerate() {
1771                         if btn.hit_test(px, py, ctx) {
1772                             hit_idx = Some(3 + i);
1773                             break;
1774                         }
1775                     }
1776                     if hit_idx.is_none() {
1777                         let offset = 3 + self.recent_files_buttons.len();
1778                         if self.btn_save.hit_test(px, py, ctx) { hit_idx = Some(offset); }
1779                         else if self.btn_save_as.hit_test(px, py, ctx) { hit_idx = Some(offset + 1); }
1780                         else if self.btn_exit.hit_test(px, py, ctx) { hit_idx = Some(offset + 2); }
1781                     }
1782                 }
1783             }
1784             1 => {
1785                 if self.dropdown_margin_units.hit_test(px, py, ctx) { hit_idx = Some(0); }
1786                 else if self.dropdown_presets.hit_test(px, py, ctx) { hit_idx = Some(1); }
1787                 else if self.slider_page_x.hit_test(px, py, ctx) { hit_idx = Some(2); }
1788                 else if self.slider_page_y.hit_test(px, py, ctx) { hit_idx = Some(3); }
1789                 else if self.page_color_selector.hit_test(px, py, ctx) { hit_idx = Some(4); }
1790                 else if self.toggle_margin.hit_test(px, py, ctx) { hit_idx = Some(5); }
1791                 else if self.toggle_word_processor.hit_test(px, py, ctx) { hit_idx = Some(6); }
1792                 else if self.slider_margin_x.hit_test(px, py, ctx) { hit_idx = Some(7); }
1793                 else if self.slider_margin_y.hit_test(px, py, ctx) { hit_idx = Some(8); }
1794             }
1795             2 => {
1796                 if self.word_processor_enabled {
1797                     if self.font_selector.hit_test(px, py, ctx) { hit_idx = Some(0); }
1798                     else if self.sidebar_size.hit_test(px, py, ctx) { hit_idx = Some(1); }
1799                     else if self.slider_r.hit_test(px, py, ctx) { hit_idx = Some(2); }
1800                     else if self.slider_g.hit_test(px, py, ctx) { hit_idx = Some(3); }
1801                     else if self.slider_b.hit_test(px, py, ctx) { hit_idx = Some(4); }
1802                 } else if self.selected_idx.is_some() {
1803                     let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
1804                         Some(Element::Text { .. }) => (true, false),
1805                         Some(Element::Vector { .. }) => (false, true),
1806                         _ => (false, false),
1807                     };
1808                     if is_text {
1809                         if self.sidebar_x.hit_test(px, py, ctx) { hit_idx = Some(0); }
1810                         else if self.sidebar_y.hit_test(px, py, ctx) { hit_idx = Some(1); }
1811                         else if self.sidebar_w.hit_test(px, py, ctx) { hit_idx = Some(2); }
1812                         else if self.sidebar_h.hit_test(px, py, ctx) { hit_idx = Some(3); }
1813                         else if self.sidebar_text.hit_test(px, py, ctx) { hit_idx = Some(4); }
1814                         else if self.font_selector.hit_test(px, py, ctx) { hit_idx = Some(5); }
1815                         else if self.toggle_multiline.hit_test(px, py, ctx) { hit_idx = Some(6); }
1816                         else if self.dropdown_text_align_h.hit_test(px, py, ctx) { hit_idx = Some(7); }
1817                         else if self.dropdown_text_align_v.hit_test(px, py, ctx) { hit_idx = Some(8); }
1818                         else if self.sidebar_size.hit_test(px, py, ctx) { hit_idx = Some(9); }
1819                         else if self.slider_r.hit_test(px, py, ctx) { hit_idx = Some(10); }
1820                         else if self.slider_g.hit_test(px, py, ctx) { hit_idx = Some(11); }
1821                         else if self.slider_b.hit_test(px, py, ctx) { hit_idx = Some(12); }
1822                     } else if is_vector {
1823                         if self.sidebar_x.hit_test(px, py, ctx) { hit_idx = Some(0); }
1824                         else if self.sidebar_y.hit_test(px, py, ctx) { hit_idx = Some(1); }
1825                         else if self.sidebar_w.hit_test(px, py, ctx) { hit_idx = Some(2); }
1826                         else if self.sidebar_h.hit_test(px, py, ctx) { hit_idx = Some(3); }
1827                         else if self.sidebar_size.hit_test(px, py, ctx) { hit_idx = Some(4); }
1828                         else if self.dropdown_line_cap.hit_test(px, py, ctx) { hit_idx = Some(5); }
1829                         else if self.slider_r.hit_test(px, py, ctx) { hit_idx = Some(6); }
1830                         else if self.slider_g.hit_test(px, py, ctx) { hit_idx = Some(7); }
1831                         else if self.slider_b.hit_test(px, py, ctx) { hit_idx = Some(8); }
1832                     } else {
1833                         if self.sidebar_x.hit_test(px, py, ctx) { hit_idx = Some(0); }
1834                         else if self.sidebar_y.hit_test(px, py, ctx) { hit_idx = Some(1); }
1835                         else if self.sidebar_w.hit_test(px, py, ctx) { hit_idx = Some(2); }
1836                         else if self.sidebar_h.hit_test(px, py, ctx) { hit_idx = Some(3); }
1837                         else if self.slider_r.hit_test(px, py, ctx) { hit_idx = Some(4); }
1838                         else if self.slider_g.hit_test(px, py, ctx) { hit_idx = Some(5); }
1839                         else if self.slider_b.hit_test(px, py, ctx) { hit_idx = Some(6); }
1840                     }
1841                 } else {
1842                     if self.label_sel_status.hit_test(px, py, ctx) { hit_idx = Some(0); }
1843                     else if self.label_sel_desc1.hit_test(px, py, ctx) { hit_idx = Some(1); }
1844                     else if self.label_sel_desc2.hit_test(px, py, ctx) { hit_idx = Some(2); }
1845                 }
1846             }
1847             3 => {
1848                 if self.btn_add_text.hit_test(px, py, ctx) { hit_idx = Some(0); }
1849                 else if self.btn_add_rect.hit_test(px, py, ctx) { hit_idx = Some(1); }
1850                 else if self.btn_add_banner.hit_test(px, py, ctx) { hit_idx = Some(2); }
1851                 else if self.btn_add_vector.hit_test(px, py, ctx) { hit_idx = Some(3); }
1852                 else if self.label_total_elements.hit_test(px, py, ctx) { hit_idx = Some(4); }
1853             }
1854             4 => {
1855                 if self.toggle_grid.hit_test(px, py, ctx) { hit_idx = Some(0); }
1856                 else if self.grid_color_selector.hit_test(px, py, ctx) { hit_idx = Some(1); }
1857                 else if self.spinbox_grid_size.hit_test(px, py, ctx) { hit_idx = Some(2); }
1858                 else if self.label_grid_snap.hit_test(px, py, ctx) { hit_idx = Some(3); }
1859                 else if self.slider_zoom.hit_test(px, py, ctx) { hit_idx = Some(4); }
1860                 else if self.toggle_rulers.hit_test(px, py, ctx) { hit_idx = Some(5); }
1861                 else if self.dropdown_units.hit_test(px, py, ctx) { hit_idx = Some(6); }
1862                 else if self.margin_color_selector.hit_test(px, py, ctx) { hit_idx = Some(7); }
1863                 else if self.spinbox_margin_thickness.hit_test(px, py, ctx) { hit_idx = Some(8); }
1864                 else if self.dropdown_grid_units.hit_test(px, py, ctx) { hit_idx = Some(9); }
1865             }
1866             5 => {
1867                 for (i, btn) in self.layer_buttons.iter().enumerate() {
1868                     if btn.hit_test(px, py, ctx) {
1869                         hit_idx = Some(i);
1870                         break;
1871                     }
1872                 }
1873             }
1874             _ => {}
1875         }
1876 
1877         match selected_page {
1878             0 => {
1879                 if hit_idx == Some(0) { self.btn_new_doc.focus(); } else { self.btn_new_doc.unfocus(); }
1880                 if hit_idx == Some(1) { self.btn_open.focus(); } else { self.btn_open.unfocus(); }
1881                 self.recent_files_list.focused = hit_idx == Some(2);
1882                 for (i, btn) in self.recent_files_buttons.iter_mut().enumerate() {
1883                     if hit_idx == Some(3 + i) { btn.focus(); } else { btn.unfocus(); }
1884                 }
1885                 let offset = 3 + self.recent_files_buttons.len();
1886                 if hit_idx == Some(offset) { self.btn_save.focus(); } else { self.btn_save.unfocus(); }
1887                 if hit_idx == Some(offset + 1) { self.btn_save_as.focus(); } else { self.btn_save_as.unfocus(); }
1888                 if hit_idx == Some(offset + 2) { self.btn_exit.focus(); } else { self.btn_exit.unfocus(); }
1889             }
1890             1 => {
1891                 if hit_idx == Some(0) { self.dropdown_margin_units.focus(); } else { self.dropdown_margin_units.unfocus(); }
1892                 if hit_idx == Some(1) { self.dropdown_presets.focus(); } else { self.dropdown_presets.unfocus(); }
1893                 if hit_idx == Some(2) { self.slider_page_x.focus(); } else { self.slider_page_x.unfocus(); }
1894                 if hit_idx == Some(3) { self.slider_page_y.focus(); } else { self.slider_page_y.unfocus(); }
1895                 if hit_idx == Some(4) { self.page_color_selector.focus(); } else { self.page_color_selector.unfocus(); }
1896                 if hit_idx == Some(5) { self.toggle_margin.focus(); } else { self.toggle_margin.unfocus(); }
1897                 if hit_idx == Some(6) { self.toggle_word_processor.focus(); } else { self.toggle_word_processor.unfocus(); }
1898                 if hit_idx == Some(7) { self.slider_margin_x.focus(); } else { self.slider_margin_x.unfocus(); }
1899                 if hit_idx == Some(8) { self.slider_margin_y.focus(); } else { self.slider_margin_y.unfocus(); }
1900             }
1901             2 => {
1902                 if self.word_processor_enabled {
1903                     if hit_idx == Some(0) { self.font_selector.focus(); } else { self.font_selector.unfocus(); }
1904                     if hit_idx == Some(1) { self.sidebar_size.focus(); } else { self.sidebar_size.unfocus(); }
1905                     if hit_idx == Some(2) { self.slider_r.focus(); } else { self.slider_r.unfocus(); }
1906                     if hit_idx == Some(3) { self.slider_g.focus(); } else { self.slider_g.unfocus(); }
1907                     if hit_idx == Some(4) { self.slider_b.focus(); } else { self.slider_b.unfocus(); }
1908                 } else if self.selected_idx.is_some() {
1909                     let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
1910                         Some(Element::Text { .. }) => (true, false),
1911                         Some(Element::Vector { .. }) => (false, true),
1912                         _ => (false, false),
1913                     };
1914                     if is_text {
1915                         if hit_idx == Some(0) { self.sidebar_x.focus(); } else { self.sidebar_x.unfocus(); }
1916                         if hit_idx == Some(1) { self.sidebar_y.focus(); } else { self.sidebar_y.unfocus(); }
1917                         if hit_idx == Some(2) { self.sidebar_w.focus(); } else { self.sidebar_w.unfocus(); }
1918                         if hit_idx == Some(3) { self.sidebar_h.focus(); } else { self.sidebar_h.unfocus(); }
1919                         if hit_idx == Some(4) { self.sidebar_text.focus(); } else { self.sidebar_text.unfocus(); }
1920                         if hit_idx == Some(5) { self.font_selector.focus(); } else { self.font_selector.unfocus(); }
1921                         if hit_idx == Some(6) { self.toggle_multiline.focus(); } else { self.toggle_multiline.unfocus(); }
1922                         if hit_idx == Some(7) { self.dropdown_text_align_h.focus(); } else { self.dropdown_text_align_h.unfocus(); }
1923                         if hit_idx == Some(8) { self.dropdown_text_align_v.focus(); } else { self.dropdown_text_align_v.unfocus(); }
1924                         if hit_idx == Some(9) { self.sidebar_size.focus(); } else { self.sidebar_size.unfocus(); }
1925                         if hit_idx == Some(10) { self.slider_r.focus(); } else { self.slider_r.unfocus(); }
1926                         if hit_idx == Some(11) { self.slider_g.focus(); } else { self.slider_g.unfocus(); }
1927                         if hit_idx == Some(12) { self.slider_b.focus(); } else { self.slider_b.unfocus(); }
1928                     } else if is_vector {
1929                         if hit_idx == Some(0) { self.sidebar_x.focus(); } else { self.sidebar_x.unfocus(); }
1930                         if hit_idx == Some(1) { self.sidebar_y.focus(); } else { self.sidebar_y.unfocus(); }
1931                         if hit_idx == Some(2) { self.sidebar_w.focus(); } else { self.sidebar_w.unfocus(); }
1932                         if hit_idx == Some(3) { self.sidebar_h.focus(); } else { self.sidebar_h.unfocus(); }
1933                         if hit_idx == Some(4) { self.sidebar_size.focus(); } else { self.sidebar_size.unfocus(); }
1934                         if hit_idx == Some(5) { self.dropdown_line_cap.focus(); } else { self.dropdown_line_cap.unfocus(); }
1935                         if hit_idx == Some(6) { self.slider_r.focus(); } else { self.slider_r.unfocus(); }
1936                         if hit_idx == Some(7) { self.slider_g.focus(); } else { self.slider_g.unfocus(); }
1937                         if hit_idx == Some(8) { self.slider_b.focus(); } else { self.slider_b.unfocus(); }
1938                     } else {
1939                         if hit_idx == Some(0) { self.sidebar_x.focus(); } else { self.sidebar_x.unfocus(); }
1940                         if hit_idx == Some(1) { self.sidebar_y.focus(); } else { self.sidebar_y.unfocus(); }
1941                         if hit_idx == Some(2) { self.sidebar_w.focus(); } else { self.sidebar_w.unfocus(); }
1942                         if hit_idx == Some(3) { self.sidebar_h.focus(); } else { self.sidebar_h.unfocus(); }
1943                         if hit_idx == Some(4) { self.slider_r.focus(); } else { self.slider_r.unfocus(); }
1944                         if hit_idx == Some(5) { self.slider_g.focus(); } else { self.slider_g.unfocus(); }
1945                         if hit_idx == Some(6) { self.slider_b.focus(); } else { self.slider_b.unfocus(); }
1946                     }
1947                 } else {
1948                     if hit_idx == Some(0) { self.label_sel_status.focus(); } else { self.label_sel_status.unfocus(); }
1949                     if hit_idx == Some(1) { self.label_sel_desc1.focus(); } else { self.label_sel_desc1.unfocus(); }
1950                     if hit_idx == Some(2) { self.label_sel_desc2.focus(); } else { self.label_sel_desc2.unfocus(); }
1951                 }
1952             }
1953             3 => {
1954                 if hit_idx == Some(0) { self.btn_add_text.focus(); } else { self.btn_add_text.unfocus(); }
1955                 if hit_idx == Some(1) { self.btn_add_rect.focus(); } else { self.btn_add_rect.unfocus(); }
1956                 if hit_idx == Some(2) { self.btn_add_banner.focus(); } else { self.btn_add_banner.unfocus(); }
1957                 if hit_idx == Some(3) { self.btn_add_vector.focus(); } else { self.btn_add_vector.unfocus(); }
1958                 if hit_idx == Some(4) { self.label_total_elements.focus(); } else { self.label_total_elements.unfocus(); }
1959             }
1960             4 => {
1961                 if hit_idx == Some(0) { self.toggle_grid.focus(); } else { self.toggle_grid.unfocus(); }
1962                 if hit_idx == Some(1) { self.grid_color_selector.focus(); } else { self.grid_color_selector.unfocus(); }
1963                 if hit_idx == Some(2) { self.spinbox_grid_size.focus(); } else { self.spinbox_grid_size.unfocus(); }
1964                 if hit_idx == Some(3) { self.label_grid_snap.focus(); } else { self.label_grid_snap.unfocus(); }
1965                 if hit_idx == Some(4) { self.slider_zoom.focus(); } else { self.slider_zoom.unfocus(); }
1966                 if hit_idx == Some(5) { self.toggle_rulers.focus(); } else { self.toggle_rulers.unfocus(); }
1967                 if hit_idx == Some(6) { self.dropdown_units.focus(); } else { self.dropdown_units.unfocus(); }
1968                 if hit_idx == Some(7) { self.margin_color_selector.focus(); } else { self.margin_color_selector.unfocus(); }
1969                 if hit_idx == Some(8) { self.spinbox_margin_thickness.focus(); } else { self.spinbox_margin_thickness.unfocus(); }
1970                 if hit_idx == Some(9) { self.dropdown_grid_units.focus(); } else { self.dropdown_grid_units.unfocus(); }
1971             }
1972             5 => {
1973                 for (i, btn) in self.layer_buttons.iter_mut().enumerate() {
1974                     if hit_idx == Some(i) { btn.focus(); } else { btn.unfocus(); }
1975                 }
1976             }
1977             _ => {}
1978         }
1979     }
1980 
1981     fn active_page_widgets_cursor_moved(&mut self, px: f32, py: f32) -> bool {
1982         let mut changed = false;
1983         // Routed dispatch (6bd shrink): one Event per widget root through the router.
1984         let mv = cce_ui::widget::Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
1985         let ctx = &mut self.ui_context;
1986         let selected_page = self.paginator.selected_page();
1987         match selected_page {
1988             0 => {
1989                 if ctx.propagate_event(&mv, self.btn_new_doc.id()) { changed = true; }
1990                 if ctx.propagate_event(&mv, self.btn_open.id()) { changed = true; }
1991                 if self.recent_files_list.cursor_moved(px, py) { changed = true; }
1992                 for btn in &mut self.recent_files_buttons {
1993                     if ctx.propagate_event(&mv, btn.id()) { changed = true; }
1994                 }
1995                 if ctx.propagate_event(&mv, self.btn_save.id()) { changed = true; }
1996                 if ctx.propagate_event(&mv, self.btn_save_as.id()) { changed = true; }
1997                 if ctx.propagate_event(&mv, self.btn_exit.id()) { changed = true; }
1998             }
1999             1 => {
2000                 if ctx.propagate_event(&mv, self.dropdown_margin_units.id()) { changed = true; }
2001                 if ctx.propagate_event(&mv, self.dropdown_presets.id()) { changed = true; }
2002                 if ctx.propagate_event(&mv, self.slider_page_x.id()) { changed = true; }
2003                 if ctx.propagate_event(&mv, self.slider_page_y.id()) { changed = true; }
2004                 if ctx.propagate_event(&mv, self.page_color_selector.id()) { changed = true; }
2005                 if ctx.propagate_event(&mv, self.toggle_margin.id()) { changed = true; }
2006                 if ctx.propagate_event(&mv, self.toggle_word_processor.id()) { changed = true; }
2007                 if ctx.propagate_event(&mv, self.slider_margin_x.id()) { changed = true; }
2008                 if ctx.propagate_event(&mv, self.slider_margin_y.id()) { changed = true; }
2009             }
2010             2 => {
2011                 if self.word_processor_enabled {
2012                     if ctx.propagate_event(&mv, self.font_selector.id()) { changed = true; }
2013                     if ctx.propagate_event(&mv, self.sidebar_size.id()) { changed = true; }
2014                     if ctx.propagate_event(&mv, self.slider_r.id()) { changed = true; }
2015                     if ctx.propagate_event(&mv, self.slider_g.id()) { changed = true; }
2016                     if ctx.propagate_event(&mv, self.slider_b.id()) { changed = true; }
2017                 } else if self.selected_idx.is_some() {
2018                     if ctx.propagate_event(&mv, self.dropdown_align_h.id()) { changed = true; }
2019                     if ctx.propagate_event(&mv, self.dropdown_align_v.id()) { changed = true; }
2020 
2021                     let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
2022                         Some(Element::Text { .. }) => (true, false),
2023                         Some(Element::Vector { .. }) => (false, true),
2024                         _ => (false, false),
2025                     };
2026                     if is_text {
2027                         if ctx.propagate_event(&mv, self.sidebar_x.id()) { changed = true; }
2028                         if ctx.propagate_event(&mv, self.sidebar_y.id()) { changed = true; }
2029                         if ctx.propagate_event(&mv, self.sidebar_w.id()) { changed = true; }
2030                         if ctx.propagate_event(&mv, self.sidebar_h.id()) { changed = true; }
2031                         if ctx.propagate_event(&mv, self.sidebar_text.id()) { changed = true; }
2032                         if ctx.propagate_event(&mv, self.font_selector.id()) { changed = true; }
2033                         if ctx.propagate_event(&mv, self.toggle_multiline.id()) { changed = true; }
2034                         if ctx.propagate_event(&mv, self.dropdown_text_align_h.id()) { changed = true; }
2035                         if ctx.propagate_event(&mv, self.dropdown_text_align_v.id()) { changed = true; }
2036                         if ctx.propagate_event(&mv, self.sidebar_size.id()) { changed = true; }
2037                         if ctx.propagate_event(&mv, self.slider_r.id()) { changed = true; }
2038                         if ctx.propagate_event(&mv, self.slider_g.id()) { changed = true; }
2039                         if ctx.propagate_event(&mv, self.slider_b.id()) { changed = true; }
2040                     } else if is_vector {
2041                         if ctx.propagate_event(&mv, self.sidebar_x.id()) { changed = true; }
2042                         if ctx.propagate_event(&mv, self.sidebar_y.id()) { changed = true; }
2043                         if ctx.propagate_event(&mv, self.sidebar_w.id()) { changed = true; }
2044                         if ctx.propagate_event(&mv, self.sidebar_h.id()) { changed = true; }
2045                         if ctx.propagate_event(&mv, self.sidebar_size.id()) { changed = true; }
2046                         if ctx.propagate_event(&mv, self.dropdown_line_cap.id()) { changed = true; }
2047                         if ctx.propagate_event(&mv, self.slider_r.id()) { changed = true; }
2048                         if ctx.propagate_event(&mv, self.slider_g.id()) { changed = true; }
2049                         if ctx.propagate_event(&mv, self.slider_b.id()) { changed = true; }
2050                     } else {
2051                         if ctx.propagate_event(&mv, self.sidebar_x.id()) { changed = true; }
2052                         if ctx.propagate_event(&mv, self.sidebar_y.id()) { changed = true; }
2053                         if ctx.propagate_event(&mv, self.sidebar_w.id()) { changed = true; }
2054                         if ctx.propagate_event(&mv, self.sidebar_h.id()) { changed = true; }
2055                         if ctx.propagate_event(&mv, self.slider_r.id()) { changed = true; }
2056                         if ctx.propagate_event(&mv, self.slider_g.id()) { changed = true; }
2057                         if ctx.propagate_event(&mv, self.slider_b.id()) { changed = true; }
2058                     }
2059                 } else {
2060                     if ctx.propagate_event(&mv, self.label_sel_status.id()) { changed = true; }
2061                     if ctx.propagate_event(&mv, self.label_sel_desc1.id()) { changed = true; }
2062                     if ctx.propagate_event(&mv, self.label_sel_desc2.id()) { changed = true; }
2063                 }
2064             }
2065             3 => {
2066                 if ctx.propagate_event(&mv, self.btn_add_text.id()) { changed = true; }
2067                 if ctx.propagate_event(&mv, self.btn_add_rect.id()) { changed = true; }
2068                 if ctx.propagate_event(&mv, self.btn_add_banner.id()) { changed = true; }
2069                 if ctx.propagate_event(&mv, self.btn_add_vector.id()) { changed = true; }
2070                 if ctx.propagate_event(&mv, self.label_total_elements.id()) { changed = true; }
2071             }
2072             4 => {
2073                 if ctx.propagate_event(&mv, self.toggle_grid.id()) { changed = true; }
2074                 if ctx.propagate_event(&mv, self.grid_color_selector.id()) { changed = true; }
2075                 if ctx.propagate_event(&mv, self.spinbox_grid_size.id()) { changed = true; }
2076                 if ctx.propagate_event(&mv, self.label_grid_snap.id()) { changed = true; }
2077                 if ctx.propagate_event(&mv, self.slider_zoom.id()) { changed = true; }
2078                 if ctx.propagate_event(&mv, self.toggle_rulers.id()) { changed = true; }
2079                 if ctx.propagate_event(&mv, self.dropdown_units.id()) { changed = true; }
2080                 if ctx.propagate_event(&mv, self.margin_color_selector.id()) { changed = true; }
2081                 if ctx.propagate_event(&mv, self.spinbox_margin_thickness.id()) { changed = true; }
2082                 if ctx.propagate_event(&mv, self.dropdown_grid_units.id()) { changed = true; }
2083             }
2084             5 => {
2085                 for btn in &mut self.layer_buttons {
2086                     if ctx.propagate_event(&mv, btn.id()) { changed = true; }
2087                 }
2088             }
2089             _ => {}
2090         }
2091         changed
2092     }
2093 
2094     fn active_page_widgets_mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
2095         let mut changed = false;
2096         let ev = cce_ui::widget::Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py };
2097         let ctx = &mut self.ui_context;
2098         let selected_page = self.paginator.selected_page();
2099         match selected_page {
2100             0 => {
2101                 if ctx.propagate_event(&ev, self.btn_new_doc.id()) { changed = true; }
2102                 if ctx.propagate_event(&ev, self.btn_open.id()) { changed = true; }
2103                 if button == MouseButton::Left {
2104                     let handled = match state {
2105                         ElementState::Pressed => self.recent_files_list.press(px, py),
2106                         ElementState::Released => self.recent_files_list.release(),
2107                     };
2108                     if handled { changed = true; }
2109                 }
2110                 for btn in &mut self.recent_files_buttons {
2111                     if ctx.propagate_event(&ev, btn.id()) { changed = true; }
2112                 }
2113                 if ctx.propagate_event(&ev, self.btn_save.id()) { changed = true; }
2114                 if ctx.propagate_event(&ev, self.btn_save_as.id()) { changed = true; }
2115                 if ctx.propagate_event(&ev, self.btn_exit.id()) { changed = true; }
2116             }
2117             1 => {
2118                 if ctx.propagate_event(&ev, self.dropdown_margin_units.id()) { changed = true; }
2119                 if ctx.propagate_event(&ev, self.dropdown_presets.id()) { changed = true; }
2120                 if ctx.propagate_event(&ev, self.slider_page_x.id()) { changed = true; }
2121                 if ctx.propagate_event(&ev, self.slider_page_y.id()) { changed = true; }
2122                 if ctx.propagate_event(&ev, self.page_color_selector.id()) { changed = true; }
2123                 if ctx.propagate_event(&ev, self.toggle_margin.id()) { changed = true; }
2124                 if ctx.propagate_event(&ev, self.toggle_word_processor.id()) { changed = true; }
2125                 if ctx.propagate_event(&ev, self.slider_margin_x.id()) { changed = true; }
2126                 if ctx.propagate_event(&ev, self.slider_margin_y.id()) { changed = true; }
2127             }
2128             2 => {
2129                 if self.word_processor_enabled {
2130                     if ctx.propagate_event(&ev, self.font_selector.id()) { changed = true; }
2131                     if ctx.propagate_event(&ev, self.sidebar_size.id()) { changed = true; }
2132                     if ctx.propagate_event(&ev, self.slider_r.id()) { changed = true; }
2133                     if ctx.propagate_event(&ev, self.slider_g.id()) { changed = true; }
2134                     if ctx.propagate_event(&ev, self.slider_b.id()) { changed = true; }
2135                 } else if self.selected_idx.is_some() {
2136                     if ctx.propagate_event(&ev, self.dropdown_align_h.id()) { changed = true; }
2137                     if ctx.propagate_event(&ev, self.dropdown_align_v.id()) { changed = true; }
2138 
2139                     let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
2140                         Some(Element::Text { .. }) => (true, false),
2141                         Some(Element::Vector { .. }) => (false, true),
2142                         _ => (false, false),
2143                     };
2144                     if is_text {
2145                         if ctx.propagate_event(&ev, self.sidebar_x.id()) { changed = true; }
2146                         if ctx.propagate_event(&ev, self.sidebar_y.id()) { changed = true; }
2147                         if ctx.propagate_event(&ev, self.sidebar_w.id()) { changed = true; }
2148                         if ctx.propagate_event(&ev, self.sidebar_h.id()) { changed = true; }
2149                         if ctx.propagate_event(&ev, self.sidebar_text.id()) { changed = true; }
2150                         if ctx.propagate_event(&ev, self.font_selector.id()) { changed = true; }
2151                         if ctx.propagate_event(&ev, self.toggle_multiline.id()) { changed = true; }
2152                         if ctx.propagate_event(&ev, self.dropdown_text_align_h.id()) { changed = true; }
2153                         if ctx.propagate_event(&ev, self.dropdown_text_align_v.id()) { changed = true; }
2154                         if ctx.propagate_event(&ev, self.sidebar_size.id()) { changed = true; }
2155                         if ctx.propagate_event(&ev, self.slider_r.id()) { changed = true; }
2156                         if ctx.propagate_event(&ev, self.slider_g.id()) { changed = true; }
2157                         if ctx.propagate_event(&ev, self.slider_b.id()) { changed = true; }
2158                     } else if is_vector {
2159                         if ctx.propagate_event(&ev, self.sidebar_x.id()) { changed = true; }
2160                         if ctx.propagate_event(&ev, self.sidebar_y.id()) { changed = true; }
2161                         if ctx.propagate_event(&ev, self.sidebar_w.id()) { changed = true; }
2162                         if ctx.propagate_event(&ev, self.sidebar_h.id()) { changed = true; }
2163                         if ctx.propagate_event(&ev, self.sidebar_size.id()) { changed = true; }
2164                         if ctx.propagate_event(&ev, self.dropdown_line_cap.id()) { changed = true; }
2165                         if ctx.propagate_event(&ev, self.slider_r.id()) { changed = true; }
2166                         if ctx.propagate_event(&ev, self.slider_g.id()) { changed = true; }
2167                         if ctx.propagate_event(&ev, self.slider_b.id()) { changed = true; }
2168                     } else {
2169                         if ctx.propagate_event(&ev, self.sidebar_x.id()) { changed = true; }
2170                         if ctx.propagate_event(&ev, self.sidebar_y.id()) { changed = true; }
2171                         if ctx.propagate_event(&ev, self.sidebar_w.id()) { changed = true; }
2172                         if ctx.propagate_event(&ev, self.sidebar_h.id()) { changed = true; }
2173                         if ctx.propagate_event(&ev, self.slider_r.id()) { changed = true; }
2174                         if ctx.propagate_event(&ev, self.slider_g.id()) { changed = true; }
2175                         if ctx.propagate_event(&ev, self.slider_b.id()) { changed = true; }
2176                     }
2177                 } else {
2178                     if ctx.propagate_event(&ev, self.label_sel_status.id()) { changed = true; }
2179                     if ctx.propagate_event(&ev, self.label_sel_desc1.id()) { changed = true; }
2180                     if ctx.propagate_event(&ev, self.label_sel_desc2.id()) { changed = true; }
2181                 }
2182             }
2183             3 => {
2184                 if ctx.propagate_event(&ev, self.btn_add_text.id()) { changed = true; }
2185                 if ctx.propagate_event(&ev, self.btn_add_rect.id()) { changed = true; }
2186                 if ctx.propagate_event(&ev, self.btn_add_banner.id()) { changed = true; }
2187                 if ctx.propagate_event(&ev, self.btn_add_vector.id()) { changed = true; }
2188                 if ctx.propagate_event(&ev, self.label_total_elements.id()) { changed = true; }
2189             }
2190             4 => {
2191                 if ctx.propagate_event(&ev, self.toggle_grid.id()) { changed = true; }
2192                 if ctx.propagate_event(&ev, self.grid_color_selector.id()) { changed = true; }
2193                 if ctx.propagate_event(&ev, self.spinbox_grid_size.id()) { changed = true; }
2194                 if ctx.propagate_event(&ev, self.label_grid_snap.id()) { changed = true; }
2195                 if ctx.propagate_event(&ev, self.slider_zoom.id()) { changed = true; }
2196                 if ctx.propagate_event(&ev, self.toggle_rulers.id()) { changed = true; }
2197                 if ctx.propagate_event(&ev, self.dropdown_units.id()) { changed = true; }
2198                 if ctx.propagate_event(&ev, self.margin_color_selector.id()) { changed = true; }
2199                 if ctx.propagate_event(&ev, self.spinbox_margin_thickness.id()) { changed = true; }
2200                 if ctx.propagate_event(&ev, self.dropdown_grid_units.id()) { changed = true; }
2201             }
2202             5 => {
2203                 for btn in &mut self.layer_buttons {
2204                     if ctx.propagate_event(&ev, btn.id()) { changed = true; }
2205                 }
2206             }
2207             _ => {}
2208         }
2209         changed
2210     }
2211 
2212     fn active_page_widgets_mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
2213         let mut changed = false;
2214         let wev = cce_ui::widget::Event::MouseWheel { delta: *delta, x: px, y: py, local_x: px, local_y: py };
2215         let ctx = &mut self.ui_context;
2216         let selected_page = self.paginator.selected_page();
2217         match selected_page {
2218             0 => {
2219                 if ctx.propagate_event(&wev, self.btn_new_doc.id()) { changed = true; }
2220                 if ctx.propagate_event(&wev, self.btn_open.id()) { changed = true; }
2221                 if self.recent_files_list.wheel(delta, px, py) { changed = true; }
2222                 for btn in &mut self.recent_files_buttons {
2223                     if ctx.propagate_event(&wev, btn.id()) { changed = true; }
2224                 }
2225                 if ctx.propagate_event(&wev, self.btn_save.id()) { changed = true; }
2226                 if ctx.propagate_event(&wev, self.btn_save_as.id()) { changed = true; }
2227                 if ctx.propagate_event(&wev, self.btn_exit.id()) { changed = true; }
2228             }
2229             1 => {
2230                 if ctx.propagate_event(&wev, self.dropdown_margin_units.id()) { changed = true; }
2231                 if ctx.propagate_event(&wev, self.dropdown_presets.id()) { changed = true; }
2232                 if ctx.propagate_event(&wev, self.slider_page_x.id()) { changed = true; }
2233                 if ctx.propagate_event(&wev, self.slider_page_y.id()) { changed = true; }
2234                 if ctx.propagate_event(&wev, self.page_color_selector.id()) { changed = true; }
2235                 if ctx.propagate_event(&wev, self.toggle_margin.id()) { changed = true; }
2236                 if ctx.propagate_event(&wev, self.toggle_word_processor.id()) { changed = true; }
2237                 if ctx.propagate_event(&wev, self.slider_margin_x.id()) { changed = true; }
2238                 if ctx.propagate_event(&wev, self.slider_margin_y.id()) { changed = true; }
2239             }
2240             2 => {
2241                 if self.word_processor_enabled {
2242                     if ctx.propagate_event(&wev, self.font_selector.id()) { changed = true; }
2243                     if ctx.propagate_event(&wev, self.sidebar_size.id()) { changed = true; }
2244                     if ctx.propagate_event(&wev, self.slider_r.id()) { changed = true; }
2245                     if ctx.propagate_event(&wev, self.slider_g.id()) { changed = true; }
2246                     if ctx.propagate_event(&wev, self.slider_b.id()) { changed = true; }
2247                 } else if self.selected_idx.is_some() {
2248                     if ctx.propagate_event(&wev, self.dropdown_align_h.id()) { changed = true; }
2249                     if ctx.propagate_event(&wev, self.dropdown_align_v.id()) { changed = true; }
2250 
2251                     let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
2252                         Some(Element::Text { .. }) => (true, false),
2253                         Some(Element::Vector { .. }) => (false, true),
2254                         _ => (false, false),
2255                     };
2256                     if is_text {
2257                         if ctx.propagate_event(&wev, self.sidebar_x.id()) { changed = true; }
2258                         if ctx.propagate_event(&wev, self.sidebar_y.id()) { changed = true; }
2259                         if ctx.propagate_event(&wev, self.sidebar_w.id()) { changed = true; }
2260                         if ctx.propagate_event(&wev, self.sidebar_h.id()) { changed = true; }
2261                         if ctx.propagate_event(&wev, self.sidebar_text.id()) { changed = true; }
2262                         if ctx.propagate_event(&wev, self.font_selector.id()) { changed = true; }
2263                         if ctx.propagate_event(&wev, self.toggle_multiline.id()) { changed = true; }
2264                         if ctx.propagate_event(&wev, self.dropdown_text_align_h.id()) { changed = true; }
2265                         if ctx.propagate_event(&wev, self.dropdown_text_align_v.id()) { changed = true; }
2266                         if ctx.propagate_event(&wev, self.sidebar_size.id()) { changed = true; }
2267                         if ctx.propagate_event(&wev, self.slider_r.id()) { changed = true; }
2268                         if ctx.propagate_event(&wev, self.slider_g.id()) { changed = true; }
2269                         if ctx.propagate_event(&wev, self.slider_b.id()) { changed = true; }
2270                     } else if is_vector {
2271                         if ctx.propagate_event(&wev, self.sidebar_x.id()) { changed = true; }
2272                         if ctx.propagate_event(&wev, self.sidebar_y.id()) { changed = true; }
2273                         if ctx.propagate_event(&wev, self.sidebar_w.id()) { changed = true; }
2274                         if ctx.propagate_event(&wev, self.sidebar_h.id()) { changed = true; }
2275                         if ctx.propagate_event(&wev, self.sidebar_size.id()) { changed = true; }
2276                         if ctx.propagate_event(&wev, self.dropdown_line_cap.id()) { changed = true; }
2277                         if ctx.propagate_event(&wev, self.slider_r.id()) { changed = true; }
2278                         if ctx.propagate_event(&wev, self.slider_g.id()) { changed = true; }
2279                         if ctx.propagate_event(&wev, self.slider_b.id()) { changed = true; }
2280                     } else {
2281                         if ctx.propagate_event(&wev, self.sidebar_x.id()) { changed = true; }
2282                         if ctx.propagate_event(&wev, self.sidebar_y.id()) { changed = true; }
2283                         if ctx.propagate_event(&wev, self.sidebar_w.id()) { changed = true; }
2284                         if ctx.propagate_event(&wev, self.sidebar_h.id()) { changed = true; }
2285                         if ctx.propagate_event(&wev, self.slider_r.id()) { changed = true; }
2286                         if ctx.propagate_event(&wev, self.slider_g.id()) { changed = true; }
2287                         if ctx.propagate_event(&wev, self.slider_b.id()) { changed = true; }
2288                     }
2289                 } else {
2290                     if ctx.propagate_event(&wev, self.label_sel_status.id()) { changed = true; }
2291                     if ctx.propagate_event(&wev, self.label_sel_desc1.id()) { changed = true; }
2292                     if ctx.propagate_event(&wev, self.label_sel_desc2.id()) { changed = true; }
2293                 }
2294             }
2295             3 => {
2296                 if ctx.propagate_event(&wev, self.btn_add_text.id()) { changed = true; }
2297                 if ctx.propagate_event(&wev, self.btn_add_rect.id()) { changed = true; }
2298                 if ctx.propagate_event(&wev, self.btn_add_banner.id()) { changed = true; }
2299                 if ctx.propagate_event(&wev, self.btn_add_vector.id()) { changed = true; }
2300                 if ctx.propagate_event(&wev, self.label_total_elements.id()) { changed = true; }
2301             }
2302             4 => {
2303                 if ctx.propagate_event(&wev, self.toggle_grid.id()) { changed = true; }
2304                 if ctx.propagate_event(&wev, self.grid_color_selector.id()) { changed = true; }
2305                 if ctx.propagate_event(&wev, self.spinbox_grid_size.id()) { changed = true; }
2306                 if ctx.propagate_event(&wev, self.label_grid_snap.id()) { changed = true; }
2307                 if ctx.propagate_event(&wev, self.slider_zoom.id()) { changed = true; }
2308                 if ctx.propagate_event(&wev, self.toggle_rulers.id()) { changed = true; }
2309                 if ctx.propagate_event(&wev, self.dropdown_units.id()) { changed = true; }
2310                 if ctx.propagate_event(&wev, self.margin_color_selector.id()) { changed = true; }
2311                 if ctx.propagate_event(&wev, self.spinbox_margin_thickness.id()) { changed = true; }
2312                 if ctx.propagate_event(&wev, self.dropdown_grid_units.id()) { changed = true; }
2313             }
2314             5 => {
2315                 for btn in &mut self.layer_buttons {
2316                     if ctx.propagate_event(&wev, btn.id()) { changed = true; }
2317                 }
2318             }
2319             _ => {}
2320         }
2321         changed
2322     }
2323 
2324     fn active_page_widgets_keyboard_input(&mut self, event: &KeyEvent) -> bool {
2325         let mut changed = false;
2326         let kev = cce_ui::widget::Event::KeyInput(event.clone());
2327         let ctx = &mut self.ui_context;
2328         let selected_page = self.paginator.selected_page();
2329         match selected_page {
2330             0 => {
2331                 if self.btn_new_doc.focused(ctx) { if ctx.propagate_event(&kev, self.btn_new_doc.id()) { changed = true; } }
2332                 if self.btn_open.focused(ctx) { if ctx.propagate_event(&kev, self.btn_open.id()) { changed = true; } }
2333                 if self.recent_files_list.focused { if self.recent_files_list.keyboard(event) { changed = true; } }
2334                 for btn in &mut self.recent_files_buttons {
2335                     if btn.focused(ctx) { if ctx.propagate_event(&kev, btn.id()) { changed = true; } }
2336                 }
2337                 if self.btn_save.focused(ctx) { if ctx.propagate_event(&kev, self.btn_save.id()) { changed = true; } }
2338                 if self.btn_save_as.focused(ctx) { if ctx.propagate_event(&kev, self.btn_save_as.id()) { changed = true; } }
2339                 if self.btn_exit.focused(ctx) { if ctx.propagate_event(&kev, self.btn_exit.id()) { changed = true; } }
2340             }
2341             1 => {
2342                 if self.dropdown_margin_units.focused(ctx) { if ctx.propagate_event(&kev, self.dropdown_margin_units.id()) { changed = true; } }
2343                 if self.dropdown_presets.focused(ctx) { if ctx.propagate_event(&kev, self.dropdown_presets.id()) { changed = true; } }
2344                 if self.slider_page_x.focused(ctx) { if ctx.propagate_event(&kev, self.slider_page_x.id()) { changed = true; } }
2345                 if self.slider_page_y.focused(ctx) { if ctx.propagate_event(&kev, self.slider_page_y.id()) { changed = true; } }
2346                 if self.page_color_selector.focused(ctx) { if ctx.propagate_event(&kev, self.page_color_selector.id()) { changed = true; } }
2347                 if self.toggle_margin.focused(ctx) { if ctx.propagate_event(&kev, self.toggle_margin.id()) { changed = true; } }
2348                 if self.toggle_word_processor.focused(ctx) { if ctx.propagate_event(&kev, self.toggle_word_processor.id()) { changed = true; } }
2349                 if self.slider_margin_x.focused(ctx) { if ctx.propagate_event(&kev, self.slider_margin_x.id()) { changed = true; } }
2350                 if self.slider_margin_y.focused(ctx) { if ctx.propagate_event(&kev, self.slider_margin_y.id()) { changed = true; } }
2351             }
2352             2 => {
2353                 if self.word_processor_enabled {
2354                     if self.font_selector.focused(ctx) { if ctx.propagate_event(&kev, self.font_selector.id()) { changed = true; } }
2355                     if self.sidebar_size.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_size.id()) { changed = true; } }
2356                     if self.slider_r.focused(ctx) { if ctx.propagate_event(&kev, self.slider_r.id()) { changed = true; } }
2357                     if self.slider_g.focused(ctx) { if ctx.propagate_event(&kev, self.slider_g.id()) { changed = true; } }
2358                     if self.slider_b.focused(ctx) { if ctx.propagate_event(&kev, self.slider_b.id()) { changed = true; } }
2359                 } else if self.selected_idx.is_some() {
2360                     if self.dropdown_align_h.focused(ctx) { if ctx.propagate_event(&kev, self.dropdown_align_h.id()) { changed = true; } }
2361                     if self.dropdown_align_v.focused(ctx) { if ctx.propagate_event(&kev, self.dropdown_align_v.id()) { changed = true; } }
2362 
2363                     let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
2364                         Some(Element::Text { .. }) => (true, false),
2365                         Some(Element::Vector { .. }) => (false, true),
2366                         _ => (false, false),
2367                     };
2368                     if is_text {
2369                         if self.sidebar_x.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_x.id()) { changed = true; } }
2370                         if self.sidebar_y.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_y.id()) { changed = true; } }
2371                         if self.sidebar_w.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_w.id()) { changed = true; } }
2372                         if self.sidebar_h.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_h.id()) { changed = true; } }
2373                         if self.sidebar_text.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_text.id()) { changed = true; } }
2374                         if self.font_selector.focused(ctx) { if ctx.propagate_event(&kev, self.font_selector.id()) { changed = true; } }
2375                         if self.toggle_multiline.focused(ctx) { if ctx.propagate_event(&kev, self.toggle_multiline.id()) { changed = true; } }
2376                         if self.dropdown_text_align_h.focused(ctx) { if ctx.propagate_event(&kev, self.dropdown_text_align_h.id()) { changed = true; } }
2377                         if self.dropdown_text_align_v.focused(ctx) { if ctx.propagate_event(&kev, self.dropdown_text_align_v.id()) { changed = true; } }
2378                         if self.sidebar_size.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_size.id()) { changed = true; } }
2379                         if self.slider_r.focused(ctx) { if ctx.propagate_event(&kev, self.slider_r.id()) { changed = true; } }
2380                         if self.slider_g.focused(ctx) { if ctx.propagate_event(&kev, self.slider_g.id()) { changed = true; } }
2381                         if self.slider_b.focused(ctx) { if ctx.propagate_event(&kev, self.slider_b.id()) { changed = true; } }
2382                     } else if is_vector {
2383                         if self.sidebar_x.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_x.id()) { changed = true; } }
2384                         if self.sidebar_y.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_y.id()) { changed = true; } }
2385                         if self.sidebar_w.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_w.id()) { changed = true; } }
2386                         if self.sidebar_h.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_h.id()) { changed = true; } }
2387                         if self.sidebar_size.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_size.id()) { changed = true; } }
2388                         if self.dropdown_line_cap.focused(ctx) { if ctx.propagate_event(&kev, self.dropdown_line_cap.id()) { changed = true; } }
2389                         if self.slider_r.focused(ctx) { if ctx.propagate_event(&kev, self.slider_r.id()) { changed = true; } }
2390                         if self.slider_g.focused(ctx) { if ctx.propagate_event(&kev, self.slider_g.id()) { changed = true; } }
2391                         if self.slider_b.focused(ctx) { if ctx.propagate_event(&kev, self.slider_b.id()) { changed = true; } }
2392                     } else {
2393                         if self.sidebar_x.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_x.id()) { changed = true; } }
2394                         if self.sidebar_y.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_y.id()) { changed = true; } }
2395                         if self.sidebar_w.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_w.id()) { changed = true; } }
2396                         if self.sidebar_h.focused(ctx) { if ctx.propagate_event(&kev, self.sidebar_h.id()) { changed = true; } }
2397                         if self.slider_r.focused(ctx) { if ctx.propagate_event(&kev, self.slider_r.id()) { changed = true; } }
2398                         if self.slider_g.focused(ctx) { if ctx.propagate_event(&kev, self.slider_g.id()) { changed = true; } }
2399                         if self.slider_b.focused(ctx) { if ctx.propagate_event(&kev, self.slider_b.id()) { changed = true; } }
2400                     }
2401                 } else {
2402                     if self.label_sel_status.focused(ctx) { if ctx.propagate_event(&kev, self.label_sel_status.id()) { changed = true; } }
2403                     if self.label_sel_desc1.focused(ctx) { if ctx.propagate_event(&kev, self.label_sel_desc1.id()) { changed = true; } }
2404                     if self.label_sel_desc2.focused(ctx) { if ctx.propagate_event(&kev, self.label_sel_desc2.id()) { changed = true; } }
2405                 }
2406             }
2407             3 => {
2408                 if self.btn_add_text.focused(ctx) { if ctx.propagate_event(&kev, self.btn_add_text.id()) { changed = true; } }
2409                 if self.btn_add_rect.focused(ctx) { if ctx.propagate_event(&kev, self.btn_add_rect.id()) { changed = true; } }
2410                 if self.btn_add_banner.focused(ctx) { if ctx.propagate_event(&kev, self.btn_add_banner.id()) { changed = true; } }
2411                 if self.btn_add_vector.focused(ctx) { if ctx.propagate_event(&kev, self.btn_add_vector.id()) { changed = true; } }
2412                 if self.label_total_elements.focused(ctx) { if ctx.propagate_event(&kev, self.label_total_elements.id()) { changed = true; } }
2413             }
2414             4 => {
2415                 if self.toggle_grid.focused(ctx) { if ctx.propagate_event(&kev, self.toggle_grid.id()) { changed = true; } }
2416                 if self.grid_color_selector.focused(ctx) { if ctx.propagate_event(&kev, self.grid_color_selector.id()) { changed = true; } }
2417                 if self.spinbox_grid_size.focused(ctx) { if ctx.propagate_event(&kev, self.spinbox_grid_size.id()) { changed = true; } }
2418                 if self.label_grid_snap.focused(ctx) { if ctx.propagate_event(&kev, self.label_grid_snap.id()) { changed = true; } }
2419                 if self.slider_zoom.focused(ctx) { if ctx.propagate_event(&kev, self.slider_zoom.id()) { changed = true; } }
2420                 if self.toggle_rulers.focused(ctx) { if ctx.propagate_event(&kev, self.toggle_rulers.id()) { changed = true; } }
2421                 if self.dropdown_units.focused(ctx) { if ctx.propagate_event(&kev, self.dropdown_units.id()) { changed = true; } }
2422                 if self.margin_color_selector.focused(ctx) { if ctx.propagate_event(&kev, self.margin_color_selector.id()) { changed = true; } }
2423                 if self.spinbox_margin_thickness.focused(ctx) { if ctx.propagate_event(&kev, self.spinbox_margin_thickness.id()) { changed = true; } }
2424                 if self.dropdown_grid_units.focused(ctx) { if ctx.propagate_event(&kev, self.dropdown_grid_units.id()) { changed = true; } }
2425             }
2426             5 => {
2427                 for btn in &mut self.layer_buttons {
2428                     if btn.focused(ctx) { if ctx.propagate_event(&kev, btn.id()) { changed = true; } }
2429                 }
2430             }
2431             _ => {}
2432         }
2433         changed
2434     }
2435 
2436     /// Per-frame canvas-pan glide/coast: true while `pan_x`/`pan_y` are
2437     /// still moving, so the frame loop keeps drawing.
2438     fn tick_pan(&mut self, dt: f32) -> bool {
2439         self.pan_motion.reconcile(self.pan_x, self.pan_y);
2440         if !self.pan_motion.is_animating() {
2441             return false;
2442         }
2443         let moved = self.pan_motion.tick(dt, Bounds::UNBOUNDED, Bounds::UNBOUNDED);
2444         self.pan_x = self.pan_motion.x.pos();
2445         self.pan_y = self.pan_motion.y.pos();
2446         moved || self.pan_motion.is_animating()
2447     }
2448 
2449     fn active_page_widgets_tick(&mut self, dt: f32) -> bool {
2450         let mut changed = false;
2451         let ctx = &mut self.ui_context;
2452         let selected_page = self.paginator.selected_page();
2453         match selected_page {
2454             0 => {
2455                 if self.btn_new_doc.tick(dt, ctx) { changed = true; }
2456                 if self.btn_open.tick(dt, ctx) { changed = true; }
2457                 // Raise/sink upkeep for the recent-files scrollbar.
2458                 if self.recent_files_list.tick(dt) { changed = true; }
2459                 for btn in &mut self.recent_files_buttons {
2460                     if btn.tick(dt, ctx) { changed = true; }
2461                 }
2462                 if self.btn_save.tick(dt, ctx) { changed = true; }
2463                 if self.btn_save_as.tick(dt, ctx) { changed = true; }
2464                 if self.btn_exit.tick(dt, ctx) { changed = true; }
2465             }
2466             1 => {
2467                 if self.dropdown_margin_units.tick(dt, ctx) { changed = true; }
2468                 if self.dropdown_presets.tick(dt, ctx) { changed = true; }
2469                 if self.slider_page_x.tick(dt, ctx) { changed = true; }
2470                 if self.slider_page_y.tick(dt, ctx) { changed = true; }
2471                 if self.page_color_selector.tick(dt, ctx) { changed = true; }
2472                 if self.toggle_margin.tick(dt, ctx) { changed = true; }
2473                 if self.toggle_word_processor.tick(dt, ctx) { changed = true; }
2474                 if self.slider_margin_x.tick(dt, ctx) { changed = true; }
2475                 if self.slider_margin_y.tick(dt, ctx) { changed = true; }
2476             }
2477             2 => {
2478                 if self.word_processor_enabled {
2479                     if self.font_selector.tick(dt, ctx) { changed = true; }
2480                     if self.sidebar_size.tick(dt, ctx) { changed = true; }
2481                     if self.slider_r.tick(dt, ctx) { changed = true; }
2482                     if self.slider_g.tick(dt, ctx) { changed = true; }
2483                     if self.slider_b.tick(dt, ctx) { changed = true; }
2484                 } else if self.selected_idx.is_some() {
2485                     if self.dropdown_align_h.tick(dt, ctx) { changed = true; }
2486                     if self.dropdown_align_v.tick(dt, ctx) { changed = true; }
2487 
2488                     let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
2489                         Some(Element::Text { .. }) => (true, false),
2490                         Some(Element::Vector { .. }) => (false, true),
2491                         _ => (false, false),
2492                     };
2493                     if is_text {
2494                         if self.sidebar_x.tick(dt, ctx) { changed = true; }
2495                         if self.sidebar_y.tick(dt, ctx) { changed = true; }
2496                         if self.sidebar_w.tick(dt, ctx) { changed = true; }
2497                         if self.sidebar_h.tick(dt, ctx) { changed = true; }
2498                         if self.sidebar_text.tick(dt, ctx) { changed = true; }
2499                         if self.font_selector.tick(dt, ctx) { changed = true; }
2500                         if self.toggle_multiline.tick(dt, ctx) { changed = true; }
2501                         if self.dropdown_text_align_h.tick(dt, ctx) { changed = true; }
2502                         if self.dropdown_text_align_v.tick(dt, ctx) { changed = true; }
2503                         if self.sidebar_size.tick(dt, ctx) { changed = true; }
2504                         if self.slider_r.tick(dt, ctx) { changed = true; }
2505                         if self.slider_g.tick(dt, ctx) { changed = true; }
2506                         if self.slider_b.tick(dt, ctx) { changed = true; }
2507                     } else if is_vector {
2508                         if self.sidebar_x.tick(dt, ctx) { changed = true; }
2509                         if self.sidebar_y.tick(dt, ctx) { changed = true; }
2510                         if self.sidebar_w.tick(dt, ctx) { changed = true; }
2511                         if self.sidebar_h.tick(dt, ctx) { changed = true; }
2512                         if self.sidebar_size.tick(dt, ctx) { changed = true; }
2513                         if self.dropdown_line_cap.tick(dt, ctx) { changed = true; }
2514                         if self.slider_r.tick(dt, ctx) { changed = true; }
2515                         if self.slider_g.tick(dt, ctx) { changed = true; }
2516                         if self.slider_b.tick(dt, ctx) { changed = true; }
2517                     } else {
2518                         if self.sidebar_x.tick(dt, ctx) { changed = true; }
2519                         if self.sidebar_y.tick(dt, ctx) { changed = true; }
2520                         if self.sidebar_w.tick(dt, ctx) { changed = true; }
2521                         if self.sidebar_h.tick(dt, ctx) { changed = true; }
2522                         if self.slider_r.tick(dt, ctx) { changed = true; }
2523                         if self.slider_g.tick(dt, ctx) { changed = true; }
2524                         if self.slider_b.tick(dt, ctx) { changed = true; }
2525                     }
2526                 } else {
2527                     if self.label_sel_status.tick(dt, ctx) { changed = true; }
2528                     if self.label_sel_desc1.tick(dt, ctx) { changed = true; }
2529                     if self.label_sel_desc2.tick(dt, ctx) { changed = true; }
2530                 }
2531             }
2532             3 => {
2533                 if self.btn_add_text.tick(dt, ctx) { changed = true; }
2534                 if self.btn_add_rect.tick(dt, ctx) { changed = true; }
2535                 if self.btn_add_banner.tick(dt, ctx) { changed = true; }
2536                 if self.btn_add_vector.tick(dt, ctx) { changed = true; }
2537                 if self.label_total_elements.tick(dt, ctx) { changed = true; }
2538             }
2539             4 => {
2540                 if self.toggle_grid.tick(dt, ctx) { changed = true; }
2541                 if self.grid_color_selector.tick(dt, ctx) { changed = true; }
2542                 if self.spinbox_grid_size.tick(dt, ctx) { changed = true; }
2543                 if self.label_grid_snap.tick(dt, ctx) { changed = true; }
2544                 if self.slider_zoom.tick(dt, ctx) { changed = true; }
2545                 if self.toggle_rulers.tick(dt, ctx) { changed = true; }
2546                 if self.dropdown_units.tick(dt, ctx) { changed = true; }
2547                 if self.margin_color_selector.tick(dt, ctx) { changed = true; }
2548                 if self.spinbox_margin_thickness.tick(dt, ctx) { changed = true; }
2549                 if self.dropdown_grid_units.tick(dt, ctx) { changed = true; }
2550             }
2551             5 => {
2552                 for btn in &mut self.layer_buttons {
2553                     if btn.tick(dt, ctx) { changed = true; }
2554                 }
2555             }
2556             _ => {}
2557         }
2558         changed
2559     }
2560 }
2561 
2562 /// Adapts the ported view() body's `quads.push`/`.extend` calls to the single paint path.
2563 struct __LayoutQuadSink<'a> {
2564     pc: &'a mut cce_ui::scene::paint::PaintCtx,
2565 }
2566 impl<'a> __LayoutQuadSink<'a> {
2567     fn push(&mut self, q: (f32, f32, f32, f32, [f32; 4])) {
2568         self.pc.quad(cce_ui::scene::layout::Rect { x: q.0, y: q.1, width: q.2, height: q.3 }, q.4);
2569     }
2570     fn extend<I: IntoIterator<Item = (f32, f32, f32, f32, [f32; 4])>>(&mut self, it: I) {
2571         for q in it {
2572             self.push(q);
2573         }
2574     }
2575 }
2576 
2577 impl Application for LayoutApp {
2578     type Message = AppMessage;
2579 
2580     fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
2581         Some(&self.ui_context)
2582     }
2583 
2584     // The engine ticks the exposed context each loop — this is what drives the
2585     // dropdown expand/contract animation frames.
2586     fn ui_context_mut(&mut self) -> Option<&mut cce_ui::context::UiContext> {
2587         Some(&mut self.ui_context)
2588     }
2589 
2590     fn new(_qh: &QueueHandle<EngineState<Self>>, _sender: calloop::channel::Sender<Self::Message>) -> Self {
2591         cce_ui::scale::set_scale_factor(1.0);
2592         let btn_new_doc = Button::new(0.0, 0.0, 240.0, 26.0).with_label("New Document");
2593         let btn_open = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Open");
2594         let btn_save = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Save");
2595         let btn_save_as = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Save As");
2596         let btn_exit = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Exit Application");
2597 
2598         let paginator = Paginator::new(vec![
2599             "File".to_string(),
2600             "Page".to_string(),
2601             "Element".to_string(),
2602             "Canvas".to_string(),
2603             "Guides".to_string(),
2604             "Layers".to_string(),
2605         ]).with_title("LAYOUT");
2606 
2607         // Default paper sheet sizing (Letter)
2608         let page_w = 510.0;
2609         let page_h = 660.0;
2610 
2611         // Initialize elements relative to the paper sheet top-left (0, 0)
2612         let elements = vec![];
2613 
2614         // Create sidebar dropdown for page size presets
2615         let mut dropdown_presets = Dropdown::new(
2616             PRESETS.iter().map(|p| p.name.to_string()).collect(),
2617             0
2618         ).with_label("Page Size Preset");
2619         dropdown_presets.set_rect(0.0, 0.0, 240.0, 26.0);
2620 
2621         let mut slider_page_x = Slider::new().with_range(100.0, 1500.0).with_value((page_w - 100.0) / (1500.0 - 100.0)).with_readout(true).with_label("Page Width (X)");
2622         slider_page_x.set_rect(0.0, 0.0, 240.0, 18.0);
2623         let mut slider_page_y = Slider::new().with_range(100.0, 1500.0).with_value((page_h - 100.0) / (1500.0 - 100.0)).with_readout(true).with_label("Page Height (Y)");
2624         slider_page_y.set_rect(0.0, 0.0, 240.0, 18.0);
2625 
2626         let mut page_color_selector = ColorSelector::new([245, 245, 250])
2627             .with_label("Page Color");
2628         page_color_selector.set_rect(0.0, 0.0, 240.0, 26.0);
2629         let page_color = [0.96, 0.96, 0.98, 1.0];
2630 
2631         let mut toggle_margin = Toggle::new().with_label("Show Page Margin");
2632         toggle_margin.set_rect(0.0, 0.0, 240.0, 26.0);
2633 
2634         let mut toggle_word_processor = Toggle::new().with_label("Word Processor");
2635         toggle_word_processor.set_rect(0.0, 0.0, 240.0, 26.0);
2636 
2637         let wp_text_box = TextBox::new(String::new())
2638             .with_multiline(true)
2639             .with_draw_bg_border(false)
2640             .with_text_color(Some([0x1a, 0x1a, 0x24]))
2641             .with_max_width(None);
2642 
2643         let mut font_selector = FontSelector::new("monospace".to_string()).with_label("Font");
2644         font_selector.set_rect(0.0, 0.0, 240.0, 26.0);
2645 
2646         let mut dropdown_text_align_h = Dropdown::new(
2647             vec![
2648                 "Left".to_string(),
2649                 "Center".to_string(),
2650                 "Right".to_string(),
2651             ],
2652             0,
2653         ).with_label("Horizontal Alignment");
2654         dropdown_text_align_h.set_rect(0.0, 0.0, 240.0, 26.0);
2655 
2656         let mut dropdown_text_align_v = Dropdown::new(
2657             vec![
2658                 "Top".to_string(),
2659                 "Middle".to_string(),
2660                 "Bottom".to_string(),
2661             ],
2662             0,
2663         ).with_label("Vertical Alignment");
2664         dropdown_text_align_v.set_rect(0.0, 0.0, 240.0, 26.0);
2665 
2666         let mut toggle_multiline = Toggle::new().with_label("Multiline Text");
2667         toggle_multiline.set_rect(0.0, 0.0, 240.0, 26.0);
2668 
2669         let mut dropdown_align_h = Dropdown::new(
2670             vec![
2671                 "--".to_string(),
2672                 "Left".to_string(),
2673                 "Center".to_string(),
2674                 "Right".to_string(),
2675             ],
2676             0,
2677         ).with_label("Horizontal Alignment");
2678         dropdown_align_h.set_rect(0.0, 0.0, 240.0, 26.0);
2679 
2680         let mut dropdown_align_v = Dropdown::new(
2681             vec![
2682                 "--".to_string(),
2683                 "Top".to_string(),
2684                 "Middle".to_string(),
2685                 "Bottom".to_string(),
2686             ],
2687             0,
2688         ).with_label("Vertical Alignment");
2689         dropdown_align_v.set_rect(0.0, 0.0, 240.0, 26.0);
2690 
2691         let mut dropdown_margin_units = Dropdown::new(
2692             vec![
2693                 "Pixels (px)".to_string(),
2694                 "Inches (in)".to_string(),
2695                 "Centimeters (cm)".to_string(),
2696                 "Millimeters (mm)".to_string(),
2697             ],
2698             0,
2699         ).with_label("Units");
2700         dropdown_margin_units.set_rect(0.0, 0.0, 240.0, 26.0);
2701 
2702         let mut slider_margin_x = Slider::new().with_range(0.0, 100.0).with_value(0.2).with_readout(true).with_label("Margin Width (X)");
2703         slider_margin_x.set_rect(0.0, 0.0, 240.0, 18.0);
2704         let mut slider_margin_y = Slider::new().with_range(0.0, 100.0).with_value(0.2).with_readout(true).with_label("Margin Height (Y)");
2705         slider_margin_y.set_rect(0.0, 0.0, 240.0, 18.0);
2706 
2707         // Create sidebar spinbox inputs
2708         let mut sidebar_x = Spinbox::new(0, -2000, 2000, 1).with_label("X Position");
2709         sidebar_x.set_rect(0.0, 0.0, 240.0, 26.0);
2710         let mut sidebar_y = Spinbox::new(0, -2000, 2000, 1).with_label("Y Position");
2711         sidebar_y.set_rect(0.0, 0.0, 240.0, 26.0);
2712         let mut sidebar_w = Spinbox::new(100, -2000, 2000, 1).with_label("Width");
2713         sidebar_w.set_rect(0.0, 0.0, 240.0, 26.0);
2714         let mut sidebar_h = Spinbox::new(50, -2000, 2000, 1).with_label("Height");
2715         sidebar_h.set_rect(0.0, 0.0, 240.0, 26.0);
2716 
2717         let mut dropdown_line_cap = Dropdown::new(
2718             vec![
2719                 "Arrow".to_string(),
2720                 "Round".to_string(),
2721                 "Flat".to_string(),
2722             ],
2723             0,
2724         ).with_label("Line End Shape");
2725         dropdown_line_cap.set_rect(0.0, 0.0, 240.0, 26.0);
2726         let mut sidebar_text = TextBox::new(String::new()).with_label("Text Content");
2727         sidebar_text.set_rect(0.0, 0.0, 240.0, 26.0);
2728         let mut sidebar_size = Spinbox::new(14, 4, 120, 1).with_label("Text Size");
2729         sidebar_size.set_rect(0.0, 0.0, 240.0, 26.0);
2730 
2731         // Create color sliders with readout
2732         let mut slider_r = Slider::new().with_range(0.0, 1.0).with_value(0.5).with_readout(true).with_label("Red Color");
2733         slider_r.set_rect(0.0, 0.0, 240.0, 18.0);
2734         let mut slider_g = Slider::new().with_range(0.0, 1.0).with_value(0.5).with_readout(true).with_label("Green Color");
2735         slider_g.set_rect(0.0, 0.0, 240.0, 18.0);
2736         let mut slider_b = Slider::new().with_range(0.0, 1.0).with_value(0.5).with_readout(true).with_label("Blue Color");
2737         slider_b.set_rect(0.0, 0.0, 240.0, 18.0);
2738         let mut slider_zoom = Slider::new().with_range(0.5, 2.0).with_value((1.0 - 0.5) / (2.0 - 0.5)).with_scroll(true).with_readout(true).with_label("Zoom");
2739         slider_zoom.set_rect(0.0, 0.0, 240.0, 18.0);
2740 
2741         // Page 1 Buttons
2742         let mut toggle_grid = Toggle::new().with_label("Toggle Grid");
2743         toggle_grid.set_rect(0.0, 0.0, 240.0, 26.0);
2744         toggle_grid.set_toggled(true);
2745 
2746         let mut grid_color_selector = ColorSelector::new([51, 89, 153])
2747             .with_label("Grid Color");
2748         grid_color_selector.set_rect(0.0, 0.0, 240.0, 26.0);
2749 
2750         let mut spinbox_grid_size = Spinbox::new(15, 5, 100, 1)
2751             .with_decimals(1)
2752             .with_label("Grid Dot Size (px)");
2753         spinbox_grid_size.set_rect(0.0, 0.0, 240.0, 26.0);
2754 
2755         let mut margin_color_selector = ColorSelector::new([51, 128, 217])
2756             .with_label("Margin Guide Color");
2757         margin_color_selector.set_rect(0.0, 0.0, 240.0, 26.0);
2758 
2759         let mut spinbox_margin_thickness = Spinbox::new(1, 1, 20, 1)
2760             .with_label("Margin Thickness (px)");
2761         spinbox_margin_thickness.set_rect(0.0, 0.0, 240.0, 26.0);
2762 
2763 
2764 
2765         let btn_add_text = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Text Box");
2766         let btn_add_rect = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Rectangle");
2767         let btn_add_banner = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Banner");
2768         let btn_add_vector = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Line");
2769 
2770         // Page 3 Buttons
2771         let mut toggle_rulers = Toggle::new().with_label("Show Rulers");
2772         toggle_rulers.set_rect(0.0, 0.0, 240.0, 26.0);
2773         let mut dropdown_units = Dropdown::new(
2774             vec![
2775                 "Pixels (px)".to_string(),
2776                 "Inches (in)".to_string(),
2777                 "Centimeters (cm)".to_string(),
2778                 "Millimeters (mm)".to_string(),
2779             ],
2780             0,
2781         ).with_label("Ruler Units");
2782         dropdown_units.set_rect(0.0, 0.0, 240.0, 26.0);
2783 
2784         let mut dropdown_grid_units = Dropdown::new(
2785             vec![
2786                 "Pixels (px)".to_string(),
2787                 "Inches (in)".to_string(),
2788                 "Centimeters (cm)".to_string(),
2789                 "Millimeters (mm)".to_string(),
2790             ],
2791             0,
2792         ).with_label("Grid Units");
2793         dropdown_grid_units.set_rect(0.0, 0.0, 240.0, 26.0);
2794 
2795         let mut label_sel_status = Label::new("No Selection").with_color([0x83, 0x83, 0x8a]);
2796         label_sel_status.set_rect(0.0, 0.0, 240.0, 18.0);
2797 
2798         let mut label_sel_desc1 = Label::new("Click canvas elements").with_color([0x60, 0x60, 0x68]).with_font_size(10.0);
2799         label_sel_desc1.set_rect(0.0, 0.0, 240.0, 14.0);
2800 
2801         let mut label_sel_desc2 = Label::new("to edit properties.").with_color([0x60, 0x60, 0x68]).with_font_size(10.0);
2802         label_sel_desc2.set_rect(0.0, 0.0, 240.0, 14.0);
2803 
2804         let mut label_grid_snap = Toggle::new().with_label("Grid Snapping: ON (20px)");
2805         label_grid_snap.set_rect(0.0, 0.0, 240.0, 26.0);
2806         label_grid_snap.set_toggled(true);
2807 
2808         let mut label_total_elements = Label::new("Total Elements: 0");
2809         label_total_elements.set_rect(0.0, 0.0, 240.0, 18.0);
2810 
2811         let recent_files = Self::load_recent_files();
2812         // Designer raise/sink treatment, self-contained for a framed region:
2813         // push_prims lays the sunk bar under the translucent bg fill (a dim
2814         // ghost inside the well) and the raised bar on top while scrolling.
2815         let recent_files_list = ScrollRegion::new(22.0, 2.0).with_sink_behind(true);
2816         let mut recent_files_buttons = Vec::new();
2817         for file in &recent_files {
2818             let label = file.file_name()
2819                 .map(|n| n.to_string_lossy().to_string())
2820                 .unwrap_or_else(|| file.to_string_lossy().to_string());
2821             let btn = Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(&label);
2822             recent_files_buttons.push(btn);
2823         }
2824 
2825         let mut app = Self {
2826             keys: LayoutKeys::load(),
2827             btn_new_doc,
2828             btn_open,
2829             recent_files,
2830             recent_files_list,
2831             recent_files_buttons,
2832             btn_exit,
2833             btn_save,
2834             btn_save_as,
2835             paginator,
2836             elements,
2837             selected_idx: None,
2838             dragging: None,
2839             grid_enabled: true,
2840             width: 1024,
2841             height: 768,
2842             scale_factor: 1.0,
2843             text_prims: Vec::new(),
2844             font_system: cce_ui::create_font_system(),
2845             needs_rebuild: true,
2846 
2847             dropdown_presets,
2848             slider_page_x,
2849             slider_page_y,
2850             page_color_selector,
2851             page_color,
2852             toggle_margin,
2853             dropdown_margin_units,
2854             slider_margin_x,
2855             slider_margin_y,
2856             margin_enabled: false,
2857             margin_x: 20.0,
2858             margin_y: 20.0,
2859             toggle_word_processor,
2860             word_processor_enabled: false,
2861             wp_text_box,
2862             sidebar_x,
2863             sidebar_y,
2864             sidebar_w,
2865             sidebar_h,
2866             sidebar_text,
2867             sidebar_size,
2868             font_selector,
2869             dropdown_text_align_h,
2870             dropdown_text_align_v,
2871             toggle_multiline,
2872             dropdown_align_h,
2873             dropdown_align_v,
2874             dropdown_line_cap,
2875             wp_base_font_size: 14.0,
2876 
2877             slider_r,
2878             slider_g,
2879             slider_b,
2880             slider_zoom,
2881 
2882             toggle_grid,
2883             grid_color_selector,
2884             grid_color: [0.20, 0.35, 0.60, 0.8],
2885             grid_size: 1.5,
2886             spinbox_grid_size,
2887             dropdown_grid_units,
2888             margin_color_selector,
2889             margin_color: [0.20, 0.50, 0.85, 0.35],
2890             spinbox_margin_thickness,
2891             margin_thickness: 1.0,
2892             btn_add_text,
2893             btn_add_rect,
2894             btn_add_banner,
2895             btn_add_vector,
2896             toggle_rulers,
2897             dropdown_units,
2898 
2899             label_sel_status,
2900             label_sel_desc1,
2901             label_sel_desc2,
2902             label_grid_snap,
2903             label_total_elements,
2904 
2905             last_selected: None,
2906             page_w,
2907             page_h,
2908             pan_x: 0.0,
2909             pan_y: 0.0,
2910             pan_motion: ScrollMotion::new(),
2911             sidebar_quads: Vec::new(),
2912             current_file_path: None,
2913             layer_buttons: Vec::new(),
2914             ui_context: cce_ui::context::UiContext::new(),
2915             last_saved_document: LayoutDocument {
2916                 page_w,
2917                 page_h,
2918                 page_color,
2919                 margin_enabled: false,
2920                 margin_x: 20.0,
2921                 margin_y: 20.0,
2922                 word_processor_enabled: false,
2923                 wp_text: String::new(),
2924                 elements: Vec::new(),
2925                 grid_color: [0.20, 0.35, 0.60, 0.8],
2926                 grid_size: 1.5,
2927                 margin_color: [0.20, 0.50, 0.85, 0.35],
2928                 margin_thickness: 1.0,
2929                 grid_enabled: true,
2930                 grid_units: 0,
2931                 zoom: 0.33333334,
2932                 rulers_enabled: false,
2933                 ruler_units: 0,
2934             },
2935         };
2936 
2937         app.paginator.set_selected_page(2);
2938         app.sync_sidebar_fields();
2939         app.sync_page_units();
2940         app.rebuild_text_items();
2941         app
2942     }
2943 
2944     fn settings(&self) -> WindowSettings {
2945         let filename = match &self.current_file_path {
2946             Some(path) => path.file_name()
2947                 .and_then(|n| n.to_str())
2948                 .unwrap_or("None"),
2949             None => "None",
2950         };
2951         let unsaved_suffix = if self.has_unsaved_changes() {
2952             " (Unsaved Changes)"
2953         } else {
2954             ""
2955         };
2956         WindowSettings {
2957             title: format!("Clear Layout Interface - {}{}", filename, unsaved_suffix),
2958             app_id: "cce-layout-interface".to_string(),
2959             width: 1024,
2960             height: 768,
2961             fullscreen: false,
2962             min_size: Some((800, 600)),
2963         }
2964     }
2965 
2966     fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool) {
2967         match msg {
2968             AppMessage::Exit => {
2969                 *exit = true;
2970             }
2971             AppMessage::Save => {
2972                 if let Some(ref path) = self.current_file_path {
2973                     if let Err(e) = self.save_document(path) {
2974                         log::error!("Failed to save document: {}", e);
2975                     } else {
2976                         log::info!("Successfully saved layout to {:?}", path);
2977                         self.last_saved_document = self.get_current_document();
2978                         self.add_recent_file(path.clone());
2979                     }
2980                 } else {
2981                     self.perform_save_as();
2982                 }
2983             }
2984             AppMessage::SaveAs => {
2985                 self.perform_save_as();
2986             }
2987             AppMessage::Open => {
2988                 self.perform_open();
2989             }
2990             AppMessage::NewDocument => {
2991                 self.elements.clear();
2992                 self.selected_idx = None;
2993                 self.dragging = None;
2994                 self.pan_x = 0.0;
2995                 self.pan_y = 0.0;
2996                 self.page_color = [0.96, 0.96, 0.98, 1.0];
2997                 self.page_color_selector.color = [245, 245, 250];
2998                 self.grid_color = [0.20, 0.35, 0.60, 0.8];
2999                 self.grid_color_selector.color = [51, 89, 153];
3000                 self.grid_size = 1.5;
3001                 self.spinbox_grid_size.value = 15;
3002                 self.margin_color = [0.20, 0.50, 0.85, 0.35];
3003                 self.margin_color_selector.color = [51, 128, 217];
3004                 self.margin_thickness = 1.0;
3005                 self.spinbox_margin_thickness.value = 1;
3006                 self.margin_enabled = false;
3007                 self.margin_x = 20.0;
3008                 self.margin_y = 20.0;
3009                 self.toggle_margin.set_toggled(false);
3010 
3011                 self.dropdown_presets.selected = 0;
3012                 self.page_w = PRESETS[0].w;
3013                 self.page_h = PRESETS[0].h;
3014                 let val_x = (self.page_w - 100.0) / (1500.0 - 100.0);
3015                 self.slider_page_x.set_value(val_x);
3016                 let val_y = (self.page_h - 100.0) / (1500.0 - 100.0);
3017                 self.slider_page_y.set_value(val_y);
3018 
3019                 self.dropdown_margin_units.selected = 0;
3020                 self.sync_page_units();
3021 
3022                 self.grid_enabled = true;
3023                 self.toggle_grid.set_toggled(true);
3024                 self.label_grid_snap.set_toggled(true);
3025                 self.dropdown_grid_units.selected = 0;
3026                 self.slider_zoom.set_value(0.33333334);
3027                 self.toggle_rulers.set_toggled(false);
3028                 self.dropdown_units.selected = 0;
3029 
3030                 self.toggle_word_processor.set_toggled(false);
3031                 self.word_processor_enabled = false;
3032                 self.wp_text_box.text = String::new();
3033                 self.wp_text_box.edit_buffer = String::new();
3034                 self.wp_text_box.editing = false;
3035 
3036                 self.current_file_path = None;
3037 
3038                 self.rebuild_layers_tab_widgets();
3039                 self.sync_sidebar_fields();
3040                 self.last_saved_document = self.get_current_document();
3041                 *needs_rebuild = true;
3042                 self.needs_rebuild = true;
3043             }
3044             AppMessage::AddText => {
3045                 let next_idx = self.elements.len();
3046                 self.elements.push(Element::Text {
3047                     text: "New Text Block".to_string(),
3048                     x: 40.0,
3049                     y: 100.0,
3050                     w: 200.0,
3051                     h: 30.0,
3052                     font_size: 14.0,
3053                     color: [0.0, 0.0, 0.0, 1.0],
3054                     font_family: "monospace".to_string(),
3055                     align_h: TextAlignH::Left,
3056                     align_v: TextAlignV::Top,
3057                     multiline: true,
3058                 });
3059                 self.selected_idx = Some(next_idx);
3060                 self.rebuild_layers_tab_widgets();
3061                 self.sync_sidebar_fields();
3062                 *needs_rebuild = true;
3063                 self.needs_rebuild = true;
3064             }
3065             AppMessage::AddRectangle => {
3066                 let next_idx = self.elements.len();
3067                 self.elements.push(Element::Shape {
3068                     shape_type: ShapeType::Rectangle,
3069                     x: 40.0,
3070                     y: 100.0,
3071                     w: 150.0,
3072                     h: 100.0,
3073                     color: [0.2, 0.6, 0.4, 1.0],
3074                 });
3075                 self.selected_idx = Some(next_idx);
3076                 self.rebuild_layers_tab_widgets();
3077                 self.sync_sidebar_fields();
3078                 *needs_rebuild = true;
3079                 self.needs_rebuild = true;
3080             }
3081             AppMessage::AddBanner => {
3082                 let next_idx = self.elements.len();
3083                 self.elements.push(Element::Shape {
3084                     shape_type: ShapeType::Banner,
3085                     x: 40.0,
3086                     y: 100.0,
3087                     w: 300.0,
3088                     h: 50.0,
3089                     color: [0.8, 0.4, 0.2, 1.0],
3090                 });
3091                 self.selected_idx = Some(next_idx);
3092                 self.rebuild_layers_tab_widgets();
3093                 self.sync_sidebar_fields();
3094                 *needs_rebuild = true;
3095                 self.needs_rebuild = true;
3096             }
3097             AppMessage::AddVector => {
3098                 let next_idx = self.elements.len();
3099                 self.elements.push(Element::Vector {
3100                     x1: 40.0,
3101                     y1: 100.0,
3102                     x2: 240.0,
3103                     y2: 100.0,
3104                     stroke_width: 4.0,
3105                     color: [0.2, 0.4, 0.8, 1.0],
3106                     line_cap: LineCap::Arrow,
3107                 });
3108                 self.selected_idx = Some(next_idx);
3109                 self.rebuild_layers_tab_widgets();
3110                 self.sync_sidebar_fields();
3111                 *needs_rebuild = true;
3112                 self.needs_rebuild = true;
3113             }
3114         }
3115     }
3116 
3117     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
3118         if self.paginator.tick(dt, &mut self.ui_context) {
3119             *needs_rebuild = true;
3120             self.needs_rebuild = true;
3121         }
3122         if self.tick_pan(dt) {
3123             *needs_rebuild = true;
3124             self.needs_rebuild = true;
3125         }
3126         if self.active_page_widgets_tick(dt) {
3127             *needs_rebuild = true;
3128             self.needs_rebuild = true;
3129         }
3130         let _ = self.page_color_selector.tick(dt, &mut self.ui_context);
3131         let r = self.page_color_selector.color[0] as f32 / 255.0;
3132         let g = self.page_color_selector.color[1] as f32 / 255.0;
3133         let b = self.page_color_selector.color[2] as f32 / 255.0;
3134         let new_col = [r, g, b, 1.0];
3135         if self.page_color != new_col {
3136             self.page_color = new_col;
3137             *needs_rebuild = true;
3138             self.needs_rebuild = true;
3139         }
3140 
3141         let mut new_grid_enabled = self.grid_enabled;
3142         if self.toggle_grid.toggled() != self.grid_enabled {
3143             new_grid_enabled = self.toggle_grid.toggled();
3144         } else if self.label_grid_snap.toggled() != self.grid_enabled {
3145             new_grid_enabled = self.label_grid_snap.toggled();
3146         }
3147 
3148         if self.grid_enabled != new_grid_enabled {
3149             self.grid_enabled = new_grid_enabled;
3150             self.toggle_grid.set_toggled(new_grid_enabled);
3151             self.label_grid_snap.set_toggled(new_grid_enabled);
3152             self.sync_sidebar_fields();
3153             *needs_rebuild = true;
3154             self.needs_rebuild = true;
3155         }
3156 
3157         let _ = self.grid_color_selector.tick(dt, &mut self.ui_context);
3158         let gr = self.grid_color_selector.color[0] as f32 / 255.0;
3159         let gg = self.grid_color_selector.color[1] as f32 / 255.0;
3160         let gb = self.grid_color_selector.color[2] as f32 / 255.0;
3161         let new_grid_col = [gr, gg, gb, 0.8];
3162         if self.grid_color != new_grid_col {
3163             self.grid_color = new_grid_col;
3164             *needs_rebuild = true;
3165             self.needs_rebuild = true;
3166         }
3167 
3168         let new_grid_size = if self.spinbox_grid_size.editing {
3169             self.spinbox_grid_size.edit_buffer.parse::<f32>().unwrap_or(1.5)
3170         } else {
3171             self.spinbox_grid_size.value as f32 / 10.0
3172         };
3173         if self.grid_size != new_grid_size {
3174             self.grid_size = new_grid_size;
3175             *needs_rebuild = true;
3176             self.needs_rebuild = true;
3177         }
3178 
3179         let _ = self.margin_color_selector.tick(dt, &mut self.ui_context);
3180         let mr = self.margin_color_selector.color[0] as f32 / 255.0;
3181         let mg = self.margin_color_selector.color[1] as f32 / 255.0;
3182         let mb = self.margin_color_selector.color[2] as f32 / 255.0;
3183         let new_margin_col = [mr, mg, mb, 0.35];
3184         if self.margin_color != new_margin_col {
3185             self.margin_color = new_margin_col;
3186             *needs_rebuild = true;
3187             self.needs_rebuild = true;
3188         }
3189 
3190         let new_margin_thickness = if self.spinbox_margin_thickness.editing {
3191             self.spinbox_margin_thickness.edit_buffer.parse::<f32>().unwrap_or(1.0)
3192         } else {
3193             self.spinbox_margin_thickness.value as f32
3194         };
3195         if self.margin_thickness != new_margin_thickness {
3196             self.margin_thickness = new_margin_thickness;
3197             *needs_rebuild = true;
3198             self.needs_rebuild = true;
3199         }
3200 
3201         // Preset dropdown selection change
3202         if self.dropdown_presets.take_change() {
3203             let sel = self.dropdown_presets.selected;
3204             if sel < PRESETS.len() - 1 {
3205                 self.page_w = PRESETS[sel].w;
3206                 self.page_h = PRESETS[sel].h;
3207                 let val_x = (self.page_w - 100.0) / (1500.0 - 100.0);
3208                 self.slider_page_x.set_value(val_x);
3209                 let val_y = (self.page_h - 100.0) / (1500.0 - 100.0);
3210                 self.slider_page_y.set_value(val_y);
3211                 self.pan_x = 0.0;
3212                 self.pan_y = 0.0;
3213                 *needs_rebuild = true;
3214                 self.needs_rebuild = true;
3215             }
3216         }
3217 
3218         // Margin units dropdown change
3219         if self.dropdown_margin_units.take_change() {
3220             self.sync_page_units();
3221             *needs_rebuild = true;
3222             self.needs_rebuild = true;
3223         }
3224 
3225         // Ruler units dropdown change
3226         if self.dropdown_units.take_change() {
3227             *needs_rebuild = true;
3228             self.needs_rebuild = true;
3229         }
3230 
3231         // Grid units dropdown change
3232         if self.dropdown_grid_units.take_change() {
3233             self.sync_sidebar_fields();
3234             *needs_rebuild = true;
3235             self.needs_rebuild = true;
3236         }
3237 
3238         let unit_idx = self.dropdown_margin_units.selected;
3239         let px_per_unit = match unit_idx {
3240             0 => 1.0,           // Pixels
3241             1 => 60.0,          // Inches
3242             2 => 23.622047,     // Centimeters
3243             3 => 2.3622047,     // Millimeters
3244             _ => 1.0,
3245         };
3246 
3247         // Page width slider change
3248         let page_w = self.slider_page_x.get_scaled_value() * px_per_unit;
3249         if (self.page_w - page_w).abs() > 0.01 {
3250             self.page_w = page_w;
3251             self.dropdown_presets.selected = PRESETS.len() - 1; // "Custom"
3252             *needs_rebuild = true;
3253             self.needs_rebuild = true;
3254         }
3255 
3256         // Page height slider change
3257         let page_h = self.slider_page_y.get_scaled_value() * px_per_unit;
3258         if (self.page_h - page_h).abs() > 0.01 {
3259             self.page_h = page_h;
3260             self.dropdown_presets.selected = PRESETS.len() - 1; // "Custom"
3261             *needs_rebuild = true;
3262             self.needs_rebuild = true;
3263         }
3264 
3265         let margin_enabled = self.toggle_margin.toggled();
3266         if self.margin_enabled != margin_enabled {
3267             self.margin_enabled = margin_enabled;
3268             *needs_rebuild = true;
3269             self.needs_rebuild = true;
3270         }
3271 
3272         let margin_x = self.slider_margin_x.get_scaled_value() * px_per_unit;
3273         if (self.margin_x - margin_x).abs() > 0.01 {
3274             self.margin_x = margin_x;
3275             *needs_rebuild = true;
3276             self.needs_rebuild = true;
3277         }
3278 
3279         let margin_y = self.slider_margin_y.get_scaled_value() * px_per_unit;
3280         if (self.margin_y - margin_y).abs() > 0.01 {
3281             self.margin_y = margin_y;
3282             *needs_rebuild = true;
3283             self.needs_rebuild = true;
3284         }
3285 
3286         let wp_enabled = self.toggle_word_processor.toggled();
3287         if self.word_processor_enabled != wp_enabled {
3288             self.word_processor_enabled = wp_enabled;
3289             if wp_enabled {
3290                 self.wp_text_box.focus();
3291                 self.selected_idx = None;
3292                 self.dragging = None;
3293             } else {
3294                 self.wp_text_box.unfocus();
3295             }
3296             self.sync_sidebar_fields();
3297             *needs_rebuild = true;
3298             self.needs_rebuild = true;
3299         }
3300     }
3301 
3302     fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
3303         // Id-rooted router: the word-processor box dispatches by id — keep its
3304         // registration fresh (idempotent) whether or not the mode is enabled.
3305         {
3306             let (id, ptr) = (self.wp_text_box.id(), self.wp_text_box.as_ptr_mut());
3307             self.ui_context.register_widget(id, ptr);
3308         }
3309         // Phase 6aj single paint path: view() geometry + view_vectors + text prims (single-run
3310         // via text_with, boxed canvas text via text_boxed). App FontSystem is bundled now (was
3311         // _with_system_fonts) — kept only for widgets' prepare_text; text renders via the engine
3312         // cache, fixing the face-ID invisibility.
3313         let mut __pc = cce_ui::scene::paint::PaintCtx::new();
3314         // The standard root plate (cce-ui PlateSpec::window).
3315         __pc.root_plate(size.width as f32, size.height as f32);
3316         let quads = &mut __LayoutQuadSink { pc: &mut __pc };
3317         let size_changed = self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale;
3318         if self.needs_rebuild || size_changed {
3319             self.width = size.width as u32;
3320             self.height = size.height as u32;
3321             self.scale_factor = scale;
3322 
3323             // Set paginator bounds in the sidebar region
3324             cce_ui::scale::set_scale_factor(scale as f32);
3325             let (side_x, side_y, side_w, side_h) = self.sidebar_rect();
3326             self.paginator.set_rect(side_x, side_y, side_w, side_h);
3327 
3328             self.rebuild_layers_tab_widgets();
3329             self.rebuild_text_items();
3330             self.needs_rebuild = false;
3331         }
3332 
3333 
3334 
3335         // 3. Render Canvas & centered paper sheet
3336         let (canvas_x, canvas_y, canvas_w, canvas_h) = self.canvas_rect();
3337         let zoom = self.render_zoom();
3338 
3339         // Centered Paper Position
3340         let (page_x, page_y) = self.page_origin();
3341 
3342         // Dark slate canvas backdrop
3343         let mut canvas_bg = [0.12, 0.12, 0.15, 1.0];
3344         if let Some(opacity) = cce_ui::color::read_opacity_if_configured() {
3345             canvas_bg[3] = opacity;
3346         }
3347         quads.push((canvas_x, canvas_y, canvas_w, canvas_h, canvas_bg));
3348 
3349         // Axis Rulers
3350         // TODO(style): the 20px band, the 12/6 tick lengths and the corner
3351         // block are the ruler's own geometry, not spacing on a plate.
3352         if self.toggle_rulers.toggled() {
3353             let ruler_bg = [0.18, 0.18, 0.22, 1.0];
3354             let tick_color = [0.45, 0.45, 0.50, 0.8];
3355             let border_col = [0.30, 0.30, 0.35, 1.0];
3356 
3357             // Rulers backgrounds
3358             quads.push((page_x - 20.0, page_y - 20.0, self.page_w * zoom + 20.0, 20.0, ruler_bg));
3359             quads.push((page_x - 20.0, page_y - 20.0, 20.0, self.page_h * zoom + 20.0, ruler_bg));
3360 
3361             // Outer borders
3362             quads.push((page_x - 20.0, page_y, self.page_w * zoom + 20.0, 1.0, border_col));
3363             quads.push((page_x, page_y - 20.0, 1.0, self.page_h * zoom + 20.0, border_col));
3364 
3365             let unit_idx = self.dropdown_units.selected;
3366             let (px_per_unit, tick_step, label_step, _) = match unit_idx {
3367                 0 => (1.0, 50.0, 100.0, 0),       // Pixels
3368                 1 => (60.0, 0.25, 1.0, 0),        // Inches
3369                 2 => (23.622047, 0.5, 1.0, 0),    // Centimeters
3370                 3 => (2.3622047, 5.0, 10.0, 0),   // Millimeters
3371                 _ => (1.0, 50.0, 100.0, 0),
3372             };
3373 
3374             // X Axis Ticks
3375             let max_unit_w = self.page_w / px_per_unit;
3376             let mut val = 0.0;
3377             while val <= max_unit_w + 0.001 {
3378                 let tx = page_x + val * px_per_unit * zoom;
3379                 let is_major = (val / label_step).round() * label_step;
3380                 if (val - is_major).abs() < 0.001 {
3381                     quads.push((tx, page_y - 12.0, 1.0, 12.0, tick_color));
3382                 } else {
3383                     quads.push((tx, page_y - 6.0, 1.0, 6.0, tick_color));
3384                 }
3385                 val += tick_step;
3386             }
3387 
3388             // Y Axis Ticks
3389             let max_unit_h = self.page_h / px_per_unit;
3390             let mut val = 0.0;
3391             while val <= max_unit_h + 0.001 {
3392                 let ty = page_y + val * px_per_unit * zoom;
3393                 let is_major = (val / label_step).round() * label_step;
3394                 if (val - is_major).abs() < 0.001 {
3395                     quads.push((page_x - 12.0, ty, 12.0, 1.0, tick_color));
3396                 } else {
3397                     quads.push((page_x - 6.0, ty, 6.0, 1.0, tick_color));
3398                 }
3399                 val += tick_step;
3400             }
3401 
3402             // Corner block
3403             quads.push((page_x - 20.0, page_y - 20.0, 20.0, 20.0, [0.15, 0.15, 0.18, 1.0]));
3404             quads.push((page_x - 20.0, page_y - 20.0, 20.0, 1.0, border_col));
3405             quads.push((page_x - 20.0, page_y - 20.0, 1.0, 20.0, border_col));
3406         }
3407 
3408         // Paper drop shadow
3409         quads.push((page_x + 3.0, page_y + 3.0, self.page_w * zoom, self.page_h * zoom, [0.05, 0.05, 0.08, 0.35]));
3410 
3411         // Paper sheet backing (Elegant Off-White)
3412         quads.push((page_x, page_y, self.page_w * zoom, self.page_h * zoom, self.page_color));
3413 
3414         // Thin paper border outline
3415         let border_col = [0.75, 0.75, 0.80, 0.5];
3416         quads.push((page_x, page_y, self.page_w * zoom, 1.0, border_col));
3417         quads.push((page_x, page_y + self.page_h * zoom - 1.0, self.page_w * zoom, 1.0, border_col));
3418         quads.push((page_x, page_y, 1.0, self.page_h * zoom, border_col));
3419         quads.push((page_x + self.page_w * zoom - 1.0, page_y, 1.0, self.page_h * zoom, border_col));
3420 
3421         // Draw page margin inside the paper sheet if enabled
3422         if self.margin_enabled {
3423             let mx = page_x + self.margin_x * zoom;
3424             let my = page_y + self.margin_y * zoom;
3425             let mw = (self.page_w - 2.0 * self.margin_x) * zoom;
3426             let mh = (self.page_h - 2.0 * self.margin_y) * zoom;
3427             if mw > 0.0 && mh > 0.0 {
3428                 let margin_col = self.margin_color;
3429                 let thick = self.margin_thickness;
3430                 quads.push((mx, my, mw, thick, margin_col));
3431                 quads.push((mx, my + mh - thick, mw, thick, margin_col));
3432                 quads.push((mx, my, thick, mh, margin_col));
3433                 quads.push((mx + mw - thick, my, thick, mh, margin_col));
3434             }
3435         }
3436 
3437         // 4. Snapping Grid inside the paper bounds
3438         if self.grid_enabled {
3439             let spacing = self.grid_spacing() * zoom;
3440             let mut cx = spacing;
3441             let dot_size = (self.grid_size * zoom).max(1.0);
3442             while cx < self.page_w * zoom {
3443                 let mut cy = spacing;
3444                 while cy < self.page_h * zoom {
3445                     quads.push((page_x + cx, page_y + cy, dot_size, dot_size, self.grid_color));
3446                     cy += spacing;
3447                 }
3448                 cx += spacing;
3449             }
3450         }
3451 
3452         // 5. Render Layout Elements or Word Processor
3453         if self.word_processor_enabled {
3454             let mx = page_x + self.margin_x * zoom;
3455             let my = page_y + self.margin_y * zoom;
3456             let mw = (self.page_w - 2.0 * self.margin_x) * zoom;
3457             let mh = (self.page_h - 2.0 * self.margin_y) * zoom;
3458             if mw > 0.0 && mh > 0.0 {
3459                 self.wp_text_box.set_rect(mx, my, mw, mh);
3460                 self.wp_text_box.font_size = self.wp_base_font_size * zoom;
3461                 quads.extend(self.wp_text_box.extra_quads());
3462             }
3463         } else {
3464             for (idx, element) in self.elements.iter().enumerate() {
3465                 match element {
3466                     Element::Text { text: _, x, y, w, h, font_size: _, color: _, .. } => {
3467                         // Translucent text block container overlay
3468                         quads.push((page_x + *x * zoom, page_y + *y * zoom, *w * zoom, *h * zoom, [0.20, 0.30, 0.70, 0.05]));
3469 
3470                         let border_color = [0.45, 0.45, 0.55, 0.22];
3471                         quads.push((page_x + *x * zoom, page_y + *y * zoom, *w * zoom, 1.0, border_color));
3472                         quads.push((page_x + *x * zoom, page_y + *y * zoom + *h * zoom - 1.0, *w * zoom, 1.0, border_color));
3473                         quads.push((page_x + *x * zoom, page_y + *y * zoom, 1.0, *h * zoom, border_color));
3474                         quads.push((page_x + *x * zoom + *w * zoom - 1.0, page_y + *y * zoom, 1.0, *h * zoom, border_color));
3475                     }
3476                     Element::Shape { shape_type, x, y, w, h, color } => {
3477                         match shape_type {
3478                             ShapeType::Rectangle => {
3479                                 quads.push((page_x + *x * zoom, page_y + *y * zoom, *w * zoom, *h * zoom, *color));
3480                             }
3481                             ShapeType::Banner => {
3482                                 // Banner drop shadow
3483                                 quads.push((page_x + *x * zoom + 3.0, page_y + *y * zoom + 3.0, *w * zoom, *h * zoom, [0.05, 0.05, 0.08, 0.3]));
3484 
3485                                 quads.push((page_x + *x * zoom, page_y + *y * zoom, *w * zoom, *h * zoom, *color));
3486 
3487                                 let inner_col = [1.0, 1.0, 1.0, 0.2];
3488                                 quads.push((page_x + *x * zoom + 2.0, page_y + *y * zoom + 2.0, *w * zoom - 4.0, 1.0, inner_col));
3489                                 quads.push((page_x + *x * zoom + 2.0, page_y + *y * zoom + *h * zoom - 3.0, *w * zoom - 4.0, 1.0, inner_col));
3490                                 quads.push((page_x + *x * zoom + 2.0, page_y + *y * zoom + 2.0, 1.0, *h * zoom - 4.0, inner_col));
3491                                 quads.push((page_x + *x * zoom + *w * zoom - 3.0, page_y + *y * zoom + 2.0, 1.0, *h * zoom - 4.0, inner_col));
3492                             }
3493                         }
3494                     }
3495                     Element::Vector { .. } => {}
3496                 }
3497 
3498                 // Draw highlight border if selected
3499                 if self.selected_idx == Some(idx) {
3500                     let (ex, ey, ew, eh) = match element {
3501                         Element::Text { x, y, w, h, .. } => (*x, *y, *w, *h),
3502                         Element::Shape { x, y, w, h, .. } => (*x, *y, *w, *h),
3503                         Element::Vector { x1, y1, x2, y2, .. } => {
3504                             let x = x1.min(*x2);
3505                             let y = y1.min(*y2);
3506                             let w = (x1 - x2).abs();
3507                             let h = (y1 - y2).abs();
3508                             (x, y, w, h)
3509                         }
3510                     };
3511                     let sel_col = [0.15, 0.45, 0.90, 1.0]; // Bright neon blue
3512                     quads.push((page_x + ex * zoom - 1.0, page_y + ey * zoom - 1.0, ew * zoom + 2.0, 1.0, sel_col));
3513                     quads.push((page_x + ex * zoom - 1.0, page_y + ey * zoom + eh * zoom, ew * zoom + 2.0, 1.0, sel_col));
3514                     quads.push((page_x + ex * zoom - 1.0, page_y + ey * zoom - 1.0, 1.0, eh * zoom + 2.0, sel_col));
3515                     quads.push((page_x + ex * zoom + ew * zoom, page_y + ey * zoom - 1.0, 1.0, eh * zoom + 2.0, sel_col));
3516 
3517                     // Corner handles
3518                     let hs = 5.0;
3519                     let hc = [1.0, 1.0, 1.0, 1.0];
3520                     let hborder = [0.15, 0.45, 0.90, 1.0];
3521                     let handles = [
3522                         (page_x + ex * zoom - 2.0, page_y + ey * zoom - 2.0),
3523                         (page_x + ex * zoom + ew * zoom - 3.0, page_y + ey * zoom - 2.0),
3524                         (page_x + ex * zoom - 2.0, page_y + ey * zoom + eh * zoom - 3.0),
3525                         (page_x + ex * zoom + ew * zoom - 3.0, page_y + ey * zoom + eh * zoom - 3.0),
3526                     ];
3527                     for (hx, hy) in handles {
3528                         quads.push((hx, hy, hs, hs, hborder));
3529                         quads.push((hx + 1.0, hy + 1.0, hs - 2.0, hs - 2.0, hc));
3530                     }
3531                 }
3532             }
3533         }
3534 
3535         // Render Paginator Sidebar (includes backgrounds and active tab sliding container).
3536         // The tab column's state fills are rounded rects (cce-ui's ButtonStrip rounds its
3537         // segments), so read both bridges: plain quads, then the rounded ones.
3538         quads.extend(self.paginator.extra_quads());
3539         for (qx, qy, qw, qh, qr, qc, qcorners) in self.paginator.all_rounded_quads(&self.ui_context) {
3540             quads.pc.rounded_rect(cce_ui::scene::layout::Rect { x: qx, y: qy, width: qw, height: qh }, qr, qcorners, qc);
3541         }
3542         if let Some(hq) = self.paginator.highlight_quad(&self.ui_context) {
3543             quads.push(hq);
3544         }
3545         quads.extend(self.sidebar_quads.iter().cloned());
3546 
3547         // Divider between Sidebar and Canvas: a hairline down the middle of
3548         // the root gap that separates them.
3549         {
3550             let (canvas_x, canvas_y, _, canvas_h) = self.canvas_rect();
3551             quads.push((canvas_x - root_plate_gap() / 2.0, canvas_y, 1.0, canvas_h, [0.20, 0.20, 0.25, 1.0]));
3552         }
3553 
3554         // Vectors (the legacy view_vectors body), after plain geometry as the wrapper ordered.
3555         if !self.word_processor_enabled {
3556             let zoom = self.render_zoom();
3557             let (page_x, page_y) = self.page_origin();
3558             for element in &self.elements {
3559                 if let Element::Vector { x1, y1, x2, y2, stroke_width, color, line_cap } = element {
3560                     let vx1 = page_x + *x1 * zoom;
3561                     let vy1 = page_y + *y1 * zoom;
3562                     let vx2 = page_x + *x2 * zoom;
3563                     let vy2 = page_y + *y2 * zoom;
3564                     let vthickness = *stroke_width * zoom;
3565                     let cap = match line_cap {
3566                         LineCap::Flat => cce_ui::scene::paint::Cap::Flat,
3567                         LineCap::Round => cce_ui::scene::paint::Cap::Round,
3568                         LineCap::Arrow => cce_ui::scene::paint::Cap::Arrow,
3569                     };
3570                     __pc.vector(vx1, vy1, vx2, vy2, vthickness, *color, cap);
3571                 }
3572             }
3573         }
3574 
3575         // Text prims (single-run and boxed).
3576         for (text, size, x, y, color, font, bounds, layout) in &self.text_prims {
3577             match layout {
3578                 Some(l) => __pc.text_boxed(text.clone(), *x, *y, *size, *color, font.clone(), *bounds, cce_ui::scene::paint::TextAttrs::default(), *l),
3579                 None => __pc.text_with(text.clone(), *x, *y, *size, *color, font.clone(), *bounds),
3580             }
3581         }
3582 
3583         // Widget text via the paint walk (not the legacy text_labels* getters): the
3584         // paginator's sidebar tabs, and the word-processor text box when active.
3585         cce_ui::scene::painter::append_widget_text(&self.ui_context, &self.paginator, &mut __pc);
3586         if self.word_processor_enabled {
3587             cce_ui::scene::painter::append_widget_text(&self.ui_context, &self.wp_text_box, &mut __pc);
3588         }
3589 
3590         // Open dropdown popovers, last, on top of everything — PaintCtx is a
3591         // RenderTarget, and render_popovers sweeps the registry for open
3592         // popovers (this app registers none explicitly). Without this pass an
3593         // open menu was invisible: it hit-tested and occluded, but never drew.
3594         cce_ui::layout::render_popovers(&mut __pc, &self.ui_context);
3595 
3596         Some(__pc.finish())
3597     }
3598 
3599     fn display_list_text(&self) -> bool {
3600         true
3601     }
3602 
3603     fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
3604         let mut changed = false;
3605         let px = pos.x as f32;
3606         let py = pos.y as f32;
3607 
3608         if px < self.canvas_rect().0 {
3609             if {
3610                 // Self-routing composite: handle_event, not propagate — the router's
3611                 // children-first descent would let the embedded strip consume this.
3612                 let mv = cce_ui::widget::Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
3613                 self.paginator.handle_event(&mv, &mut self.ui_context)
3614             } { changed = true; }
3615             if self.active_page_widgets_cursor_moved(px, py) { changed = true; }
3616             if self.paginator.selected_page() == 2 && changed {
3617                 self.apply_sidebar_changes();
3618             }
3619         } else {
3620             // Clear hover state on sidebar widgets if mouse moves to canvas
3621             if {
3622                 // Self-routing composite: handle_event, not propagate — the router's
3623                 // children-first descent would let the embedded strip consume this.
3624                 let mv = cce_ui::widget::Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
3625                 self.paginator.handle_event(&mv, &mut self.ui_context)
3626             } { changed = true; }
3627             if self.active_page_widgets_cursor_moved(px, py) { changed = true; }
3628 
3629             // Compute centering coordinates for canvas elements
3630             let zoom = self.render_zoom();
3631             let (page_x, page_y) = self.page_origin();
3632 
3633             let cx = (px - page_x) / zoom;
3634             let cy = (py - page_y) / zoom;
3635 
3636             if self.word_processor_enabled {
3637                 let mv = cce_ui::widget::Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
3638                 let root = self.wp_text_box.id();
3639                 if self.ui_context.propagate_event(&mv, root) {
3640                     changed = true;
3641                 }
3642             } else if let Some((idx, ox, oy)) = self.dragging {
3643                 let mut new_x = cx - ox;
3644                 let mut new_y = cy - oy;
3645 
3646                 if self.grid_enabled {
3647                     let spacing = self.grid_spacing();
3648                     new_x = (new_x / spacing).round() * spacing;
3649                     new_y = (new_y / spacing).round() * spacing;
3650                 }
3651 
3652                 match &mut self.elements[idx] {
3653                     Element::Text { x, y, .. } => {
3654                         *x = new_x;
3655                         *y = new_y;
3656                     }
3657                     Element::Shape { x, y, .. } => {
3658                         *x = new_x;
3659                         *y = new_y;
3660                     }
3661                     Element::Vector { x1, y1, x2, y2, .. } => {
3662                         let dx = new_x - *x1;
3663                         let dy = new_y - *y1;
3664                         *x1 = new_x;
3665                         *y1 = new_y;
3666                         *x2 += dx;
3667                         *y2 += dy;
3668                     }
3669                 }
3670                 self.sync_sidebar_fields();
3671                 changed = true;
3672             }
3673         }
3674 
3675         if changed {
3676             *needs_rebuild = true;
3677             self.needs_rebuild = true;
3678         }
3679     }
3680 
3681     fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
3682         let mut changed = false;
3683         let mut msg_out = None;
3684         let px = pos.x as f32;
3685         let py = pos.y as f32;
3686 
3687         if px < self.canvas_rect().0 {
3688             let pag_mouse = {
3689                 // Self-routing composite: handle_event, not propagate (see pointer move).
3690                 let ev = cce_ui::widget::Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py };
3691                 self.paginator.handle_event(&ev, &mut self.ui_context)
3692             };
3693             if pag_mouse {
3694                 changed = true;
3695                 if let Some((new_page, _)) = self.paginator.menu_click() {
3696                     self.active_page_widgets_unfocus();
3697                     self.apply_sidebar_changes();
3698                     self.paginator.set_selected_page(new_page);
3699                 }
3700             } else {
3701                 if state == ElementState::Pressed {
3702                     self.active_page_widgets_focus_handle(px, py);
3703                 }
3704 
3705                 if self.active_page_widgets_mouse_input(button, state, px, py) {
3706                     changed = true;
3707                 }
3708 
3709                 if self.paginator.selected_page() == 2 {
3710                     self.apply_sidebar_changes();
3711                 }
3712 
3713                 let active_page = self.paginator.selected_page();
3714                 if active_page == 0 {
3715                     if state == ElementState::Released {
3716                         if self.btn_new_doc.take_click() {
3717                             msg_out = Some(AppMessage::NewDocument);
3718                         }
3719                         if self.btn_open.take_click() {
3720                             msg_out = Some(AppMessage::Open);
3721                         }
3722                         let mut clicked_file = None;
3723                         for (i, btn) in self.recent_files_buttons.iter_mut().enumerate() {
3724                             if btn.take_click() {
3725                                 clicked_file = Some(self.recent_files[i].clone());
3726                             }
3727                         }
3728                         if let Some(path) = clicked_file {
3729                             self.selected_idx = None;
3730                             self.dragging = None;
3731                             self.pan_x = 0.0;
3732                             self.pan_y = 0.0;
3733                             if let Err(e) = self.load_document(&path) {
3734                                 log::error!("Failed to load document: {}", e);
3735                             } else {
3736                                 log::info!("Successfully loaded layout from {:?}", path);
3737                                 self.current_file_path = Some(path.clone());
3738                                 self.last_saved_document = self.get_current_document();
3739                                 self.add_recent_file(path);
3740                             }
3741                         }
3742                         if self.btn_save.take_click() {
3743                             msg_out = Some(AppMessage::Save);
3744                         }
3745                         if self.btn_save_as.take_click() {
3746                             msg_out = Some(AppMessage::SaveAs);
3747                         }
3748                         if self.btn_exit.take_click() {
3749                             msg_out = Some(AppMessage::Exit);
3750                         }
3751                     }
3752                 } else if active_page == 1 {
3753                     let _ = self.toggle_margin.take_click();
3754                     let _ = self.toggle_word_processor.take_click();
3755 
3756                 } else if active_page == 3 {
3757                     if state == ElementState::Released {
3758                         if self.btn_add_text.take_click() {
3759                             msg_out = Some(AppMessage::AddText);
3760                         }
3761                         if self.btn_add_rect.take_click() {
3762                             msg_out = Some(AppMessage::AddRectangle);
3763                         }
3764                         if self.btn_add_banner.take_click() {
3765                             msg_out = Some(AppMessage::AddBanner);
3766                         }
3767                         if self.btn_add_vector.take_click() {
3768                             msg_out = Some(AppMessage::AddVector);
3769                         }
3770                     }
3771                 } else if active_page == 4 {
3772                     let _ = self.toggle_grid.take_click();
3773                     let _ = self.grid_color_selector.take_click();
3774                     let _ = self.label_grid_snap.take_click();
3775                     let _ = self.toggle_rulers.take_click();
3776                     let _ = self.margin_color_selector.take_click();
3777                 } else if active_page == 5 {
3778                     if state == ElementState::Released {
3779                         let mut clicked_idx = None;
3780                         for (idx, btn) in self.layer_buttons.iter_mut().enumerate() {
3781                             if btn.take_click() {
3782                                 clicked_idx = Some(idx);
3783                             }
3784                         }
3785                         if let Some(idx) = clicked_idx {
3786                             self.selected_idx = Some(idx);
3787                             self.sync_sidebar_fields();
3788                             self.rebuild_layers_tab_widgets();
3789                             changed = true;
3790                         }
3791                     }
3792                 }
3793             }
3794         } else {
3795             // Compute centering coordinates for canvas elements
3796             let zoom = self.render_zoom();
3797             let (page_x, page_y) = self.page_origin();
3798 
3799             let cx = (px - page_x) / zoom;
3800             let cy = (py - page_y) / zoom;
3801 
3802             if self.word_processor_enabled {
3803                 if {
3804                     let ev = cce_ui::widget::Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py };
3805                     let root = self.wp_text_box.id();
3806                     self.ui_context.propagate_event(&ev, root)
3807                 } {
3808                     changed = true;
3809                 } else if state == ElementState::Pressed {
3810                     self.wp_text_box.unfocus();
3811                     changed = true;
3812                 }
3813             } else if state == ElementState::Pressed {
3814                 let mut found = None;
3815                 for (idx, element) in self.elements.iter().enumerate().rev() {
3816                     match element {
3817                         Element::Text { x, y, w, h, .. } => {
3818                             if cx >= *x && cx <= *x + *w && cy >= *y && cy <= *y + *h {
3819                                 found = Some(idx);
3820                                 break;
3821                             }
3822                         }
3823                         Element::Shape { x, y, w, h, .. } => {
3824                             if cx >= *x && cx <= *x + *w && cy >= *y && cy <= *y + *h {
3825                                 found = Some(idx);
3826                                 break;
3827                             }
3828                         }
3829                         Element::Vector { x1, y1, x2, y2, stroke_width, .. } => {
3830                             let dist = point_to_line_segment_distance(cx, cy, *x1, *y1, *x2, *y2);
3831                             let select_tolerance = (stroke_width * 1.5).max(10.0);
3832                             if dist <= select_tolerance {
3833                                 found = Some(idx);
3834                                 break;
3835                             }
3836                         }
3837                     }
3838                 }
3839 
3840                 if let Some(idx) = found {
3841                     self.selected_idx = Some(idx);
3842                     let (ex, ey) = match &self.elements[idx] {
3843                         Element::Text { x, y, .. } => (*x, *y),
3844                         Element::Shape { x, y, .. } => (*x, *y),
3845                         Element::Vector { x1, y1, .. } => (*x1, *y1),
3846                     };
3847                     self.dragging = Some((idx, cx - ex, cy - ey));
3848                     self.rebuild_layers_tab_widgets();
3849                     self.sync_sidebar_fields();
3850                     changed = true;
3851                 } else {
3852                     self.selected_idx = None;
3853                     self.dragging = None;
3854                     self.rebuild_layers_tab_widgets();
3855                     self.sync_sidebar_fields();
3856                     changed = true;
3857                 }
3858             } else if state == ElementState::Released {
3859                 self.dragging = None;
3860             }
3861         }
3862 
3863         if changed {
3864             *needs_rebuild = true;
3865             self.needs_rebuild = true;
3866         }
3867 
3868         msg_out
3869     }
3870 
3871     fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool) {
3872         let px = pos.x as f32;
3873         let py = pos.y as f32;
3874         if px < self.canvas_rect().0 {
3875             let mut wheel_handled = false;
3876             if self.active_page_widgets_mouse_wheel(delta, px, py) {
3877                 wheel_handled = true;
3878             }
3879             if {
3880                 let wev = cce_ui::widget::Event::MouseWheel { delta: *delta, x: px, y: py, local_x: px, local_y: py };
3881                 self.paginator.handle_event(&wev, &mut self.ui_context)
3882             } {
3883                 wheel_handled = true;
3884             }
3885             if wheel_handled {
3886                 if self.paginator.selected_page() == 2 {
3887                     self.apply_sidebar_changes();
3888                 }
3889                 *needs_rebuild = true;
3890                 self.needs_rebuild = true;
3891             }
3892         } else {
3893             // Canvas pan: the page follows the wheel's own sign (a notch is
3894             // LINE_PX, pixels are 1:1), unclamped. The motion glides notches
3895             // and coasts a flick; `tick_pan` carries the drawn values after it.
3896             let (dx, dy) = match delta {
3897                 MouseScrollDelta::LineDelta(x, y) => (*x * LINE_PX, *y * LINE_PX),
3898                 MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
3899             };
3900             let discrete = matches!(delta, MouseScrollDelta::LineDelta(..));
3901             self.pan_motion.reconcile(self.pan_x, self.pan_y);
3902             if self.pan_motion.apply_px(dx, dy, discrete, Bounds::UNBOUNDED, Bounds::UNBOUNDED) {
3903                 self.pan_x = self.pan_motion.x.pos();
3904                 self.pan_y = self.pan_motion.y.pos();
3905                 *needs_rebuild = true;
3906                 self.needs_rebuild = true;
3907             }
3908         }
3909     }
3910 
3911     fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
3912         if event.state == ElementState::Pressed {
3913             // Ctrl+letter can arrive as the raw control character; keep the
3914             // \u{13} form matching whenever the chord is the ctrl+s default.
3915             let ctrl_char_save = self.keys.save_document == "ctrl+s"
3916                 && event.ctrl
3917                 && matches!(event.logical_key, Key::Character(ref s) if s == "\u{13}");
3918             if cce_ui::widget::match_key_shortcut(event, &self.keys.save_document) || ctrl_char_save {
3919                 *needs_rebuild = true;
3920                 self.needs_rebuild = true;
3921                 return Some(AppMessage::Save);
3922             }
3923         }
3924 
3925         let mut handled = false;
3926 
3927         if self.word_processor_enabled {
3928             let kev = cce_ui::widget::Event::KeyInput(event.clone());
3929             let root = self.wp_text_box.id();
3930             if self.ui_context.propagate_event(&kev, root) {
3931                 handled = true;
3932             }
3933         }
3934 
3935         if self.active_page_widgets_keyboard_input(event) {
3936             handled = true;
3937         }
3938 
3939         if {
3940             let kev = cce_ui::widget::Event::KeyInput(event.clone());
3941             self.paginator.handle_event(&kev, &mut self.ui_context)
3942         } {
3943             handled = true;
3944             let active_page = self.paginator.selected_page();
3945             if active_page == 2 {
3946                 self.apply_sidebar_changes();
3947             }
3948         }
3949 
3950         if !handled && !self.word_processor_enabled && event.state == ElementState::Pressed {
3951             if cce_ui::widget::match_key_shortcut(event, &self.keys.delete_element) {
3952                 if let Some(idx) = self.selected_idx {
3953                     if idx < self.elements.len() {
3954                         self.elements.remove(idx);
3955                         self.selected_idx = None;
3956                         self.dragging = None;
3957                         self.rebuild_layers_tab_widgets();
3958                         self.sync_sidebar_fields();
3959                         handled = true;
3960                     }
3961                 }
3962             }
3963         }
3964 
3965         if handled {
3966             *needs_rebuild = true;
3967             self.needs_rebuild = true;
3968         }
3969 
3970         None
3971     }
3972 }
3973 
3974 fn main() {
3975     env_logger::Builder::from_default_env()
3976         .filter_level(log::LevelFilter::Info)
3977         .init();
3978 
3979     let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
3980     let _guard = rt.enter();
3981 
3982     cce_ui::engine::run::<LayoutApp>();
3983 }