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 }