page layout tool
git clone https://git.lucas.co/cce-layout-interface.git
feat: introduce PageContent rendering structure and typography alignment types
Cargo.toml | 1 +
Makefile | 14 +
src/main.rs | 2460 ++++++++++++++++++++++++++++++++++++++++++++++++-----------
3 files changed, 2014 insertions(+), 461 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 95e1c19..424130b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,3 +18,4 @@ glyphon = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ef7437c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+.PHONY: build install run clean
+
+build:
+ cargo build --release
+
+install: build
+ mkdir -p ~/.local/bin
+ install -m 755 target/release/clear-layout-interface ~/.local/bin/clear-layout-interface
+
+run:
+ cargo run
+
+clean:
+ cargo clean
diff --git a/src/main.rs b/src/main.rs
index caad333..d38508c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,29 +1,138 @@
use wayland_client::QueueHandle;
use glyphon::{FontSystem, Buffer, Metrics, Attrs};
-use clear_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
+use serde::{Serialize, Deserialize};
+use clear_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings, LineCap};
use clear_ui::widget::{
MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Widget,
- MenuBar, TextBox, Slider, TextLabel, Paginator, Button, Dropdown, Toggle, ColorSelector,
- Label, SectionHeader, Spinbox
+ TextBox, Slider, TextLabel, Paginator, Button, Dropdown, Toggle, ColorSelector,
+ Label, Spinbox, Key, NamedKey, ScrollingList, FontSelector
};
+use clear_ui::layout::{RenderTarget, render_widget, Section, UiFrame};
+
+pub struct PageContent {
+ pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
+ pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
+}
+
+impl PageContent {
+ pub fn new() -> Self {
+ Self {
+ rects: Vec::new(),
+ texts: Vec::new(),
+ }
+ }
+}
+
+impl RenderTarget for PageContent {
+ fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
+ self.rects.push((color, x, y, w, h));
+ }
+
+ fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
+ self.texts.push((content.to_string(), size, x, y, color, None, None));
+ }
+
+ fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
+ self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
+ }
+
+ fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
+ self.texts.push((content.to_string(), size, x, y, color, None, bounds));
+ }
+
+ 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]>) {
+ self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
+ }
+}
+
+fn make_text_buffer_with_font(
+ fs: &mut FontSystem,
+ text: &str,
+ size: f32,
+ font: Option<&str>,
+) -> Buffer {
+ let metrics = Metrics::new(size, size * 1.4);
+ let mut buf = Buffer::new(fs, metrics);
+ let mut attrs = Attrs::new();
+ if let Some(font_name) = font {
+ let family = match font_name {
+ "monospace" => glyphon::Family::Monospace,
+ "sans-serif" => glyphon::Family::SansSerif,
+ "serif" => glyphon::Family::Serif,
+ _ => glyphon::Family::Name(font_name),
+ };
+ attrs = attrs.family(family);
+ }
+ buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
+ buf.shape_until_scroll(fs, true);
+ buf
+}
+
+
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
+pub enum TextAlignH {
+ Left,
+ Center,
+ Right,
+}
+
+impl Default for TextAlignH {
+ fn default() -> Self {
+ TextAlignH::Left
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
+pub enum TextAlignV {
+ Top,
+ Middle,
+ Bottom,
+}
+
+impl Default for TextAlignV {
+ fn default() -> Self {
+ TextAlignV::Top
+ }
+}
+
+fn point_to_line_segment_distance(px: f32, py: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
+ let dx = x2 - x1;
+ let dy = y2 - y1;
+ let len_sq = dx * dx + dy * dy;
+ if len_sq < 0.001 {
+ let dx_p = px - x1;
+ let dy_p = py - y1;
+ return (dx_p * dx_p + dy_p * dy_p).sqrt();
+ }
+ let t = (((px - x1) * dx + (py - y1) * dy) / len_sq).clamp(0.0, 1.0);
+ let proj_x = x1 + t * dx;
+ let proj_y = y1 + t * dy;
+ let dx_p = px - proj_x;
+ let dy_p = py - proj_y;
+ (dx_p * dx_p + dy_p * dy_p).sqrt()
+}
#[derive(Debug, Clone)]
enum AppMessage {
Exit,
NewDocument,
+ Open,
+ Save,
+ SaveAs,
AddText,
AddRectangle,
AddBanner,
- ToggleGrid,
+ AddVector,
}
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
enum ShapeType {
Rectangle,
Banner,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(tag = "type")]
enum Element {
Text {
text: String,
@@ -34,6 +143,12 @@ enum Element {
font_size: f32,
color: [f32; 4],
font_family: String,
+ #[serde(default, rename = "align")]
+ align_h: TextAlignH,
+ #[serde(default)]
+ align_v: TextAlignV,
+ #[serde(default = "default_multiline")]
+ multiline: bool,
},
Shape {
shape_type: ShapeType,
@@ -43,6 +158,91 @@ enum Element {
h: f32,
color: [f32; 4],
},
+ Vector {
+ x1: f32,
+ y1: f32,
+ x2: f32,
+ y2: f32,
+ stroke_width: f32,
+ color: [f32; 4],
+ #[serde(default = "default_line_cap")]
+ line_cap: LineCap,
+ },
+}
+
+fn default_line_cap() -> LineCap {
+ LineCap::Arrow
+}
+
+fn default_grid_color() -> [f32; 4] {
+ [0.20, 0.35, 0.60, 0.8]
+}
+
+fn default_grid_size() -> f32 {
+ 1.5
+}
+
+fn default_multiline() -> bool {
+ true
+}
+
+fn default_margin_color() -> [f32; 4] {
+ [0.20, 0.50, 0.85, 0.35]
+}
+
+fn default_margin_thickness() -> f32 {
+ 1.0
+}
+
+fn default_grid_enabled() -> bool {
+ true
+}
+
+fn default_grid_units() -> usize {
+ 0
+}
+
+fn default_zoom() -> f32 {
+ 0.33333334
+}
+
+fn default_rulers_enabled() -> bool {
+ false
+}
+
+fn default_ruler_units() -> usize {
+ 0
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+struct LayoutDocument {
+ page_w: f32,
+ page_h: f32,
+ page_color: [f32; 4],
+ margin_enabled: bool,
+ margin_x: f32,
+ margin_y: f32,
+ word_processor_enabled: bool,
+ wp_text: String,
+ elements: Vec<Element>,
+ #[serde(default = "default_grid_color")]
+ grid_color: [f32; 4],
+ #[serde(default = "default_grid_size")]
+ grid_size: f32,
+ #[serde(default = "default_margin_color")]
+ margin_color: [f32; 4],
+ #[serde(default = "default_margin_thickness")]
+ margin_thickness: f32,
+ #[serde(default = "default_grid_enabled")]
+ grid_enabled: bool,
+ #[serde(default = "default_grid_units")]
+ grid_units: usize,
+ #[serde(default = "default_zoom")]
+ zoom: f32,
+ #[serde(default = "default_rulers_enabled")]
+ rulers_enabled: bool,
+ #[serde(default = "default_ruler_units")]
+ ruler_units: usize,
}
struct PagePreset {
@@ -53,21 +253,38 @@ struct PagePreset {
const PRESETS: &[PagePreset] = &[
PagePreset { name: "Letter (8.5\" x 11\")", w: 510.0, h: 660.0 },
+ PagePreset { name: "Legal (8.5\" x 14\")", w: 510.0, h: 840.0 },
PagePreset { name: "Tabloid (11\" x 17\")", w: 660.0, h: 1020.0 },
+ PagePreset { name: "Executive (7.25\" x 10.5\")", w: 435.0, h: 630.0 },
+ PagePreset { name: "A3 (297 x 420 mm)", w: 701.0, h: 992.0 },
PagePreset { name: "A4 (210 x 297 mm)", w: 496.0, h: 701.0 },
PagePreset { name: "A5 (148 x 210 mm)", w: 350.0, h: 496.0 },
- PagePreset { name: "Poster (8\" x 8\")", w: 480.0, h: 480.0 },
+ PagePreset { name: "A6 (105 x 148 mm)", w: 248.0, h: 350.0 },
+ PagePreset { name: "B5 (JIS) (182 x 257 mm)", w: 430.0, h: 607.0 },
+ PagePreset { name: "Business Card (3.5\" x 2\")", w: 210.0, h: 120.0 },
+ PagePreset { name: "Presentation (16:9)", w: 800.0, h: 450.0 },
+ PagePreset { name: "Square Poster (12\" x 12\")", w: 720.0, h: 720.0 },
PagePreset { name: "Custom", w: 510.0, h: 660.0 },
];
struct LayoutApp {
- menu_bar: MenuBar,
+ btn_new_doc: Button,
+ btn_open: Button,
+ recent_files: Vec<std::path::PathBuf>,
+ recent_files_list: ScrollingList,
+ recent_files_buttons: Vec<Button>,
+ btn_exit: Button,
+ btn_save: Button,
+ btn_save_as: Button,
paginator: Paginator,
+ layer_buttons: Vec<Box<Button>>,
elements: Vec<Element>,
selected_idx: Option<usize>,
dragging: Option<(usize, f32, f32)>, // Index, offset_x, offset_y
grid_enabled: bool,
width: u32,
+ current_file_path: Option<std::path::PathBuf>,
+ last_saved_document: LayoutDocument,
height: u32,
scale_factor: f64,
text_items: Vec<TextItem>,
@@ -90,13 +307,19 @@ struct LayoutApp {
toggle_word_processor: Toggle,
word_processor_enabled: bool,
wp_text_box: TextBox,
- sidebar_x: TextBox,
- sidebar_y: TextBox,
- sidebar_w: TextBox,
- sidebar_h: TextBox,
+ sidebar_x: Spinbox,
+ sidebar_y: Spinbox,
+ sidebar_w: Spinbox,
+ sidebar_h: Spinbox,
sidebar_text: TextBox,
sidebar_size: Spinbox,
- dropdown_font_family: Dropdown,
+ font_selector: FontSelector,
+ dropdown_text_align_h: Dropdown,
+ dropdown_text_align_v: Dropdown,
+ toggle_multiline: Toggle,
+ dropdown_align_h: Dropdown,
+ dropdown_align_v: Dropdown,
+ dropdown_line_cap: Dropdown,
wp_base_font_size: f32,
slider_r: Slider,
@@ -105,22 +328,28 @@ struct LayoutApp {
slider_zoom: Slider,
// Page 1: Canvas settings controls
- btn_toggle_grid: Button,
- btn_clear_canvas: Button,
+ toggle_grid: Toggle,
+ grid_color_selector: ColorSelector,
+ grid_color: [f32; 4],
+ grid_size: f32,
+ spinbox_grid_size: Spinbox,
+ dropdown_grid_units: Dropdown,
+ margin_color_selector: ColorSelector,
+ margin_color: [f32; 4],
+ spinbox_margin_thickness: Spinbox,
+ margin_thickness: f32,
btn_add_text: Button,
btn_add_rect: Button,
btn_add_banner: Button,
+ btn_add_vector: Button,
- // Page 3: View settings controls
toggle_rulers: Toggle,
dropdown_units: Dropdown,
- section_rulers: SectionHeader,
-
label_sel_status: Label,
label_sel_desc1: Label,
label_sel_desc2: Label,
- label_grid_snap: Label,
+ label_grid_snap: Toggle,
label_total_elements: Label,
last_selected: Option<usize>,
@@ -128,7 +357,7 @@ struct LayoutApp {
page_h: f32,
pan_x: f32,
pan_y: f32,
- widgets_registered: bool,
+ sidebar_quads: Vec<(f32, f32, f32, f32, [f32; 4])>,
}
fn get_monitor_ppi() -> f32 {
@@ -171,7 +400,238 @@ fn get_monitor_ppi() -> f32 {
fallback_ppi
}
+fn get_system_fonts() -> Vec<String> {
+ let mut fonts = Vec::new();
+ if let Ok(output) = std::process::Command::new("fc-list")
+ .arg(":")
+ .arg("family")
+ .output()
+ {
+ if let Ok(stdout) = String::from_utf8(output.stdout) {
+ for line in stdout.lines() {
+ for part in line.split(',') {
+ let cleaned = part.replace('\\', "").trim().to_string();
+ if !cleaned.is_empty() {
+ fonts.push(cleaned);
+ }
+ }
+ }
+ }
+ }
+ fonts.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
+ fonts.dedup();
+ if fonts.is_empty() {
+ fonts = vec![
+ "Monospace".to_string(),
+ "Sans-Serif".to_string(),
+ "Serif".to_string(),
+ ];
+ }
+ fonts
+}
+
impl LayoutApp {
+ fn get_recent_files_path() -> Option<std::path::PathBuf> {
+ std::env::var("HOME").ok().map(|h| {
+ let mut path = std::path::PathBuf::from(h);
+ path.push(".config");
+ path.push("clear-layout-interface");
+ path.push("recent_files.json");
+ path
+ })
+ }
+
+ fn load_recent_files() -> Vec<std::path::PathBuf> {
+ if let Some(path) = Self::get_recent_files_path() {
+ if let Ok(file) = std::fs::File::open(path) {
+ if let Ok(list) = serde_json::from_reader::<_, Vec<String>>(file) {
+ return list.into_iter().map(std::path::PathBuf::from).collect();
+ }
+ }
+ }
+ Vec::new()
+ }
+
+ fn save_recent_files(files: &[std::path::PathBuf]) {
+ if let Some(path) = Self::get_recent_files_path() {
+ if let Some(parent) = path.parent() {
+ let _ = std::fs::create_dir_all(parent);
+ }
+ if let Ok(file) = std::fs::File::create(path) {
+ let list: Vec<String> = files.iter().map(|p| p.to_string_lossy().to_string()).collect();
+ let _ = serde_json::to_writer_pretty(file, &list);
+ }
+ }
+ }
+
+ fn add_recent_file(&mut self, path: std::path::PathBuf) {
+ self.recent_files.retain(|p| p != &path);
+ self.recent_files.insert(0, path);
+ self.recent_files.truncate(10);
+ Self::save_recent_files(&self.recent_files);
+ self.rebuild_recent_buttons();
+ }
+
+ fn rebuild_recent_buttons(&mut self) {
+ self.recent_files_buttons.clear();
+ for file in &self.recent_files {
+ let label = file.file_name()
+ .map(|n| n.to_string_lossy().to_string())
+ .unwrap_or_else(|| file.to_string_lossy().to_string());
+ let btn = Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(&label);
+ self.recent_files_buttons.push(btn);
+ }
+ }
+
+ fn save_document(&self, path: &std::path::Path) -> std::io::Result<()> {
+ let doc = self.get_current_document();
+ let file = std::fs::File::create(path)?;
+ serde_json::to_writer_pretty(file, &doc)?;
+ Ok(())
+ }
+
+ fn perform_save_as(&mut self) {
+ let path_opt = std::process::Command::new("/home/lsgalante/.local/bin/clear-filesystem-interface")
+ .arg("--save")
+ .output()
+ .or_else(|_| {
+ std::process::Command::new("clear-filesystem-interface")
+ .arg("--save")
+ .output()
+ })
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let trimmed = stdout.trim();
+ if !trimmed.is_empty() {
+ Some(std::path::PathBuf::from(trimmed))
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ });
+
+ if let Some(path) = path_opt {
+ if let Err(e) = self.save_document(&path) {
+ eprintln!("ERROR: Failed to save document: {}", e);
+ } else {
+ println!("DEBUG: Successfully saved layout to {:?}", path);
+ self.current_file_path = Some(path.clone());
+ self.last_saved_document = self.get_current_document();
+ self.add_recent_file(path);
+ }
+ }
+ }
+
+ fn load_document(&mut self, path: &std::path::Path) -> std::io::Result<()> {
+ let file = std::fs::File::open(path)?;
+ let doc: LayoutDocument = serde_json::from_reader(file)?;
+
+ self.page_w = doc.page_w;
+ self.page_h = doc.page_h;
+ self.page_color = doc.page_color;
+ self.margin_enabled = doc.margin_enabled;
+ self.margin_x = doc.margin_x;
+ self.margin_y = doc.margin_y;
+ self.word_processor_enabled = doc.word_processor_enabled;
+ self.wp_text_box.text = doc.wp_text.clone();
+ self.wp_text_box.edit_buffer = doc.wp_text;
+ self.elements = doc.elements;
+
+ self.toggle_margin.set_toggled(self.margin_enabled);
+ self.toggle_word_processor.set_toggled(self.word_processor_enabled);
+
+ let r = (self.page_color[0] * 255.0).clamp(0.0, 255.0) as u8;
+ let g = (self.page_color[1] * 255.0).clamp(0.0, 255.0) as u8;
+ let b = (self.page_color[2] * 255.0).clamp(0.0, 255.0) as u8;
+ self.page_color_selector.color = [r, g, b];
+
+ self.grid_color = doc.grid_color;
+ self.grid_size = doc.grid_size;
+ self.margin_color = doc.margin_color;
+
+ let gr = (self.grid_color[0] * 255.0).clamp(0.0, 255.0) as u8;
+ let gg = (self.grid_color[1] * 255.0).clamp(0.0, 255.0) as u8;
+ let gb = (self.grid_color[2] * 255.0).clamp(0.0, 255.0) as u8;
+ self.grid_color_selector.color = [gr, gg, gb];
+ self.spinbox_grid_size.value = (self.grid_size * 10.0).round() as i32;
+
+ let mr = (self.margin_color[0] * 255.0).clamp(0.0, 255.0) as u8;
+ let mg = (self.margin_color[1] * 255.0).clamp(0.0, 255.0) as u8;
+ let mb = (self.margin_color[2] * 255.0).clamp(0.0, 255.0) as u8;
+ self.margin_color_selector.color = [mr, mg, mb];
+
+ self.margin_thickness = doc.margin_thickness;
+ self.spinbox_margin_thickness.value = self.margin_thickness.round() as i32;
+
+ self.grid_enabled = doc.grid_enabled;
+ self.toggle_grid.set_toggled(doc.grid_enabled);
+ self.label_grid_snap.set_toggled(doc.grid_enabled);
+ self.dropdown_grid_units.selected = doc.grid_units;
+ self.slider_zoom.set_value(doc.zoom);
+ self.toggle_rulers.set_toggled(doc.rulers_enabled);
+ self.dropdown_units.selected = doc.ruler_units;
+
+ let mut preset_idx = PRESETS.len() - 1; // Custom
+ for (i, preset) in PRESETS.iter().enumerate() {
+ if (preset.w - self.page_w).abs() < 1.0 && (preset.h - self.page_h).abs() < 1.0 {
+ preset_idx = i;
+ break;
+ }
+ }
+ self.dropdown_presets.selected = preset_idx;
+
+ self.sync_page_units();
+ self.rebuild_layers_tab_widgets();
+ self.sync_sidebar_fields();
+ self.needs_rebuild = true;
+
+ Ok(())
+ }
+
+ fn perform_open(&mut self) {
+ let path_opt = std::process::Command::new("/home/lsgalante/.local/bin/clear-filesystem-interface")
+ .arg("--select")
+ .output()
+ .or_else(|_| {
+ std::process::Command::new("clear-filesystem-interface")
+ .arg("--select")
+ .output()
+ })
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let trimmed = stdout.trim();
+ if !trimmed.is_empty() {
+ Some(std::path::PathBuf::from(trimmed))
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ });
+
+ if let Some(path) = path_opt {
+ self.selected_idx = None;
+ self.dragging = None;
+ self.pan_x = 0.0;
+ self.pan_y = 0.0;
+ if let Err(e) = self.load_document(&path) {
+ eprintln!("ERROR: Failed to load document: {}", e);
+ } else {
+ println!("DEBUG: Successfully loaded layout from {:?}", path);
+ self.current_file_path = Some(path.clone());
+ self.last_saved_document = self.get_current_document();
+ self.add_recent_file(path);
+ }
+ }
+ }
+
fn sync_page_units(&mut self) {
let unit_idx = self.dropdown_margin_units.selected;
let px_per_unit = match unit_idx {
@@ -215,8 +675,377 @@ impl LayoutApp {
}
fn rebuild_text_items(&mut self) {
- // Sync labels with current state
+ let font_system = &mut self.font_system;
+ let selected_page = self.paginator.selected_page();
+ let word_processor_enabled = self.word_processor_enabled;
+
+ match selected_page {
+ 0 => {
+ self.btn_new_doc.prepare_text(font_system);
+ self.btn_open.prepare_text(font_system);
+ for btn in &mut self.recent_files_buttons {
+ btn.prepare_text(font_system);
+ }
+ self.btn_save.prepare_text(font_system);
+ self.btn_save_as.prepare_text(font_system);
+ self.btn_exit.prepare_text(font_system);
+ }
+ 1 => {
+ self.dropdown_margin_units.prepare_text(font_system);
+ self.dropdown_presets.prepare_text(font_system);
+ self.slider_page_x.prepare_text(font_system);
+ self.slider_page_y.prepare_text(font_system);
+ self.page_color_selector.prepare_text(font_system);
+ self.toggle_margin.prepare_text(font_system);
+ self.toggle_word_processor.prepare_text(font_system);
+ self.slider_margin_x.prepare_text(font_system);
+ self.slider_margin_y.prepare_text(font_system);
+ }
+ 2 => {
+ if word_processor_enabled {
+ self.font_selector.prepare_text(font_system);
+ self.sidebar_size.prepare_text(font_system);
+ self.slider_r.prepare_text(font_system);
+ self.slider_g.prepare_text(font_system);
+ self.slider_b.prepare_text(font_system);
+ } else if self.selected_idx.is_some() {
+ self.dropdown_align_h.prepare_text(font_system);
+ self.dropdown_align_v.prepare_text(font_system);
+
+ let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
+ Some(Element::Text { .. }) => (true, false),
+ Some(Element::Vector { .. }) => (false, true),
+ _ => (false, false),
+ };
+ if is_text {
+ self.sidebar_x.prepare_text(font_system);
+ self.sidebar_y.prepare_text(font_system);
+ self.sidebar_w.prepare_text(font_system);
+ self.sidebar_h.prepare_text(font_system);
+ self.sidebar_text.prepare_text(font_system);
+ self.font_selector.prepare_text(font_system);
+ self.toggle_multiline.prepare_text(font_system);
+ self.dropdown_text_align_h.prepare_text(font_system);
+ self.dropdown_text_align_v.prepare_text(font_system);
+ self.sidebar_size.prepare_text(font_system);
+ self.slider_r.prepare_text(font_system);
+ self.slider_g.prepare_text(font_system);
+ self.slider_b.prepare_text(font_system);
+ } else if is_vector {
+ self.sidebar_x.prepare_text(font_system);
+ self.sidebar_y.prepare_text(font_system);
+ self.sidebar_w.prepare_text(font_system);
+ self.sidebar_h.prepare_text(font_system);
+ self.sidebar_size.prepare_text(font_system);
+ self.dropdown_line_cap.prepare_text(font_system);
+ self.slider_r.prepare_text(font_system);
+ self.slider_g.prepare_text(font_system);
+ self.slider_b.prepare_text(font_system);
+ } else {
+ self.sidebar_x.prepare_text(font_system);
+ self.sidebar_y.prepare_text(font_system);
+ self.sidebar_w.prepare_text(font_system);
+ self.sidebar_h.prepare_text(font_system);
+ self.slider_r.prepare_text(font_system);
+ self.slider_g.prepare_text(font_system);
+ self.slider_b.prepare_text(font_system);
+ }
+ }
+ }
+ 3 => {
+ self.btn_add_text.prepare_text(font_system);
+ self.btn_add_rect.prepare_text(font_system);
+ self.btn_add_banner.prepare_text(font_system);
+ self.btn_add_vector.prepare_text(font_system);
+ }
+ 4 => {
+ self.toggle_grid.prepare_text(font_system);
+ self.grid_color_selector.prepare_text(font_system);
+ self.spinbox_grid_size.prepare_text(font_system);
+ self.label_grid_snap.prepare_text(font_system);
+ self.slider_zoom.prepare_text(font_system);
+ self.toggle_rulers.prepare_text(font_system);
+ self.dropdown_units.prepare_text(font_system);
+ self.margin_color_selector.prepare_text(font_system);
+ self.spinbox_margin_thickness.prepare_text(font_system);
+ self.dropdown_grid_units.prepare_text(font_system);
+ }
+ 5 => {
+ for btn in &mut self.layer_buttons {
+ btn.prepare_text(font_system);
+ }
+ }
+ _ => {}
+ }
+
+ self.wp_text_box.prepare_text(font_system);
+
+ self.sidebar_quads.clear();
+ self.text_items.clear();
+
+ // 1. Populate sidebar page views into PageContent
+ let mut pc = PageContent::new();
+ let ui_frame = UiFrame::start(0.0);
+
+ let sidebar_w = self.paginator.sidebar_w();
+ let cx = sidebar_w;
+ let cy = 16.0;
+ let cw = 280.0 - sidebar_w;
+
+ match self.paginator.selected_page() {
+ 0 => {
+ let mut sec = Section::new(&mut pc, cx, cy, cw, "File Operations");
+ let col_w = cw - 40.0;
+ sec.widget(&mut pc, &mut self.btn_new_doc, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.btn_open, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+
+ sec.text(&mut pc, "Recent Files", 12.0, 0.0, 11.0, [0.83, 0.83, 0.83, 1.0]);
+ sec.spacing(16.0);
+
+ let list_h = 100.0;
+ let list_x = sec.ax(12.0);
+ let list_y = sec.ay();
+ sec.widget(&mut pc, &mut self.recent_files_list, 12.0, col_w, list_h);
+
+ self.recent_files_list.update_bounds(self.recent_files.len(), list_y, list_h);
+
+ let btn_h = 22.0;
+ let inner_x = list_x + 4.0;
+ let inner_w = col_w - 16.0;
+
+ for (idx, btn) in self.recent_files_buttons.iter_mut().enumerate() {
+ if let Some(draw_y) = self.recent_files_list.get_item_draw_y(idx, 0.0) {
+ clear_ui::layout::render_widget(&mut pc, btn, inner_x, draw_y, inner_w, btn_h);
+ } else {
+ btn.set_rect(-9999.0, -9999.0, 0.0, 0.0);
+ }
+ }
+ if self.recent_files.is_empty() {
+ pc.text("No recent files", list_x + 12.0, list_y + 16.0, 11.0, [0.55, 0.55, 0.60, 1.0]);
+ }
+
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.btn_save, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.btn_save_as, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.btn_exit, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.finish(&mut pc);
+ }
+ 1 => {
+ let mut sec = Section::new(&mut pc, cx, cy, cw, "Document Size");
+ let col_w = cw - 40.0;
+ sec.widget(&mut pc, &mut self.dropdown_presets, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.slider_page_x, 12.0, col_w, 18.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.slider_page_y, 12.0, col_w, 18.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.page_color_selector, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ let next_y = sec.finish(&mut pc);
+
+ let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Margins & Mode");
+ sec2.widget(&mut pc, &mut self.toggle_margin, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.dropdown_margin_units, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_margin_x, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_margin_y, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.toggle_word_processor, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.finish(&mut pc);
+ }
+ 2 => {
+ let col_w = cw - 40.0;
+ if self.word_processor_enabled {
+ let mut sec = Section::new(&mut pc, cx, cy, cw, "Word Processor");
+ sec.widget(&mut pc, &mut self.font_selector, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.sidebar_size, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.slider_r, 12.0, col_w, 18.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.slider_g, 12.0, col_w, 18.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.slider_b, 12.0, col_w, 18.0);
+ sec.spacing(12.0);
+ sec.finish(&mut pc);
+ } else if let Some(idx) = self.selected_idx {
+ let (is_text, is_vector) = match &self.elements[idx] {
+ Element::Text { .. } => (true, false),
+ Element::Vector { .. } => (false, true),
+ _ => (false, false),
+ };
+
+ let mut sec = Section::new(&mut pc, cx, cy, cw, "Geometry");
+ sec.widget(&mut pc, &mut self.sidebar_x, 12.0, col_w, 26.0);
+ sec.spacing(16.0);
+ sec.widget(&mut pc, &mut self.sidebar_y, 12.0, col_w, 26.0);
+ sec.spacing(16.0);
+ sec.widget(&mut pc, &mut self.sidebar_w, 12.0, col_w, 26.0);
+ sec.spacing(16.0);
+ sec.widget(&mut pc, &mut self.sidebar_h, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ let next_y = sec.finish(&mut pc);
+
+ let mut sec_align = Section::new(&mut pc, cx, next_y, cw, "Alignment");
+ sec_align.widget(&mut pc, &mut self.dropdown_align_h, 12.0, col_w, 26.0);
+ sec_align.spacing(12.0);
+ sec_align.widget(&mut pc, &mut self.dropdown_align_v, 12.0, col_w, 26.0);
+ let next_y = sec_align.finish(&mut pc);
+
+ if is_text {
+ let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Text Properties");
+ sec2.widget(&mut pc, &mut self.sidebar_text, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.toggle_multiline, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.font_selector, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.sidebar_size, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_r, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_g, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_b, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ let next_y_align = sec2.finish(&mut pc);
+
+ let mut sec_text_align = Section::new(&mut pc, cx, next_y_align, cw, "Text Alignment");
+ sec_text_align.widget(&mut pc, &mut self.dropdown_text_align_h, 12.0, col_w, 26.0);
+ sec_text_align.spacing(12.0);
+ sec_text_align.widget(&mut pc, &mut self.dropdown_text_align_v, 12.0, col_w, 26.0);
+ sec_text_align.finish(&mut pc);
+ } else if is_vector {
+ let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Line Properties");
+ sec2.widget(&mut pc, &mut self.sidebar_size, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.dropdown_line_cap, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_r, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_g, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_b, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.finish(&mut pc);
+ } else {
+ let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Fill Color");
+ sec2.widget(&mut pc, &mut self.slider_r, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_g, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.slider_b, 12.0, col_w, 18.0);
+ sec2.spacing(12.0);
+ sec2.finish(&mut pc);
+ }
+ } else {
+ let mut sec = Section::new(&mut pc, cx, cy, cw, "Properties");
+ sec.widget(&mut pc, &mut self.label_sel_status, 12.0, col_w, 18.0);
+ sec.spacing(8.0);
+ sec.widget(&mut pc, &mut self.label_sel_desc1, 12.0, col_w, 14.0);
+ sec.spacing(6.0);
+ sec.widget(&mut pc, &mut self.label_sel_desc2, 12.0, col_w, 14.0);
+ sec.spacing(12.0);
+ sec.finish(&mut pc);
+ }
+ }
+ 3 => {
+ let mut sec = Section::new(&mut pc, cx, cy, cw, "Add Elements");
+ let col_w = cw - 40.0;
+ sec.widget(&mut pc, &mut self.btn_add_text, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.btn_add_rect, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.btn_add_banner, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.btn_add_vector, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.label_total_elements, 12.0, col_w, 18.0);
+ sec.spacing(12.0);
+ sec.finish(&mut pc);
+ }
+ 4 => {
+ let mut sec = Section::new(&mut pc, cx, cy, cw, "Grid");
+ let col_w = cw - 40.0;
+ sec.widget(&mut pc, &mut self.toggle_grid, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.grid_color_selector, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.spinbox_grid_size, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.label_grid_snap, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ sec.widget(&mut pc, &mut self.dropdown_grid_units, 12.0, col_w, 26.0);
+ sec.spacing(12.0);
+ let next_y = sec.finish(&mut pc);
+
+ let mut sec_zoom = Section::new(&mut pc, cx, next_y, cw, "Zoom");
+ sec_zoom.widget(&mut pc, &mut self.slider_zoom, 12.0, col_w, 18.0);
+ sec_zoom.spacing(12.0);
+ let next_y = sec_zoom.finish(&mut pc);
+
+ let mut sec_margins = Section::new(&mut pc, cx, next_y, cw, "Margins");
+ sec_margins.widget(&mut pc, &mut self.margin_color_selector, 12.0, col_w, 26.0);
+ sec_margins.spacing(12.0);
+ sec_margins.widget(&mut pc, &mut self.spinbox_margin_thickness, 12.0, col_w, 26.0);
+ sec_margins.spacing(12.0);
+ let next_y = sec_margins.finish(&mut pc);
+
+ let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Rulers");
+ sec2.widget(&mut pc, &mut self.toggle_rulers, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.widget(&mut pc, &mut self.dropdown_units, 12.0, col_w, 26.0);
+ sec2.spacing(12.0);
+ sec2.finish(&mut pc);
+ }
+ 5 => {
+ let mut sec = Section::new(&mut pc, cx, cy, cw, "Layers List");
+ let col_w = cw - 40.0;
+ if self.layer_buttons.is_empty() {
+ let mut label_no_layers = Label::new("No elements found").with_color([0x83, 0x83, 0x8a]);
+ sec.widget(&mut pc, &mut label_no_layers, 12.0, col_w, 18.0);
+ sec.spacing(12.0);
+ } else {
+ for btn in &mut self.layer_buttons {
+ sec.widget(&mut pc, &mut **btn, 12.0, col_w, 26.0);
+ sec.spacing(8.0);
+ }
+ }
+ sec.finish(&mut pc);
+ }
+ _ => {}
+ }
+
+ ui_frame.finish(&mut pc);
+
+ // Store the sidebar quads
+ for (c, x, y, w, h) in pc.rects {
+ self.sidebar_quads.push((x, y, w, h, c));
+ }
+
+ // Convert pc.texts to glyphon text items
+ for (text, size, x, y, color, font, bounds) in pc.texts {
+ self.text_items.push(TextItem {
+ buffer: make_text_buffer_with_font(&mut self.font_system, &text, size, font.as_deref()),
+ x,
+ y,
+ color: glyphon::Color::rgb(
+ (color[0] * 255.0).clamp(0.0, 255.0) as u8,
+ (color[1] * 255.0).clamp(0.0, 255.0) as u8,
+ (color[2] * 255.0).clamp(0.0, 255.0) as u8,
+ ),
+ bounds,
+ });
+ }
if let Some(idx) = self.selected_idx {
self.label_sel_status.set_text(&format!("Selected Element #{}", idx + 1));
@@ -230,23 +1059,32 @@ impl LayoutApp {
self.label_sel_desc2.set_text("to edit properties.");
}
- self.label_grid_snap.set_text(&format!("Grid Snapping: {}", if self.grid_enabled { "ON (20px)" } else { "OFF" }));
+ let snap_suffix = match self.dropdown_grid_units.selected {
+ 0 => "20px",
+ 1 => "0.25in",
+ 2 => "0.5cm",
+ 3 => "5mm",
+ _ => "20px",
+ };
+ self.label_grid_snap.set_label(&format!(
+ "Grid Snapping: {}",
+ if self.grid_enabled {
+ format!("ON ({})", snap_suffix)
+ } else {
+ "OFF".to_string()
+ }
+ ));
self.label_total_elements.set_text(&format!("Total Elements: {}", self.elements.len()));
- self.text_items.clear();
-
let mut labels = Vec::new();
let canvas_w = self.width as f32 - 280.0;
- let canvas_h = self.height as f32 - 26.0;
+ let canvas_h = self.height as f32;
let zoom = self.render_zoom();
let page_w = self.page_w * zoom;
let page_h = self.page_h * zoom;
let page_x = 280.0 + (canvas_w - page_w) / 2.0 + self.pan_x;
- let page_y = 26.0 + (canvas_h - page_h) / 2.0 + self.pan_y;
-
- // 1. MenuBar text labels
- labels.extend(self.menu_bar.text_labels());
+ let page_y = (canvas_h - page_h) / 2.0 + self.pan_y;
// 2. Paginator sidebar tabs
labels.extend(self.paginator.text_labels());
@@ -255,23 +1093,6 @@ impl LayoutApp {
let _ = writeln!(file, "rebuild_text_items: labels={:?}", labels.iter().map(|l| &l.text).collect::<Vec<_>>());
}
- // 3. Collect and add active page popover labels
- let mut collector = clear_ui::layout::PopoverCollector::new();
- self.paginator.render_popover(&mut collector);
- for (text, size, x, y, color, _font) in collector.texts {
- labels.push(TextLabel {
- text,
- x,
- y,
- font_size: size,
- color: [
- (color[0] * 255.0).clamp(0.0, 255.0) as u8,
- (color[1] * 255.0).clamp(0.0, 255.0) as u8,
- (color[2] * 255.0).clamp(0.0, 255.0) as u8,
- ],
- });
- }
-
if self.toggle_rulers.toggled() {
let unit_idx = self.dropdown_units.selected;
let (px_per_unit, tick_step, label_step, precision) = match unit_idx {
@@ -322,10 +1143,16 @@ impl LayoutApp {
// 5. Canvas Element Labels (drawn relative to the paper sheet)
if self.word_processor_enabled {
let font_family = self.wp_text_box.font_family.clone();
- for label in self.wp_text_box.text_labels() {
+ for (label, bounds) in self.wp_text_box.text_labels_with_bounds() {
let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
let mut buf = Buffer::new(&mut self.font_system, metrics);
- let attrs = Attrs::new().family(glyphon::Family::Name(&font_family));
+ let family_val = match font_family.as_str() {
+ "monospace" => glyphon::Family::Monospace,
+ "sans-serif" => glyphon::Family::SansSerif,
+ "serif" => glyphon::Family::Serif,
+ _ => glyphon::Family::Name(&font_family),
+ };
+ let attrs = Attrs::new().family(family_val);
buf.set_text(&mut self.font_system, &label.text, attrs, glyphon::Shaping::Advanced);
buf.shape_until_scroll(&mut self.font_system, true);
self.text_items.push(TextItem {
@@ -333,14 +1160,13 @@ impl LayoutApp {
x: label.x,
y: label.y,
color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
+ bounds,
});
}
} else {
for (idx, element) in self.elements.iter().enumerate() {
match element {
- Element::Text { text, x, y, w: _, h: _, font_size, color, font_family } => {
- let label_x = page_x + *x * zoom + 8.0 * zoom;
- let label_y = page_y + *y * zoom + 6.0 * zoom;
+ Element::Text { text, x, y, w, h, font_size, color, font_family, align_h, align_v, multiline } => {
let label_font_size = *font_size * zoom;
let label_color = [
(color[0] * 255.0).clamp(0.0, 255.0) as u8,
@@ -350,14 +1176,53 @@ impl LayoutApp {
let metrics = Metrics::new(label_font_size, label_font_size * 1.4);
let mut buf = Buffer::new(&mut self.font_system, metrics);
- let attrs = Attrs::new().family(glyphon::Family::Name(font_family));
+ let family_val = match font_family.as_str() {
+ "monospace" => glyphon::Family::Monospace,
+ "sans-serif" => glyphon::Family::SansSerif,
+ "serif" => glyphon::Family::Serif,
+ _ => glyphon::Family::Name(font_family),
+ };
+ let attrs = Attrs::new().family(family_val);
buf.set_text(&mut self.font_system, text, attrs, glyphon::Shaping::Advanced);
+
+ let text_w = (*w * zoom - 16.0 * zoom).max(0.0);
+ let text_h = (*h * zoom - 12.0 * zoom).max(0.0);
+ let text_w_opt = if *multiline { Some(text_w) } else { None };
+ buf.set_size(&mut self.font_system, text_w_opt, Some(text_h));
+
+ let align_val = match align_h {
+ TextAlignH::Left => glyphon::cosmic_text::Align::Left,
+ TextAlignH::Center => glyphon::cosmic_text::Align::Center,
+ TextAlignH::Right => glyphon::cosmic_text::Align::Right,
+ };
+ for line in &mut buf.lines {
+ line.set_align(Some(align_val));
+ }
+
buf.shape_until_scroll(&mut self.font_system, true);
+
+ // Calculate vertical alignment offset
+ let mut layout_runs_count = 0;
+ for _ in buf.layout_runs() {
+ layout_runs_count += 1;
+ }
+ let line_height = label_font_size * 1.4;
+ let total_text_height = layout_runs_count as f32 * line_height;
+ let vertical_offset = match align_v {
+ TextAlignV::Middle => ((text_h - total_text_height) / 2.0).max(0.0),
+ TextAlignV::Bottom => (text_h - total_text_height).max(0.0),
+ TextAlignV::Top => 0.0,
+ };
+
+ let label_x = page_x + *x * zoom + 8.0 * zoom;
+ let label_y = page_y + *y * zoom + 6.0 * zoom + vertical_offset;
+
self.text_items.push(TextItem {
buffer: buf,
x: label_x,
y: label_y,
color: glyphon::Color::rgb(label_color[0], label_color[1], label_color[2]),
+ bounds: None,
});
}
Element::Shape { shape_type, x, y, w: _, h: _, color: _ } => {
@@ -373,6 +1238,15 @@ impl LayoutApp {
color: [0xee, 0xee, 0xf5],
});
}
+ Element::Vector { x1, y1, .. } => {
+ labels.push(TextLabel {
+ text: format!("Line #{}", idx + 1),
+ x: page_x + *x1 * zoom + 8.0 * zoom,
+ y: page_y + *y1 * zoom + 6.0 * zoom,
+ font_size: 11.0 * zoom,
+ color: [0xee, 0xee, 0xf5],
+ });
+ }
}
}
}
@@ -387,66 +1261,44 @@ impl LayoutApp {
x: label.x,
y: label.y,
color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
+ bounds: None,
});
}
}
- fn rebuild_element_tab_widgets(&mut self) {
- self.paginator.clear_page_widgets(1);
-
- let self_ptr = self as *mut Self;
- unsafe {
- if self.word_processor_enabled {
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).dropdown_font_family as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_size as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).slider_r as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).slider_g as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).slider_b as *mut (dyn Widget + 'static));
- } else if let Some(idx) = self.selected_idx {
- match &self.elements[idx] {
- Element::Text { .. } => {
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_x as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_y as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_w as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_h as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_text as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).dropdown_font_family as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_size as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).slider_r as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).slider_g as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).slider_b as *mut (dyn Widget + 'static));
- }
- Element::Shape { .. } => {
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_x as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_y as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_w as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).sidebar_h as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).slider_r as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).slider_g as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).slider_b as *mut (dyn Widget + 'static));
- }
+ fn rebuild_layers_tab_widgets(&mut self) {
+ self.layer_buttons.clear();
+ for (idx, element) in self.elements.iter().enumerate() {
+ let label = match element {
+ Element::Text { text, .. } => {
+ let truncated: String = text.chars().take(20).collect();
+ format!("{}. Text: \"{}\"", idx + 1, truncated)
}
- } else {
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).label_sel_status as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).label_sel_desc1 as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(1, &mut (*self_ptr).label_sel_desc2 as *mut (dyn Widget + 'static));
- }
+ Element::Shape { shape_type, .. } => {
+ let type_str = match shape_type {
+ ShapeType::Rectangle => "Rectangle",
+ ShapeType::Banner => "Banner",
+ };
+ format!("{}. {}", idx + 1, type_str)
+ }
+ Element::Vector { x1, y1, x2, y2, .. } => {
+ format!("{}. Line: ({:.0},{:.0})->({:.0},{:.0})", idx + 1, x1, y1, x2, y2)
+ }
+ };
+
+ let mut btn = Box::new(Button::new_list_row(0.0, 0.0, 224.0, 26.0).with_label(&label));
+ btn.selected = Some(idx) == self.selected_idx;
+ self.layer_buttons.push(btn);
}
- let (px, py, pw, ph) = self.paginator.rect();
- self.paginator.set_rect(px, py, pw, ph);
}
+
fn sync_sidebar_fields(&mut self) {
if self.word_processor_enabled {
if !self.sidebar_size.editing {
self.sidebar_size.value = self.wp_base_font_size.round() as i32;
}
- let dropdown_idx = match self.wp_text_box.font_family.as_str() {
- "sans-serif" => 1,
- "serif" => 2,
- _ => 0, // monospace
- };
- self.dropdown_font_family.selected = dropdown_idx;
+ self.font_selector.font_family = self.wp_text_box.font_family.clone();
if let Some(rgb) = self.wp_text_box.text_color {
self.slider_r.set_value(rgb[0] as f32 / 255.0);
@@ -470,47 +1322,124 @@ impl LayoutApp {
let element = &self.elements[idx];
match element {
- Element::Text { text, x, y, w, h, font_size, color, font_family } => {
+ Element::Text { text, x, y, w, h, font_size, color, font_family, align_h, align_v, multiline } => {
+ self.sidebar_x.base_mut().map(|b| b.label = Some("X Position".to_string()));
+ self.sidebar_y.base_mut().map(|b| b.label = Some("Y Position".to_string()));
+ self.sidebar_w.base_mut().map(|b| b.label = Some("Width".to_string()));
+ self.sidebar_h.base_mut().map(|b| b.label = Some("Height".to_string()));
+ self.sidebar_size.base_mut().map(|b| b.label = Some("Text Size".to_string()));
+
if !self.sidebar_text.editing { self.sidebar_text.text = text.clone(); }
- if !self.sidebar_x.editing { self.sidebar_x.text = format!("{:.0}", x); }
- if !self.sidebar_y.editing { self.sidebar_y.text = format!("{:.0}", y); }
- if !self.sidebar_w.editing { self.sidebar_w.text = format!("{:.0}", w); }
- if !self.sidebar_h.editing { self.sidebar_h.text = format!("{:.0}", h); }
+ if !self.sidebar_x.editing { self.sidebar_x.value = x.round() as i32; }
+ if !self.sidebar_y.editing { self.sidebar_y.value = y.round() as i32; }
+ if !self.sidebar_w.editing { self.sidebar_w.value = w.round() as i32; }
+ if !self.sidebar_h.editing { self.sidebar_h.value = h.round() as i32; }
if !self.sidebar_size.editing { self.sidebar_size.value = font_size.round() as i32; }
self.slider_r.set_value(color[0]);
self.slider_g.set_value(color[1]);
self.slider_b.set_value(color[2]);
- let dropdown_idx = match font_family.as_str() {
- "sans-serif" => 1,
- "serif" => 2,
- _ => 0, // monospace
+ self.font_selector.font_family = font_family.clone();
+ self.toggle_multiline.set_toggled(*multiline);
+
+ self.dropdown_text_align_h.selected = match align_h {
+ TextAlignH::Left => 0,
+ TextAlignH::Center => 1,
+ TextAlignH::Right => 2,
+ };
+ self.dropdown_text_align_v.selected = match align_v {
+ TextAlignV::Top => 0,
+ TextAlignV::Middle => 1,
+ TextAlignV::Bottom => 2,
};
- self.dropdown_font_family.selected = dropdown_idx;
}
Element::Shape { shape_type: _, x, y, w, h, color } => {
+ self.sidebar_x.base_mut().map(|b| b.label = Some("X Position".to_string()));
+ self.sidebar_y.base_mut().map(|b| b.label = Some("Y Position".to_string()));
+ self.sidebar_w.base_mut().map(|b| b.label = Some("Width".to_string()));
+ self.sidebar_h.base_mut().map(|b| b.label = Some("Height".to_string()));
+
if !self.sidebar_text.editing { self.sidebar_text.text = "Shape Mode".to_string(); }
- if !self.sidebar_x.editing { self.sidebar_x.text = format!("{:.0}", x); }
- if !self.sidebar_y.editing { self.sidebar_y.text = format!("{:.0}", y); }
- if !self.sidebar_w.editing { self.sidebar_w.text = format!("{:.0}", w); }
- if !self.sidebar_h.editing { self.sidebar_h.text = format!("{:.0}", h); }
+ if !self.sidebar_x.editing { self.sidebar_x.value = x.round() as i32; }
+ if !self.sidebar_y.editing { self.sidebar_y.value = y.round() as i32; }
+ if !self.sidebar_w.editing { self.sidebar_w.value = w.round() as i32; }
+ if !self.sidebar_h.editing { self.sidebar_h.value = h.round() as i32; }
+
+ self.slider_r.set_value(color[0]);
+ self.slider_g.set_value(color[1]);
+ self.slider_b.set_value(color[2]);
+ }
+ Element::Vector { x1, y1, x2, y2, stroke_width, color, line_cap } => {
+ self.sidebar_x.base_mut().map(|b| b.label = Some("Start X".to_string()));
+ self.sidebar_y.base_mut().map(|b| b.label = Some("Start Y".to_string()));
+ self.sidebar_w.base_mut().map(|b| b.label = Some("End X".to_string()));
+ self.sidebar_h.base_mut().map(|b| b.label = Some("End Y".to_string()));
+ self.sidebar_size.base_mut().map(|b| b.label = Some("Thickness".to_string()));
+
+ if !self.sidebar_text.editing { self.sidebar_text.text = "Line Mode".to_string(); }
+ if !self.sidebar_x.editing { self.sidebar_x.value = x1.round() as i32; }
+ if !self.sidebar_y.editing { self.sidebar_y.value = y1.round() as i32; }
+ if !self.sidebar_w.editing { self.sidebar_w.value = x2.round() as i32; }
+ if !self.sidebar_h.editing { self.sidebar_h.value = y2.round() as i32; }
+ if !self.sidebar_size.editing { self.sidebar_size.value = stroke_width.round() as i32; }
+ self.dropdown_line_cap.selected = match line_cap {
+ LineCap::Arrow => 0,
+ LineCap::Round => 1,
+ LineCap::Flat => 2,
+ };
self.slider_r.set_value(color[0]);
self.slider_g.set_value(color[1]);
self.slider_b.set_value(color[2]);
}
}
+
+ // Sync page alignment dropdowns based on current positions
+ let (ex, ey, ew, eh) = match element {
+ Element::Text { x, y, w, h, .. } => (*x, *y, *w, *h),
+ Element::Shape { x, y, w, h, .. } => (*x, *y, *w, *h),
+ Element::Vector { x1, y1, x2, y2, .. } => {
+ let x = x1.min(*x2);
+ let y = y1.min(*y2);
+ let w = (x1 - x2).abs();
+ let h = (y1 - y2).abs();
+ (x, y, w, h)
+ }
+ };
+
+ let center_x = ((self.page_w - ew) / 2.0).round();
+ let right_x = (self.page_w - ew).round();
+ self.dropdown_align_h.selected = if (ex - 0.0).abs() < 0.1 {
+ 1 // Left
+ } else if (ex - center_x).abs() < 0.1 {
+ 2 // Center
+ } else if (ex - right_x).abs() < 0.1 {
+ 3 // Right
+ } else {
+ 0 // "--" (No alignment)
+ };
+
+ let middle_y = ((self.page_h - eh) / 2.0).round();
+ let bottom_y = (self.page_h - eh).round();
+ self.dropdown_align_v.selected = if (ey - 0.0).abs() < 0.1 {
+ 1 // Top
+ } else if (ey - middle_y).abs() < 0.1 {
+ 2 // Middle
+ } else if (ey - bottom_y).abs() < 0.1 {
+ 3 // Bottom
+ } else {
+ 0 // "--" (No alignment)
+ };
}
}
fn apply_sidebar_changes(&mut self) {
if self.word_processor_enabled {
- let family_str = match self.dropdown_font_family.selected {
- 1 => "sans-serif",
- 2 => "serif",
- _ => "monospace",
- }.to_string();
+ let mut family_str = self.wp_text_box.font_family.clone();
+ if self.font_selector.take_change() {
+ family_str = self.font_selector.font_family.clone();
+ }
self.wp_text_box.font_family = family_str;
let size_val = if self.sidebar_size.editing {
@@ -526,10 +1455,48 @@ impl LayoutApp {
self.wp_text_box.text_color = Some([r, g, b]);
} else if let Some(idx) = self.selected_idx {
let text_val = if self.sidebar_text.editing { self.sidebar_text.edit_buffer.clone() } else { self.sidebar_text.text.clone() };
- let x_val = if self.sidebar_x.editing { &self.sidebar_x.edit_buffer } else { &self.sidebar_x.text }.parse::<f32>().unwrap_or(0.0);
- let y_val = if self.sidebar_y.editing { &self.sidebar_y.edit_buffer } else { &self.sidebar_y.text }.parse::<f32>().unwrap_or(0.0);
- let w_val = if self.sidebar_w.editing { &self.sidebar_w.edit_buffer } else { &self.sidebar_w.text }.parse::<f32>().unwrap_or(100.0);
- let h_val = if self.sidebar_h.editing { &self.sidebar_h.edit_buffer } else { &self.sidebar_h.text }.parse::<f32>().unwrap_or(50.0);
+ let x_val = if self.sidebar_x.editing {
+ self.sidebar_x.edit_buffer.parse::<f32>().unwrap_or(0.0)
+ } else {
+ self.sidebar_x.value as f32
+ };
+ let y_val = if self.sidebar_y.editing {
+ self.sidebar_y.edit_buffer.parse::<f32>().unwrap_or(0.0)
+ } else {
+ self.sidebar_y.value as f32
+ };
+ let w_val = if self.sidebar_w.editing {
+ self.sidebar_w.edit_buffer.parse::<f32>().unwrap_or(100.0)
+ } else {
+ self.sidebar_w.value as f32
+ };
+ let h_val = if self.sidebar_h.editing {
+ self.sidebar_h.edit_buffer.parse::<f32>().unwrap_or(50.0)
+ } else {
+ self.sidebar_h.value as f32
+ };
+
+ let is_vector = match &self.elements[idx] {
+ Element::Vector { .. } => true,
+ _ => false,
+ };
+ let (v_x2, v_y2) = if is_vector {
+ let nx2 = if self.sidebar_w.editing {
+ self.sidebar_w.edit_buffer.parse::<f32>().unwrap_or(0.0)
+ } else {
+ self.sidebar_w.value as f32
+ };
+ let ny2 = if self.sidebar_h.editing {
+ self.sidebar_h.edit_buffer.parse::<f32>().unwrap_or(0.0)
+ } else {
+ self.sidebar_h.value as f32
+ };
+ (nx2, ny2)
+ } else {
+ (0.0, 0.0)
+ };
+ let w_for_align = if is_vector { (v_x2 - x_val).abs() } else { w_val };
+ let h_for_align = if is_vector { (v_y2 - y_val).abs() } else { h_val };
let size_val = if self.sidebar_size.editing {
self.sidebar_size.edit_buffer.parse::<f32>().unwrap_or(16.0)
} else {
@@ -540,31 +1507,261 @@ impl LayoutApp {
let g_val = self.slider_g.value();
let b_val = self.slider_b.value();
- let family_str = match self.dropdown_font_family.selected {
- 1 => "sans-serif",
- 2 => "serif",
- _ => "monospace",
- }.to_string();
+ let mut family_str = String::new();
+ if let Some(Element::Text { font_family, .. }) = self.elements.get(idx) {
+ family_str = font_family.clone();
+ }
+ if self.font_selector.take_change() {
+ family_str = self.font_selector.font_family.clone();
+ }
+
+ let mut page_align_h = None;
+ if self.dropdown_align_h.take_change() {
+ page_align_h = Some(self.dropdown_align_h.selected);
+ }
+ let mut page_align_v = None;
+ if self.dropdown_align_v.take_change() {
+ page_align_v = Some(self.dropdown_align_v.selected);
+ }
+
+ let mut text_align_h = None;
+ if self.dropdown_text_align_h.take_change() {
+ text_align_h = Some(match self.dropdown_text_align_h.selected {
+ 1 => TextAlignH::Center,
+ 2 => TextAlignH::Right,
+ _ => TextAlignH::Left,
+ });
+ }
+ let mut text_align_v = None;
+ if self.dropdown_text_align_v.take_change() {
+ text_align_v = Some(match self.dropdown_text_align_v.selected {
+ 1 => TextAlignV::Middle,
+ 2 => TextAlignV::Bottom,
+ _ => TextAlignV::Top,
+ });
+ }
+
+ let mut final_x = x_val;
+ let mut final_y = y_val;
+
+ if let Some(sel_h) = page_align_h {
+ if sel_h == 1 {
+ final_x = 0.0;
+ } else if sel_h == 2 {
+ final_x = ((self.page_w - w_for_align) / 2.0).round();
+ } else if sel_h == 3 {
+ final_x = (self.page_w - w_for_align).round();
+ }
+ }
+ if let Some(sel_v) = page_align_v {
+ if sel_v == 1 {
+ final_y = 0.0;
+ } else if sel_v == 2 {
+ final_y = ((self.page_h - h_for_align) / 2.0).round();
+ } else if sel_v == 3 {
+ final_y = (self.page_h - h_for_align).round();
+ }
+ }
+
+ let mut final_x2 = v_x2;
+ let mut final_y2 = v_y2;
+ if is_vector {
+ let dx = final_x - x_val;
+ let dy = final_y - y_val;
+ final_x2 += dx;
+ final_y2 += dy;
+ }
match &mut self.elements[idx] {
- Element::Text { text, x, y, w, h, font_size, color, font_family } => {
+ Element::Text { text, x, y, w, h, font_size, color, font_family, align_h, align_v, multiline } => {
*text = text_val;
- *x = x_val;
- *y = y_val;
*w = w_val;
*h = h_val;
*font_size = size_val;
*color = [r_val, g_val, b_val, 1.0];
- *font_family = family_str;
+ if !family_str.is_empty() {
+ *font_family = family_str;
+ }
+ if let Some(ah) = text_align_h {
+ *align_h = ah;
+ }
+ if let Some(av) = text_align_v {
+ *align_v = av;
+ }
+ *multiline = self.toggle_multiline.toggled();
+ *x = final_x;
+ *y = final_y;
+ }
+ Element::Shape { shape_type: _, x, y, w, h, color } => {
+ *w = w_val;
+ *h = h_val;
+ *color = [r_val, g_val, b_val, 1.0];
+ *x = final_x;
+ *y = final_y;
+ }
+ Element::Vector { x1, y1, x2, y2, stroke_width, color, line_cap } => {
+ *x1 = final_x;
+ *y1 = final_y;
+ *x2 = final_x2;
+ *y2 = final_y2;
+ *stroke_width = size_val;
+ *color = [r_val, g_val, b_val, 1.0];
+ *line_cap = match self.dropdown_line_cap.selected {
+ 0 => LineCap::Arrow,
+ 1 => LineCap::Round,
+ 2 => LineCap::Flat,
+ _ => LineCap::Arrow,
+ };
+ }
+ }
+ }
+ self.rebuild_layers_tab_widgets();
+ self.sync_sidebar_fields();
+ }
+
+ fn active_page_widgets(&mut self) -> Vec<&mut dyn Widget> {
+ let mut list: Vec<&mut dyn Widget> = Vec::new();
+ match self.paginator.selected_page() {
+ 0 => {
+ list.push(&mut self.btn_new_doc);
+ list.push(&mut self.btn_open);
+ list.push(&mut self.recent_files_list);
+ for btn in &mut self.recent_files_buttons {
+ list.push(btn);
+ }
+ list.push(&mut self.btn_save);
+ list.push(&mut self.btn_save_as);
+ list.push(&mut self.btn_exit);
+ }
+ 1 => {
+ list.push(&mut self.dropdown_margin_units);
+ list.push(&mut self.dropdown_presets);
+ list.push(&mut self.slider_page_x);
+ list.push(&mut self.slider_page_y);
+ list.push(&mut self.page_color_selector);
+ list.push(&mut self.toggle_margin);
+ list.push(&mut self.toggle_word_processor);
+ list.push(&mut self.slider_margin_x);
+ list.push(&mut self.slider_margin_y);
+ }
+ 2 => {
+ if self.word_processor_enabled {
+ list.push(&mut self.font_selector);
+ list.push(&mut self.sidebar_size);
+ list.push(&mut self.slider_r);
+ list.push(&mut self.slider_g);
+ list.push(&mut self.slider_b);
+ } else if self.selected_idx.is_some() {
+ list.push(&mut self.dropdown_align_h);
+ list.push(&mut self.dropdown_align_v);
+
+ let (is_text, is_vector) = match self.selected_idx.map(|idx| &self.elements[idx]) {
+ Some(Element::Text { .. }) => (true, false),
+ Some(Element::Vector { .. }) => (false, true),
+ _ => (false, false),
+ };
+ if is_text {
+ list.push(&mut self.sidebar_x);
+ list.push(&mut self.sidebar_y);
+ list.push(&mut self.sidebar_w);
+ list.push(&mut self.sidebar_h);
+ list.push(&mut self.sidebar_text);
+ list.push(&mut self.font_selector);
+ list.push(&mut self.toggle_multiline);
+ list.push(&mut self.dropdown_text_align_h);
+ list.push(&mut self.dropdown_text_align_v);
+ list.push(&mut self.sidebar_size);
+ list.push(&mut self.slider_r);
+ list.push(&mut self.slider_g);
+ list.push(&mut self.slider_b);
+ } else if is_vector {
+ list.push(&mut self.sidebar_x);
+ list.push(&mut self.sidebar_y);
+ list.push(&mut self.sidebar_w);
+ list.push(&mut self.sidebar_h);
+ list.push(&mut self.sidebar_size);
+ list.push(&mut self.dropdown_line_cap);
+ list.push(&mut self.slider_r);
+ list.push(&mut self.slider_g);
+ list.push(&mut self.slider_b);
+ } else {
+ list.push(&mut self.sidebar_x);
+ list.push(&mut self.sidebar_y);
+ list.push(&mut self.sidebar_w);
+ list.push(&mut self.sidebar_h);
+ list.push(&mut self.slider_r);
+ list.push(&mut self.slider_g);
+ list.push(&mut self.slider_b);
+ }
+ } else {
+ list.push(&mut self.label_sel_status);
+ list.push(&mut self.label_sel_desc1);
+ list.push(&mut self.label_sel_desc2);
}
- Element::Shape { shape_type: _, x, y, w, h, color } => {
- *x = x_val;
- *y = y_val;
- *w = w_val;
- *h = h_val;
- *color = [r_val, g_val, b_val, 1.0];
+ }
+ 3 => {
+ list.push(&mut self.btn_add_text);
+ list.push(&mut self.btn_add_rect);
+ list.push(&mut self.btn_add_banner);
+ list.push(&mut self.btn_add_vector);
+ list.push(&mut self.label_total_elements);
+ }
+ 4 => {
+ list.push(&mut self.toggle_grid);
+ list.push(&mut self.grid_color_selector);
+ list.push(&mut self.spinbox_grid_size);
+ list.push(&mut self.label_grid_snap);
+ list.push(&mut self.slider_zoom);
+ list.push(&mut self.toggle_rulers);
+ list.push(&mut self.dropdown_units);
+ list.push(&mut self.margin_color_selector);
+ list.push(&mut self.spinbox_margin_thickness);
+ list.push(&mut self.dropdown_grid_units);
+ }
+ 5 => {
+ for btn in &mut self.layer_buttons {
+ list.push(&mut **btn);
}
}
+ _ => {}
+ }
+ list
+ }
+
+ pub fn get_current_document(&self) -> LayoutDocument {
+ LayoutDocument {
+ page_w: self.page_w,
+ page_h: self.page_h,
+ page_color: self.page_color,
+ margin_enabled: self.margin_enabled,
+ margin_x: self.margin_x,
+ margin_y: self.margin_y,
+ word_processor_enabled: self.word_processor_enabled,
+ wp_text: self.wp_text_box.text.clone(),
+ elements: self.elements.clone(),
+ grid_color: self.grid_color,
+ grid_size: self.spinbox_grid_size.value as f32 / 10.0,
+ margin_color: self.margin_color,
+ margin_thickness: self.margin_thickness,
+ grid_enabled: self.grid_enabled,
+ grid_units: self.dropdown_grid_units.selected,
+ zoom: self.slider_zoom.value(),
+ rulers_enabled: self.toggle_rulers.toggled(),
+ ruler_units: self.dropdown_units.selected,
+ }
+ }
+
+ pub fn has_unsaved_changes(&self) -> bool {
+ self.get_current_document() != self.last_saved_document
+ }
+
+ pub fn grid_spacing(&self) -> f32 {
+ match self.dropdown_grid_units.selected {
+ 0 => 20.0, // Pixels
+ 1 => 0.25 * 60.0, // Inches
+ 2 => 0.5 * 23.622047, // Centimeters
+ 3 => 5.0 * 2.3622047, // Millimeters
+ _ => 20.0,
}
}
}
@@ -573,71 +1770,32 @@ impl Application for LayoutApp {
type Message = AppMessage;
fn new(_qh: &QueueHandle<EngineState<Self>>, _sender: calloop::channel::Sender<Self::Message>) -> Self {
- // Build the main MenuBar
- let menu_bar = MenuBar::new(0.0, 0.0, 1024.0, 26.0)
- .with_title("clear-layout")
- .with_item("File", &["New Document", "Exit"])
- .with_item("Insert", &["Add Text Box", "Add Rectangle", "Add Banner"])
- .with_item("View", &["Toggle Grid"]);
+ clear_ui::scale::set_scale_factor(1.0);
+ let btn_new_doc = Button::new(0.0, 0.0, 240.0, 26.0).with_label("New Document");
+ let btn_open = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Open");
+ let btn_save = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Save");
+ let btn_save_as = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Save As");
+ let btn_exit = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Exit Application");
// Build the sidebar Paginator
- let paginator = Paginator::new(56.0, vec!["Page".to_string(), "Element".to_string(), "Canvas".to_string(), "View".to_string()])
+ let paginator = Paginator::new(56.0, vec![
+ "File".to_string(),
+ "Page".to_string(),
+ "Element".to_string(),
+ "Canvas".to_string(),
+ "Guides".to_string(),
+ "Layers".to_string(),
+ ])
.with_tabs_at_top(false)
- .with_tabs_rotated(true);
+ .with_tabs_rotated(true)
+ .with_sidebar_label("LAYOUT");
// Default paper sheet sizing (Letter)
let page_w = 510.0;
let page_h = 660.0;
// Initialize elements relative to the paper sheet top-left (0, 0)
- let elements = vec![
- Element::Text {
- text: "Welcome to Clear Layout!".to_string(),
- x: 40.0,
- y: 50.0,
- w: 400.0,
- h: 40.0,
- font_size: 20.0,
- color: [0.3, 0.7, 1.0, 1.0],
- font_family: "monospace".to_string(),
- },
- Element::Shape {
- shape_type: ShapeType::Rectangle,
- x: 40.0,
- y: 100.0,
- w: 430.0,
- h: 4.0,
- color: [0.2, 0.5, 0.9, 1.0],
- },
- Element::Text {
- text: "This is a word processor and layout editor layout.".to_string(),
- x: 40.0,
- y: 130.0,
- w: 430.0,
- h: 30.0,
- font_size: 14.0,
- color: [0.8, 0.8, 0.85, 1.0],
- font_family: "monospace".to_string(),
- },
- Element::Shape {
- shape_type: ShapeType::Rectangle,
- x: 40.0,
- y: 190.0,
- w: 120.0,
- h: 80.0,
- color: [0.15, 0.25, 0.4, 1.0],
- },
- Element::Text {
- text: "Card Label".to_string(),
- x: 50.0,
- y: 205.0,
- w: 100.0,
- h: 20.0,
- font_size: 11.0,
- color: [0.9, 0.9, 0.9, 1.0],
- font_family: "monospace".to_string(),
- },
- ];
+ let elements = vec![];
// Create sidebar dropdown for page size presets
let mut dropdown_presets = Dropdown::new(
@@ -668,15 +1826,53 @@ impl Application for LayoutApp {
.with_text_color(Some([0x1a, 0x1a, 0x24]))
.with_max_width(None);
- let mut dropdown_font_family = Dropdown::new(
+ let mut font_selector = FontSelector::new("monospace".to_string()).with_label("Font");
+ font_selector.set_rect(0.0, 0.0, 240.0, 26.0);
+
+ let mut dropdown_text_align_h = Dropdown::new(
+ vec![
+ "Left".to_string(),
+ "Center".to_string(),
+ "Right".to_string(),
+ ],
+ 0,
+ ).with_label("Horizontal Alignment");
+ dropdown_text_align_h.set_rect(0.0, 0.0, 240.0, 26.0);
+
+ let mut dropdown_text_align_v = Dropdown::new(
+ vec![
+ "Top".to_string(),
+ "Middle".to_string(),
+ "Bottom".to_string(),
+ ],
+ 0,
+ ).with_label("Vertical Alignment");
+ dropdown_text_align_v.set_rect(0.0, 0.0, 240.0, 26.0);
+
+ let mut toggle_multiline = Toggle::new().with_label("Multiline Text");
+ toggle_multiline.set_rect(0.0, 0.0, 240.0, 26.0);
+
+ let mut dropdown_align_h = Dropdown::new(
+ vec![
+ "--".to_string(),
+ "Left".to_string(),
+ "Center".to_string(),
+ "Right".to_string(),
+ ],
+ 0,
+ ).with_label("Horizontal Alignment");
+ dropdown_align_h.set_rect(0.0, 0.0, 240.0, 26.0);
+
+ let mut dropdown_align_v = Dropdown::new(
vec![
- "Monospace".to_string(),
- "Sans-Serif".to_string(),
- "Serif".to_string(),
+ "--".to_string(),
+ "Top".to_string(),
+ "Middle".to_string(),
+ "Bottom".to_string(),
],
0,
- ).with_label("Font Family");
- dropdown_font_family.set_rect(0.0, 0.0, 240.0, 26.0);
+ ).with_label("Vertical Alignment");
+ dropdown_align_v.set_rect(0.0, 0.0, 240.0, 26.0);
let mut dropdown_margin_units = Dropdown::new(
vec![
@@ -694,15 +1890,25 @@ impl Application for LayoutApp {
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)");
slider_margin_y.set_rect(0.0, 0.0, 240.0, 18.0);
- // Create sidebar textbox inputs
- let mut sidebar_x = TextBox::new(String::new()).with_label("X Position");
- sidebar_x.set_rect(0.0, 0.0, 110.0, 26.0);
- let mut sidebar_y = TextBox::new(String::new()).with_label("Y Position");
- sidebar_y.set_rect(0.0, 0.0, 110.0, 26.0);
- let mut sidebar_w = TextBox::new(String::new()).with_label("Width");
- sidebar_w.set_rect(0.0, 0.0, 110.0, 26.0);
- let mut sidebar_h = TextBox::new(String::new()).with_label("Height");
- sidebar_h.set_rect(0.0, 0.0, 110.0, 26.0);
+ // Create sidebar spinbox inputs
+ let mut sidebar_x = Spinbox::new(0, -2000, 2000, 1).with_label("X Position");
+ sidebar_x.set_rect(0.0, 0.0, 240.0, 26.0);
+ let mut sidebar_y = Spinbox::new(0, -2000, 2000, 1).with_label("Y Position");
+ sidebar_y.set_rect(0.0, 0.0, 240.0, 26.0);
+ let mut sidebar_w = Spinbox::new(100, -2000, 2000, 1).with_label("Width");
+ sidebar_w.set_rect(0.0, 0.0, 240.0, 26.0);
+ let mut sidebar_h = Spinbox::new(50, -2000, 2000, 1).with_label("Height");
+ sidebar_h.set_rect(0.0, 0.0, 240.0, 26.0);
+
+ let mut dropdown_line_cap = Dropdown::new(
+ vec![
+ "Arrow".to_string(),
+ "Round".to_string(),
+ "Flat".to_string(),
+ ],
+ 0,
+ ).with_label("Line End Shape");
+ dropdown_line_cap.set_rect(0.0, 0.0, 240.0, 26.0);
let mut sidebar_text = TextBox::new(String::new()).with_label("Text Content");
sidebar_text.set_rect(0.0, 0.0, 240.0, 26.0);
let mut sidebar_size = Spinbox::new(14, 4, 120, 1).with_label("Text Size");
@@ -719,11 +1925,33 @@ impl Application for LayoutApp {
slider_zoom.set_rect(0.0, 0.0, 240.0, 18.0);
// Page 1 Buttons
- let btn_toggle_grid = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Toggle Grid");
- let btn_clear_canvas = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Clear Canvas");
- let btn_add_text = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Add Text Box");
- let btn_add_rect = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Add Rectangle");
- let btn_add_banner = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Add Banner");
+ let mut toggle_grid = Toggle::new().with_label("Toggle Grid");
+ toggle_grid.set_rect(0.0, 0.0, 240.0, 26.0);
+ toggle_grid.set_toggled(true);
+
+ let mut grid_color_selector = ColorSelector::new([51, 89, 153])
+ .with_label("Grid Color");
+ grid_color_selector.set_rect(0.0, 0.0, 240.0, 26.0);
+
+ let mut spinbox_grid_size = Spinbox::new(15, 5, 100, 1)
+ .with_decimals(1)
+ .with_label("Grid Dot Size (px)");
+ spinbox_grid_size.set_rect(0.0, 0.0, 240.0, 26.0);
+
+ let mut margin_color_selector = ColorSelector::new([51, 128, 217])
+ .with_label("Margin Guide Color");
+ margin_color_selector.set_rect(0.0, 0.0, 240.0, 26.0);
+
+ let mut spinbox_margin_thickness = Spinbox::new(1, 1, 20, 1)
+ .with_label("Margin Thickness (px)");
+ spinbox_margin_thickness.set_rect(0.0, 0.0, 240.0, 26.0);
+
+
+
+ let btn_add_text = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Text Box");
+ let btn_add_rect = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Rectangle");
+ let btn_add_banner = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Banner");
+ let btn_add_vector = Button::new(0.0, 0.0, 240.0, 26.0).with_label("Line");
// Page 3 Buttons
let mut toggle_rulers = Toggle::new().with_label("Show Rulers");
@@ -739,11 +1967,16 @@ impl Application for LayoutApp {
).with_label("Ruler Units");
dropdown_units.set_rect(0.0, 0.0, 240.0, 26.0);
- // Setup the new Label and SectionHeader widgets
- let mut section_rulers = SectionHeader::new("RULERS");
- section_rulers.set_rect(0.0, 0.0, 240.0, 26.0);
-
-
+ let mut dropdown_grid_units = Dropdown::new(
+ vec![
+ "Pixels (px)".to_string(),
+ "Inches (in)".to_string(),
+ "Centimeters (cm)".to_string(),
+ "Millimeters (mm)".to_string(),
+ ],
+ 0,
+ ).with_label("Grid Units");
+ dropdown_grid_units.set_rect(0.0, 0.0, 240.0, 26.0);
let mut label_sel_status = Label::new("No Selection").with_color([0x83, 0x83, 0x8a]);
label_sel_status.set_rect(0.0, 0.0, 240.0, 18.0);
@@ -754,14 +1987,33 @@ impl Application for LayoutApp {
let mut label_sel_desc2 = Label::new("to edit properties.").with_color([0x60, 0x60, 0x68]).with_font_size(10.0);
label_sel_desc2.set_rect(0.0, 0.0, 240.0, 14.0);
- let mut label_grid_snap = Label::new("Grid Snapping: ON (20px)");
- label_grid_snap.set_rect(0.0, 0.0, 240.0, 18.0);
+ let mut label_grid_snap = Toggle::new().with_label("Grid Snapping: ON (20px)");
+ label_grid_snap.set_rect(0.0, 0.0, 240.0, 26.0);
+ label_grid_snap.set_toggled(true);
- let mut label_total_elements = Label::new("Total Elements: 5");
+ let mut label_total_elements = Label::new("Total Elements: 0");
label_total_elements.set_rect(0.0, 0.0, 240.0, 18.0);
+ let recent_files = Self::load_recent_files();
+ let recent_files_list = ScrollingList::new(22.0, 2.0);
+ let mut recent_files_buttons = Vec::new();
+ for file in &recent_files {
+ let label = file.file_name()
+ .map(|n| n.to_string_lossy().to_string())
+ .unwrap_or_else(|| file.to_string_lossy().to_string());
+ let btn = Button::new_list_row(0.0, 0.0, 0.0, 0.0).with_label(&label);
+ recent_files_buttons.push(btn);
+ }
+
let mut app = Self {
- menu_bar,
+ btn_new_doc,
+ btn_open,
+ recent_files,
+ recent_files_list,
+ recent_files_buttons,
+ btn_exit,
+ btn_save,
+ btn_save_as,
paginator,
elements,
selected_idx: None,
@@ -795,7 +2047,13 @@ impl Application for LayoutApp {
sidebar_h,
sidebar_text,
sidebar_size,
- dropdown_font_family,
+ font_selector,
+ dropdown_text_align_h,
+ dropdown_text_align_v,
+ toggle_multiline,
+ dropdown_align_h,
+ dropdown_align_v,
+ dropdown_line_cap,
wp_base_font_size: 14.0,
slider_r,
@@ -803,16 +2061,23 @@ impl Application for LayoutApp {
slider_b,
slider_zoom,
- btn_toggle_grid,
- btn_clear_canvas,
+ toggle_grid,
+ grid_color_selector,
+ grid_color: [0.20, 0.35, 0.60, 0.8],
+ grid_size: 1.5,
+ spinbox_grid_size,
+ dropdown_grid_units,
+ margin_color_selector,
+ margin_color: [0.20, 0.50, 0.85, 0.35],
+ spinbox_margin_thickness,
+ margin_thickness: 1.0,
btn_add_text,
btn_add_rect,
btn_add_banner,
+ btn_add_vector,
toggle_rulers,
dropdown_units,
- section_rulers,
-
label_sel_status,
label_sel_desc1,
label_sel_desc2,
@@ -824,10 +2089,32 @@ impl Application for LayoutApp {
page_h,
pan_x: 0.0,
pan_y: 0.0,
- widgets_registered: false,
+ sidebar_quads: Vec::new(),
+ current_file_path: None,
+ layer_buttons: Vec::new(),
+ last_saved_document: LayoutDocument {
+ page_w,
+ page_h,
+ page_color,
+ margin_enabled: false,
+ margin_x: 20.0,
+ margin_y: 20.0,
+ word_processor_enabled: false,
+ wp_text: String::new(),
+ elements: Vec::new(),
+ grid_color: [0.20, 0.35, 0.60, 0.8],
+ grid_size: 1.5,
+ margin_color: [0.20, 0.50, 0.85, 0.35],
+ margin_thickness: 1.0,
+ grid_enabled: true,
+ grid_units: 0,
+ zoom: 0.33333334,
+ rulers_enabled: false,
+ ruler_units: 0,
+ },
};
- app.rebuild_element_tab_widgets();
+ app.paginator.set_selected_page(2);
app.sync_sidebar_fields();
app.sync_page_units();
app.rebuild_text_items();
@@ -835,8 +2122,19 @@ impl Application for LayoutApp {
}
fn settings(&self) -> WindowSettings {
+ let filename = match &self.current_file_path {
+ Some(path) => path.file_name()
+ .and_then(|n| n.to_str())
+ .unwrap_or("None"),
+ None => "None",
+ };
+ let unsaved_suffix = if self.has_unsaved_changes() {
+ " (Unsaved Changes)"
+ } else {
+ ""
+ };
WindowSettings {
- title: "Clear Layout Interface".to_string(),
+ title: format!("Clear Layout Interface - {}{}", filename, unsaved_suffix),
app_id: "clear-layout-interface".to_string(),
width: 1024,
height: 768,
@@ -850,6 +2148,25 @@ impl Application for LayoutApp {
AppMessage::Exit => {
*exit = true;
}
+ AppMessage::Save => {
+ if let Some(ref path) = self.current_file_path {
+ if let Err(e) = self.save_document(path) {
+ eprintln!("ERROR: Failed to save document: {}", e);
+ } else {
+ println!("DEBUG: Successfully saved layout to {:?}", path);
+ self.last_saved_document = self.get_current_document();
+ self.add_recent_file(path.clone());
+ }
+ } else {
+ self.perform_save_as();
+ }
+ }
+ AppMessage::SaveAs => {
+ self.perform_save_as();
+ }
+ AppMessage::Open => {
+ self.perform_open();
+ }
AppMessage::NewDocument => {
self.elements.clear();
self.selected_idx = None;
@@ -858,6 +2175,14 @@ impl Application for LayoutApp {
self.pan_y = 0.0;
self.page_color = [0.96, 0.96, 0.98, 1.0];
self.page_color_selector.color = [245, 245, 250];
+ self.grid_color = [0.20, 0.35, 0.60, 0.8];
+ self.grid_color_selector.color = [51, 89, 153];
+ self.grid_size = 1.5;
+ self.spinbox_grid_size.value = 15;
+ self.margin_color = [0.20, 0.50, 0.85, 0.35];
+ self.margin_color_selector.color = [51, 128, 217];
+ self.margin_thickness = 1.0;
+ self.spinbox_margin_thickness.value = 1;
self.margin_enabled = false;
self.margin_x = 20.0;
self.margin_y = 20.0;
@@ -874,14 +2199,25 @@ impl Application for LayoutApp {
self.dropdown_margin_units.selected = 0;
self.sync_page_units();
+ self.grid_enabled = true;
+ self.toggle_grid.set_toggled(true);
+ self.label_grid_snap.set_toggled(true);
+ self.dropdown_grid_units.selected = 0;
+ self.slider_zoom.set_value(0.33333334);
+ self.toggle_rulers.set_toggled(false);
+ self.dropdown_units.selected = 0;
+
self.toggle_word_processor.set_toggled(false);
self.word_processor_enabled = false;
self.wp_text_box.text = String::new();
self.wp_text_box.edit_buffer = String::new();
self.wp_text_box.editing = false;
- self.rebuild_element_tab_widgets();
+ self.current_file_path = None;
+
+ self.rebuild_layers_tab_widgets();
self.sync_sidebar_fields();
+ self.last_saved_document = self.get_current_document();
*needs_rebuild = true;
self.needs_rebuild = true;
}
@@ -896,9 +2232,12 @@ impl Application for LayoutApp {
font_size: 14.0,
color: [0.0, 0.0, 0.0, 1.0],
font_family: "monospace".to_string(),
+ align_h: TextAlignH::Left,
+ align_v: TextAlignV::Top,
+ multiline: true,
});
self.selected_idx = Some(next_idx);
- self.rebuild_element_tab_widgets();
+ self.rebuild_layers_tab_widgets();
self.sync_sidebar_fields();
*needs_rebuild = true;
self.needs_rebuild = true;
@@ -914,7 +2253,7 @@ impl Application for LayoutApp {
color: [0.2, 0.6, 0.4, 1.0],
});
self.selected_idx = Some(next_idx);
- self.rebuild_element_tab_widgets();
+ self.rebuild_layers_tab_widgets();
self.sync_sidebar_fields();
*needs_rebuild = true;
self.needs_rebuild = true;
@@ -930,13 +2269,25 @@ impl Application for LayoutApp {
color: [0.8, 0.4, 0.2, 1.0],
});
self.selected_idx = Some(next_idx);
- self.rebuild_element_tab_widgets();
+ self.rebuild_layers_tab_widgets();
self.sync_sidebar_fields();
*needs_rebuild = true;
self.needs_rebuild = true;
}
- AppMessage::ToggleGrid => {
- self.grid_enabled = !self.grid_enabled;
+ AppMessage::AddVector => {
+ let next_idx = self.elements.len();
+ self.elements.push(Element::Vector {
+ x1: 40.0,
+ y1: 100.0,
+ x2: 240.0,
+ y2: 100.0,
+ stroke_width: 4.0,
+ color: [0.2, 0.4, 0.8, 1.0],
+ line_cap: LineCap::Arrow,
+ });
+ self.selected_idx = Some(next_idx);
+ self.rebuild_layers_tab_widgets();
+ self.sync_sidebar_fields();
*needs_rebuild = true;
self.needs_rebuild = true;
}
@@ -948,6 +2299,16 @@ impl Application for LayoutApp {
*needs_rebuild = true;
self.needs_rebuild = true;
}
+ let mut widget_rebuild = false;
+ for w in self.active_page_widgets() {
+ if w.tick(dt) {
+ widget_rebuild = true;
+ }
+ }
+ if widget_rebuild {
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ }
let _ = self.page_color_selector.tick(dt);
let r = self.page_color_selector.color[0] as f32 / 255.0;
let g = self.page_color_selector.color[1] as f32 / 255.0;
@@ -959,6 +2320,66 @@ impl Application for LayoutApp {
self.needs_rebuild = true;
}
+ let mut new_grid_enabled = self.grid_enabled;
+ if self.toggle_grid.toggled() != self.grid_enabled {
+ new_grid_enabled = self.toggle_grid.toggled();
+ } else if self.label_grid_snap.toggled() != self.grid_enabled {
+ new_grid_enabled = self.label_grid_snap.toggled();
+ }
+
+ if self.grid_enabled != new_grid_enabled {
+ self.grid_enabled = new_grid_enabled;
+ self.toggle_grid.set_toggled(new_grid_enabled);
+ self.label_grid_snap.set_toggled(new_grid_enabled);
+ self.sync_sidebar_fields();
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ }
+
+ let _ = self.grid_color_selector.tick(dt);
+ let gr = self.grid_color_selector.color[0] as f32 / 255.0;
+ let gg = self.grid_color_selector.color[1] as f32 / 255.0;
+ let gb = self.grid_color_selector.color[2] as f32 / 255.0;
+ let new_grid_col = [gr, gg, gb, 0.8];
+ if self.grid_color != new_grid_col {
+ self.grid_color = new_grid_col;
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ }
+
+ let new_grid_size = if self.spinbox_grid_size.editing {
+ self.spinbox_grid_size.edit_buffer.parse::<f32>().unwrap_or(1.5)
+ } else {
+ self.spinbox_grid_size.value as f32 / 10.0
+ };
+ if self.grid_size != new_grid_size {
+ self.grid_size = new_grid_size;
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ }
+
+ let _ = self.margin_color_selector.tick(dt);
+ let mr = self.margin_color_selector.color[0] as f32 / 255.0;
+ let mg = self.margin_color_selector.color[1] as f32 / 255.0;
+ let mb = self.margin_color_selector.color[2] as f32 / 255.0;
+ let new_margin_col = [mr, mg, mb, 0.35];
+ if self.margin_color != new_margin_col {
+ self.margin_color = new_margin_col;
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ }
+
+ let new_margin_thickness = if self.spinbox_margin_thickness.editing {
+ self.spinbox_margin_thickness.edit_buffer.parse::<f32>().unwrap_or(1.0)
+ } else {
+ self.spinbox_margin_thickness.value as f32
+ };
+ if self.margin_thickness != new_margin_thickness {
+ self.margin_thickness = new_margin_thickness;
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ }
+
// Preset dropdown selection change
if self.dropdown_presets.take_change() {
let sel = self.dropdown_presets.selected;
@@ -989,6 +2410,13 @@ impl Application for LayoutApp {
self.needs_rebuild = true;
}
+ // Grid units dropdown change
+ if self.dropdown_grid_units.take_change() {
+ self.sync_sidebar_fields();
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ }
+
let unit_idx = self.dropdown_margin_units.selected;
let px_per_unit = match unit_idx {
0 => 1.0, // Pixels
@@ -1047,7 +2475,6 @@ impl Application for LayoutApp {
} else {
self.wp_text_box.unfocus();
}
- self.rebuild_element_tab_widgets();
self.sync_sidebar_fields();
*needs_rebuild = true;
self.needs_rebuild = true;
@@ -1057,72 +2484,36 @@ impl Application for LayoutApp {
fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64) {
let size_changed = self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale;
if self.needs_rebuild || size_changed {
- if !self.widgets_registered {
- let self_ptr = self as *mut Self;
- unsafe {
- (*self_ptr).paginator.add_widget_to_page(0, &mut (*self_ptr).dropdown_margin_units as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(0, &mut (*self_ptr).dropdown_presets as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(0, &mut (*self_ptr).slider_page_x as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(0, &mut (*self_ptr).slider_page_y as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(0, &mut (*self_ptr).page_color_selector as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(0, &mut (*self_ptr).toggle_margin as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(0, &mut (*self_ptr).toggle_word_processor as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(0, &mut (*self_ptr).slider_margin_x as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(0, &mut (*self_ptr).slider_margin_y as *mut (dyn Widget + 'static));
-
-
- // Page 1 (Element) widgets are registered dynamically via rebuild_element_tab_widgets()
-
- (*self_ptr).paginator.add_widget_to_page(2, &mut (*self_ptr).btn_toggle_grid as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(2, &mut (*self_ptr).btn_clear_canvas as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(2, &mut (*self_ptr).btn_add_text as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(2, &mut (*self_ptr).btn_add_rect as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(2, &mut (*self_ptr).btn_add_banner as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(2, &mut (*self_ptr).label_grid_snap as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(2, &mut (*self_ptr).label_total_elements as *mut (dyn Widget + 'static));
-
- (*self_ptr).paginator.add_widget_to_page(3, &mut (*self_ptr).slider_zoom as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(3, &mut (*self_ptr).section_rulers as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(3, &mut (*self_ptr).toggle_rulers as *mut (dyn Widget + 'static));
- (*self_ptr).paginator.add_widget_to_page(3, &mut (*self_ptr).dropdown_units as *mut (dyn Widget + 'static));
- }
- self.widgets_registered = true;
- }
-
self.width = size.width as u32;
self.height = size.height as u32;
self.scale_factor = scale;
- // Layout MenuBar at the top
- self.menu_bar.set_rect(0.0, 0.0, size.width, 26.0);
-
// Set paginator bounds in the sidebar region
- self.paginator.set_rect(0.0, 26.0, 280.0, size.height - 26.0);
+ clear_ui::scale::set_scale_factor(scale as f32);
+ self.paginator.set_rect(0.0, 0.0, 280.0, size.height as f32);
+ self.rebuild_layers_tab_widgets();
self.rebuild_text_items();
self.needs_rebuild = false;
}
- // 1. Render Paginator Sidebar (includes backgrounds and active tab sliding container)
- quads.extend(self.paginator.extra_quads());
- if let Some(hq) = self.paginator.highlight_quad() {
- quads.push(hq);
- }
-
- // Divider between Sidebar and Canvas
- quads.push((280.0, 26.0, 1.0, self.height as f32 - 26.0, [0.20, 0.20, 0.25, 1.0]));
+
// 3. Render Canvas & centered paper sheet
let canvas_w = self.width as f32 - 280.0;
- let canvas_h = self.height as f32 - 26.0;
+ let canvas_h = self.height as f32;
let zoom = self.render_zoom();
// Centered Paper Position
let page_x = 280.0 + (canvas_w - self.page_w * zoom) / 2.0 + self.pan_x;
- let page_y = 26.0 + (canvas_h - self.page_h * zoom) / 2.0 + self.pan_y;
+ let page_y = (canvas_h - self.page_h * zoom) / 2.0 + self.pan_y;
// Dark slate canvas backdrop
- quads.push((280.0, 26.0, canvas_w, canvas_h, [0.12, 0.12, 0.15, 1.0]));
+ let mut canvas_bg = [0.12, 0.12, 0.15, 1.0];
+ if let Some(opacity) = clear_ui::color::read_opacity_if_configured() {
+ canvas_bg[3] = opacity;
+ }
+ quads.push((280.0, 0.0, canvas_w, canvas_h, canvas_bg));
// Axis Rulers
if self.toggle_rulers.toggled() {
@@ -1201,23 +2592,24 @@ impl Application for LayoutApp {
let mw = (self.page_w - 2.0 * self.margin_x) * zoom;
let mh = (self.page_h - 2.0 * self.margin_y) * zoom;
if mw > 0.0 && mh > 0.0 {
- let margin_col = [0.20, 0.50, 0.85, 0.35]; // Translucent blue margin line
- quads.push((mx, my, mw, 1.0, margin_col));
- quads.push((mx, my + mh - 1.0, mw, 1.0, margin_col));
- quads.push((mx, my, 1.0, mh, margin_col));
- quads.push((mx + mw - 1.0, my, 1.0, mh, margin_col));
+ let margin_col = self.margin_color;
+ let thick = self.margin_thickness;
+ quads.push((mx, my, mw, thick, margin_col));
+ quads.push((mx, my + mh - thick, mw, thick, margin_col));
+ quads.push((mx, my, thick, mh, margin_col));
+ quads.push((mx + mw - thick, my, thick, mh, margin_col));
}
}
// 4. Snapping Grid inside the paper bounds
if self.grid_enabled {
- let spacing = 20.0 * zoom;
- let dot_color = [0.20, 0.35, 0.60, 0.22];
+ let spacing = self.grid_spacing() * zoom;
let mut cx = spacing;
+ let dot_size = (self.grid_size * zoom).max(1.0);
while cx < self.page_w * zoom {
let mut cy = spacing;
while cy < self.page_h * zoom {
- quads.push((page_x + cx, page_y + cy, 1.5, 1.5, dot_color));
+ quads.push((page_x + cx, page_y + cy, dot_size, dot_size, self.grid_color));
cy += spacing;
}
cx += spacing;
@@ -1267,6 +2659,7 @@ impl Application for LayoutApp {
}
}
}
+ Element::Vector { .. } => {}
}
// Draw highlight border if selected
@@ -1274,6 +2667,13 @@ impl Application for LayoutApp {
let (ex, ey, ew, eh) = match element {
Element::Text { x, y, w, h, .. } => (*x, *y, *w, *h),
Element::Shape { x, y, w, h, .. } => (*x, *y, *w, *h),
+ Element::Vector { x1, y1, x2, y2, .. } => {
+ let x = x1.min(*x2);
+ let y = y1.min(*y2);
+ let w = (x1 - x2).abs();
+ let h = (y1 - y2).abs();
+ (x, y, w, h)
+ }
};
let sel_col = [0.15, 0.45, 0.90, 1.0]; // Bright neon blue
quads.push((page_x + ex * zoom - 1.0, page_y + ey * zoom - 1.0, ew * zoom + 2.0, 1.0, sel_col));
@@ -1299,26 +2699,37 @@ impl Application for LayoutApp {
}
}
- // Render Paginator active page popovers (e.g. presets and units dropdowns)
- // This is done near the end of view() to ensure popovers overlap canvas elements
- let mut collector = clear_ui::layout::PopoverCollector::new();
- self.paginator.render_popover(&mut collector);
- for (color, x, y, w, h) in collector.rects {
- quads.push((x, y, w, h, color));
+ // Render Paginator Sidebar (includes backgrounds and active tab sliding container)
+ quads.extend(self.paginator.extra_quads());
+ if let Some(hq) = self.paginator.highlight_quad() {
+ quads.push(hq);
}
+ quads.extend(self.sidebar_quads.iter().cloned());
- // 6. MenuBar Background
- let mb_color = self.menu_bar.color();
- let (mb_x, mb_y, mb_w, mb_h) = self.menu_bar.rect();
- quads.push((mb_x, mb_y, mb_w, mb_h, mb_color));
+ // Divider between Sidebar and Canvas
+ quads.push((280.0, 0.0, 1.0, self.height as f32, [0.20, 0.20, 0.25, 1.0]));
+ }
- // Header Highlights for Open Menus
- for menu in &self.menu_bar.menus {
- if let Some(hq) = menu.highlight_quad() {
- quads.push(hq);
+ fn view_vectors(&mut self, vectors: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4], LineCap)>, _size: LogicalSize, _scale: f64) {
+ if self.word_processor_enabled {
+ return;
+ }
+ let zoom = self.render_zoom();
+ let canvas_w = self.width as f32 - 280.0;
+ let canvas_h = self.height as f32;
+ let page_x = 280.0 + (canvas_w - self.page_w * zoom) / 2.0 + self.pan_x;
+ let page_y = (canvas_h - self.page_h * zoom) / 2.0 + self.pan_y;
+
+ for element in &self.elements {
+ if let Element::Vector { x1, y1, x2, y2, stroke_width, color, line_cap } = element {
+ let vx1 = page_x + *x1 * zoom;
+ let vy1 = page_y + *y1 * zoom;
+ let vx2 = page_x + *x2 * zoom;
+ let vy2 = page_y + *y2 * zoom;
+ let vthickness = *stroke_width * zoom;
+ vectors.push((vx1, vy1, vx2, vy2, vthickness, *color, *line_cap));
}
}
- quads.extend(self.menu_bar.extra_quads());
}
fn text_items(&self) -> &[TextItem] {
@@ -1330,58 +2741,67 @@ impl Application for LayoutApp {
let px = pos.x as f32;
let py = pos.y as f32;
- if self.menu_bar.is_menu_open() || self.menu_bar.hit_test(px, py) {
- if self.menu_bar.on_cursor_moved(px, py) {
- changed = true;
+ let sidebar_w = 280.0;
+ if px < sidebar_w {
+ if self.paginator.cursor_moved(px, py) { changed = true; }
+ for w in self.active_page_widgets() {
+ if w.cursor_moved(px, py) { changed = true; }
+ }
+ if self.paginator.selected_page() == 2 && changed {
+ self.apply_sidebar_changes();
}
} else {
- let sidebar_w = 280.0;
- if px < sidebar_w {
- if self.paginator.on_cursor_moved(px, py) { changed = true; }
- if self.paginator.selected_page() == 1 && changed {
- self.apply_sidebar_changes();
+ // Clear hover state on sidebar widgets if mouse moves to canvas
+ if self.paginator.cursor_moved(px, py) { changed = true; }
+ for w in self.active_page_widgets() {
+ if w.cursor_moved(px, py) { changed = true; }
+ }
+
+ // Compute centering coordinates for canvas elements
+ let canvas_w = self.width as f32 - 280.0;
+ let canvas_h = self.height as f32;
+ let zoom = self.render_zoom();
+ let page_w = self.page_w * zoom;
+ let page_h = self.page_h * zoom;
+ let page_x = 280.0 + (canvas_w - page_w) / 2.0 + self.pan_x;
+ let page_y = (canvas_h - page_h) / 2.0 + self.pan_y;
+
+ let cx = (px - page_x) / zoom;
+ let cy = (py - page_y) / zoom;
+
+ if self.word_processor_enabled {
+ if self.wp_text_box.on_cursor_moved(px, py) {
+ changed = true;
}
- } else {
- // Compute centering coordinates for canvas elements
- let canvas_w = self.width as f32 - 280.0;
- let canvas_h = self.height as f32 - 26.0;
- let zoom = self.render_zoom();
- let page_w = self.page_w * zoom;
- let page_h = self.page_h * zoom;
- let page_x = 280.0 + (canvas_w - page_w) / 2.0 + self.pan_x;
- let page_y = 26.0 + (canvas_h - page_h) / 2.0 + self.pan_y;
+ } else if let Some((idx, ox, oy)) = self.dragging {
+ let mut new_x = cx - ox;
+ let mut new_y = cy - oy;
- let cx = (px - page_x) / zoom;
- let cy = (py - page_y) / zoom;
-
- if self.word_processor_enabled {
- if self.wp_text_box.on_cursor_moved(px, py) {
- changed = true;
+ if self.grid_enabled {
+ let spacing = self.grid_spacing();
+ new_x = (new_x / spacing).round() * spacing;
+ new_y = (new_y / spacing).round() * spacing;
+ }
+
+ match &mut self.elements[idx] {
+ Element::Text { x, y, .. } => {
+ *x = new_x;
+ *y = new_y;
}
- } else if let Some((idx, ox, oy)) = self.dragging {
- let mut new_x = cx - ox;
- let mut new_y = cy - oy;
-
- if self.grid_enabled {
- new_x = (new_x / 20.0).round() * 20.0;
- new_y = (new_y / 20.0).round() * 20.0;
+ Element::Shape { x, y, .. } => {
+ *x = new_x;
+ *y = new_y;
}
-
- match &mut self.elements[idx] {
- Element::Text { x, y, .. } => {
- *x = new_x;
- *y = new_y;
- }
- Element::Shape { x, y, .. } => {
- *x = new_x;
- *y = new_y;
- }
+ Element::Vector { x1, y1, x2, y2, .. } => {
+ let dx = new_x - *x1;
+ let dy = new_y - *y1;
+ *x1 = new_x;
+ *y1 = new_y;
+ *x2 += dx;
+ *y2 += dy;
}
- self.sync_sidebar_fields();
- changed = true;
}
- }
- if self.menu_bar.on_cursor_moved(px, py) {
+ self.sync_sidebar_fields();
changed = true;
}
}
@@ -1398,130 +2818,206 @@ impl Application for LayoutApp {
let px = pos.x as f32;
let py = pos.y as f32;
- if self.menu_bar.is_menu_open() || self.menu_bar.hit_test(px, py) {
- if self.menu_bar.mouse_input(button, state, px, py) {
+ let sidebar_w = 280.0;
+ if px < sidebar_w {
+ let old_page = self.paginator.selected_page();
+ let pag_mouse = self.paginator.mouse_input(button, state, px, py);
+ if pag_mouse {
changed = true;
- if let Some((menu_idx, item_idx)) = self.menu_bar.menu_click() {
- match (menu_idx, item_idx) {
- (0, 0) => msg_out = Some(AppMessage::NewDocument),
- (0, 1) => msg_out = Some(AppMessage::Exit),
- (1, 0) => msg_out = Some(AppMessage::AddText),
- (1, 1) => msg_out = Some(AppMessage::AddRectangle),
- (1, 2) => msg_out = Some(AppMessage::AddBanner),
- (2, 0) => msg_out = Some(AppMessage::ToggleGrid),
- _ => {}
+ if self.paginator.take_click() {
+ let new_page = self.paginator.selected_page();
+ self.paginator.set_selected_page(old_page);
+ for w in self.active_page_widgets() {
+ w.unfocus();
}
+ self.apply_sidebar_changes();
+ self.paginator.set_selected_page(new_page);
}
- }
- if state == ElementState::Pressed && !self.menu_bar.hit_test(px, py) {
- self.menu_bar.unfocus();
- changed = true;
- }
- } else {
- let sidebar_w = 280.0;
- if px < sidebar_w {
- if self.paginator.mouse_input(button, state, px, py) {
- changed = true;
- if self.paginator.take_click() {
- self.dropdown_presets.unfocus();
- self.slider_page_x.unfocus();
- self.slider_page_y.unfocus();
- self.dropdown_units.unfocus();
- self.page_color_selector.unfocus();
- self.toggle_margin.unfocus();
- self.toggle_word_processor.unfocus();
- self.dropdown_margin_units.unfocus();
- self.slider_margin_x.unfocus();
- self.slider_margin_y.unfocus();
- self.sidebar_x.unfocus();
- self.sidebar_y.unfocus();
- self.sidebar_w.unfocus();
- self.sidebar_h.unfocus();
- self.sidebar_text.unfocus();
- self.sidebar_size.unfocus();
- } else {
- let active_page = self.paginator.selected_page();
- if active_page == 0 {
- let _ = self.toggle_margin.take_click();
- let _ = self.toggle_word_processor.take_click();
- } else if active_page == 1 {
- self.apply_sidebar_changes();
- } else if active_page == 2 {
- if state == ElementState::Released {
- if self.btn_toggle_grid.take_click() {
- msg_out = Some(AppMessage::ToggleGrid);
- }
- if self.btn_clear_canvas.take_click() {
- msg_out = Some(AppMessage::NewDocument);
- }
- if self.btn_add_text.take_click() {
- msg_out = Some(AppMessage::AddText);
- }
- if self.btn_add_rect.take_click() {
- msg_out = Some(AppMessage::AddRectangle);
- }
- if self.btn_add_banner.take_click() {
- msg_out = Some(AppMessage::AddBanner);
- }
+ } else {
+ if state == ElementState::Pressed {
+ let mut hit_any = false;
+ let mut widgets = self.active_page_widgets();
+ let mut focus_idx = None;
+ for (idx, w) in widgets.iter().enumerate() {
+ if w.hit_test(px, py) {
+ focus_idx = Some(idx);
+ hit_any = true;
+ break;
+ }
+ }
+ if hit_any {
+ for (idx, w) in widgets.iter_mut().enumerate() {
+ if Some(idx) == focus_idx {
+ w.focus();
+ } else {
+ w.unfocus();
}
- } else if active_page == 3 {
- let _ = self.toggle_rulers.take_click();
+ }
+ } else {
+ for w in widgets.iter_mut() {
+ w.unfocus();
}
}
}
- } else {
- // Compute centering coordinates for canvas elements
- let canvas_w = self.width as f32 - 280.0;
- let canvas_h = self.height as f32 - 26.0;
- let zoom = self.render_zoom();
- let page_w = self.page_w * zoom;
- let page_h = self.page_h * zoom;
- let page_x = 280.0 + (canvas_w - page_w) / 2.0 + self.pan_x;
- let page_y = 26.0 + (canvas_h - page_h) / 2.0 + self.pan_y;
-
- let cx = (px - page_x) / zoom;
- let cy = (py - page_y) / zoom;
- if self.word_processor_enabled {
- if self.wp_text_box.mouse_input(button, state, px, py) {
- changed = true;
- } else if state == ElementState::Pressed {
- self.wp_text_box.unfocus();
+ for w in self.active_page_widgets() {
+ if w.mouse_input(button, state, px, py) {
changed = true;
}
- } else if state == ElementState::Pressed {
- let mut found = None;
- for (idx, element) in self.elements.iter().enumerate().rev() {
- let (ex, ey, ew, eh) = match element {
- Element::Text { x, y, w, h, .. } => (*x, *y, *w, *h),
- Element::Shape { x, y, w, h, .. } => (*x, *y, *w, *h),
- };
- if cx >= ex && cx <= ex + ew && cy >= ey && cy <= ey + eh {
- found = Some(idx);
- break;
+ }
+
+ if self.paginator.selected_page() == 2 {
+ self.apply_sidebar_changes();
+ }
+
+ let active_page = self.paginator.selected_page();
+ if active_page == 0 {
+ if state == ElementState::Released {
+ if self.btn_new_doc.take_click() {
+ msg_out = Some(AppMessage::NewDocument);
+ }
+ if self.btn_open.take_click() {
+ msg_out = Some(AppMessage::Open);
+ }
+ let mut clicked_file = None;
+ for (i, btn) in self.recent_files_buttons.iter_mut().enumerate() {
+ if btn.take_click() {
+ clicked_file = Some(self.recent_files[i].clone());
+ }
+ }
+ if let Some(path) = clicked_file {
+ self.selected_idx = None;
+ self.dragging = None;
+ self.pan_x = 0.0;
+ self.pan_y = 0.0;
+ if let Err(e) = self.load_document(&path) {
+ eprintln!("ERROR: Failed to load document: {}", e);
+ } else {
+ println!("DEBUG: Successfully loaded layout from {:?}", path);
+ self.current_file_path = Some(path.clone());
+ self.last_saved_document = self.get_current_document();
+ self.add_recent_file(path);
+ }
+ }
+ if self.btn_save.take_click() {
+ msg_out = Some(AppMessage::Save);
+ }
+ if self.btn_save_as.take_click() {
+ msg_out = Some(AppMessage::SaveAs);
+ }
+ if self.btn_exit.take_click() {
+ msg_out = Some(AppMessage::Exit);
+ }
+ }
+ } else if active_page == 1 {
+ let _ = self.toggle_margin.take_click();
+ let _ = self.toggle_word_processor.take_click();
+
+ } else if active_page == 3 {
+ if state == ElementState::Released {
+ if self.btn_add_text.take_click() {
+ msg_out = Some(AppMessage::AddText);
+ }
+ if self.btn_add_rect.take_click() {
+ msg_out = Some(AppMessage::AddRectangle);
+ }
+ if self.btn_add_banner.take_click() {
+ msg_out = Some(AppMessage::AddBanner);
+ }
+ if self.btn_add_vector.take_click() {
+ msg_out = Some(AppMessage::AddVector);
+ }
+ }
+ } else if active_page == 4 {
+ let _ = self.toggle_grid.take_click();
+ let _ = self.grid_color_selector.take_click();
+ let _ = self.label_grid_snap.take_click();
+ let _ = self.toggle_rulers.take_click();
+ let _ = self.margin_color_selector.take_click();
+ } else if active_page == 5 {
+ if state == ElementState::Released {
+ let mut clicked_idx = None;
+ for (idx, btn) in self.layer_buttons.iter_mut().enumerate() {
+ if btn.take_click() {
+ clicked_idx = Some(idx);
+ }
+ }
+ if let Some(idx) = clicked_idx {
+ self.selected_idx = Some(idx);
+ self.sync_sidebar_fields();
+ self.rebuild_layers_tab_widgets();
+ changed = true;
}
}
+ }
+ }
+ } else {
+ // Compute centering coordinates for canvas elements
+ let canvas_w = self.width as f32 - 280.0;
+ let canvas_h = self.height as f32;
+ let zoom = self.render_zoom();
+ let page_w = self.page_w * zoom;
+ let page_h = self.page_h * zoom;
+ let page_x = 280.0 + (canvas_w - page_w) / 2.0 + self.pan_x;
+ let page_y = (canvas_h - page_h) / 2.0 + self.pan_y;
+
+ let cx = (px - page_x) / zoom;
+ let cy = (py - page_y) / zoom;
- if let Some(idx) = found {
- self.selected_idx = Some(idx);
- let (ex, ey) = match &self.elements[idx] {
- Element::Text { x, y, .. } => (*x, *y),
- Element::Shape { x, y, .. } => (*x, *y),
- };
- self.dragging = Some((idx, cx - ex, cy - ey));
- self.rebuild_element_tab_widgets();
- self.sync_sidebar_fields();
- changed = true;
- } else {
- self.selected_idx = None;
- self.dragging = None;
- self.rebuild_element_tab_widgets();
- self.sync_sidebar_fields();
- changed = true;
+ if self.word_processor_enabled {
+ if self.wp_text_box.mouse_input(button, state, px, py) {
+ changed = true;
+ } else if state == ElementState::Pressed {
+ self.wp_text_box.unfocus();
+ changed = true;
+ }
+ } else if state == ElementState::Pressed {
+ let mut found = None;
+ for (idx, element) in self.elements.iter().enumerate().rev() {
+ match element {
+ Element::Text { x, y, w, h, .. } => {
+ if cx >= *x && cx <= *x + *w && cy >= *y && cy <= *y + *h {
+ found = Some(idx);
+ break;
+ }
+ }
+ Element::Shape { x, y, w, h, .. } => {
+ if cx >= *x && cx <= *x + *w && cy >= *y && cy <= *y + *h {
+ found = Some(idx);
+ break;
+ }
+ }
+ Element::Vector { x1, y1, x2, y2, stroke_width, .. } => {
+ let dist = point_to_line_segment_distance(cx, cy, *x1, *y1, *x2, *y2);
+ let select_tolerance = (stroke_width * 1.5).max(10.0);
+ if dist <= select_tolerance {
+ found = Some(idx);
+ break;
+ }
+ }
}
- } else if state == ElementState::Released {
+ }
+
+ if let Some(idx) = found {
+ self.selected_idx = Some(idx);
+ let (ex, ey) = match &self.elements[idx] {
+ Element::Text { x, y, .. } => (*x, *y),
+ Element::Shape { x, y, .. } => (*x, *y),
+ Element::Vector { x1, y1, .. } => (*x1, *y1),
+ };
+ self.dragging = Some((idx, cx - ex, cy - ey));
+ self.rebuild_layers_tab_widgets();
+ self.sync_sidebar_fields();
+ changed = true;
+ } else {
+ self.selected_idx = None;
self.dragging = None;
+ self.rebuild_layers_tab_widgets();
+ self.sync_sidebar_fields();
+ changed = true;
}
+ } else if state == ElementState::Released {
+ self.dragging = None;
}
}
@@ -1537,8 +3033,17 @@ impl Application for LayoutApp {
let px = pos.x as f32;
let py = pos.y as f32;
if px < 280.0 {
+ let mut wheel_handled = false;
+ for w in self.active_page_widgets() {
+ if w.mouse_wheel(delta, px, py) {
+ wheel_handled = true;
+ }
+ }
if self.paginator.mouse_wheel(delta, px, py) {
- if self.paginator.selected_page() == 1 {
+ wheel_handled = true;
+ }
+ if wheel_handled {
+ if self.paginator.selected_page() == 2 {
self.apply_sidebar_changes();
}
*needs_rebuild = true;
@@ -1557,6 +3062,16 @@ impl Application for LayoutApp {
}
fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
+ if event.state == ElementState::Pressed && event.ctrl {
+ if let Key::Character(ref s) = event.logical_key {
+ if s == "s" || s == "S" || s == "\u{13}" || s == "\x13" {
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ return Some(AppMessage::Save);
+ }
+ }
+ }
+
let mut handled = false;
if self.word_processor_enabled {
@@ -1565,14 +3080,37 @@ impl Application for LayoutApp {
}
}
+ for w in self.active_page_widgets() {
+ if w.focused() {
+ if w.keyboard_input(event) {
+ handled = true;
+ }
+ }
+ }
+
if self.paginator.keyboard_input(event) {
handled = true;
let active_page = self.paginator.selected_page();
- if active_page == 1 {
+ if active_page == 2 {
self.apply_sidebar_changes();
}
}
+ if !handled && !self.word_processor_enabled && event.state == ElementState::Pressed {
+ if let Key::Named(NamedKey::Delete) = event.logical_key {
+ if let Some(idx) = self.selected_idx {
+ if idx < self.elements.len() {
+ self.elements.remove(idx);
+ self.selected_idx = None;
+ self.dragging = None;
+ self.rebuild_layers_tab_widgets();
+ self.sync_sidebar_fields();
+ handled = true;
+ }
+ }
+ }
+ }
+
if handled {
*needs_rebuild = true;
self.needs_rebuild = true;