2026-03-22 22:44:32 +03:00
|
|
|
|
use super::{H7CAD, Message};
|
|
|
|
|
|
use super::document::DocumentTab;
|
|
|
|
|
|
use super::history::history_dropdown_labels;
|
|
|
|
|
|
use super::helpers::grid_plane_from_camera;
|
|
|
|
|
|
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
2026-05-01 12:16:46 +03:00
|
|
|
|
use crate::scene::grip::{grips_to_screen, grips_to_screen_paper};
|
2026-04-23 09:48:53 +03:00
|
|
|
|
use crate::scene::paper_canvas::PaperCanvas;
|
2026-04-23 03:19:15 +03:00
|
|
|
|
use crate::scene::viewport_pane::{PaperViewportPane, ViewportPane};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
use crate::ui::overlay;
|
2026-05-01 16:12:32 +03:00
|
|
|
|
use iced::widget::{button, canvas, column, container, mouse_area, pick_list, row, shader, stack, text, text_input, Row, Space};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
use iced::window;
|
|
|
|
|
|
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
|
|
|
|
|
|
|
|
|
|
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
|
|
|
|
|
|
|
|
|
|
|
|
impl H7CAD {
|
|
|
|
|
|
pub fn view(&self, window_id: window::Id) -> Element<'_, Message> {
|
2026-04-08 22:36:28 +03:00
|
|
|
|
// ── Floating panel windows ─────────────────────────────────────────
|
2026-03-22 22:44:32 +03:00
|
|
|
|
if Some(window_id) == self.layer_window {
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
|
return tab.layers.view_window();
|
|
|
|
|
|
}
|
2026-04-08 22:36:28 +03:00
|
|
|
|
if Some(window_id) == self.page_setup_window {
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::page_setup::view_window(
|
2026-04-08 22:36:28 +03:00
|
|
|
|
&self.page_setup_w, &self.page_setup_h,
|
|
|
|
|
|
&self.page_setup_plot_area, self.page_setup_center,
|
|
|
|
|
|
&self.page_setup_offset_x, &self.page_setup_offset_y,
|
|
|
|
|
|
&self.page_setup_rotation, &self.page_setup_scale,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.textstyle_window {
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
|
let styles: Vec<String> = tab.scene.document.text_styles
|
|
|
|
|
|
.iter().map(|s| s.name.clone()).collect();
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::textstyle::view_window(styles, &self.textstyle_selected,
|
2026-04-08 22:36:28 +03:00
|
|
|
|
&self.textstyle_font, &self.textstyle_width, &self.textstyle_oblique);
|
|
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.tablestyle_window {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
|
let styles: Vec<String> = tab.scene.document.objects.values()
|
|
|
|
|
|
.filter_map(|o| if let ObjectType::TableStyle(s) = o { Some(s.name.clone()) } else { None })
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let selected_style = tab.scene.document.objects.values()
|
|
|
|
|
|
.find_map(|o| if let ObjectType::TableStyle(s) = o {
|
|
|
|
|
|
if s.name == self.tablestyle_selected { Some(s) } else { None }
|
|
|
|
|
|
} else { None });
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::tablestyle::view_window(styles, &self.tablestyle_selected, selected_style);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.mlstyle_window {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
|
let styles: Vec<String> = tab.scene.document.objects.values()
|
|
|
|
|
|
.filter_map(|o| if let ObjectType::MLineStyle(s) = o { Some(s.name.clone()) } else { None })
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let selected_style = tab.scene.document.objects.values()
|
|
|
|
|
|
.find_map(|o| if let ObjectType::MLineStyle(s) = o {
|
|
|
|
|
|
if s.name == self.mlstyle_selected { Some(s) } else { None }
|
|
|
|
|
|
} else { None });
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::mlstyle::view_window(styles, &self.mlstyle_selected, selected_style,
|
2026-04-08 22:36:28 +03:00
|
|
|
|
tab.scene.document.header.multiline_style.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.layout_manager_window {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let layouts = self.tabs[i].scene.layout_names();
|
|
|
|
|
|
let current = self.tabs[i].scene.current_layout.clone();
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::layout_manager::view_window(layouts, &self.layout_manager_selected,
|
2026-04-08 22:36:28 +03:00
|
|
|
|
&self.layout_manager_rename_buf, current);
|
|
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.plotstyle_window {
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::plotstyle::view_window(
|
2026-04-08 22:36:28 +03:00
|
|
|
|
self.active_plot_style.as_ref(), self.plotstyle_panel_aci,
|
|
|
|
|
|
&self.ps_color_buf, &self.ps_lineweight_buf, &self.ps_screening_buf,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.dimstyle_window {
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
|
let styles: Vec<String> = tab.scene.document.dim_styles
|
|
|
|
|
|
.iter().map(|s| s.name.clone()).collect();
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::dimstyle::view_window(
|
|
|
|
|
|
styles, &self.dimstyle_selected, self.dimstyle_tab,
|
|
|
|
|
|
crate::ui::dimstyle::DimStyleValues {
|
|
|
|
|
|
dimdle: &self.ds_dimdle, dimdli: &self.ds_dimdli, dimgap: &self.ds_dimgap,
|
|
|
|
|
|
dimexe: &self.ds_dimexe, dimexo: &self.ds_dimexo,
|
|
|
|
|
|
dimsd1: self.ds_dimsd1, dimsd2: self.ds_dimsd2,
|
|
|
|
|
|
dimse1: self.ds_dimse1, dimse2: self.ds_dimse2,
|
|
|
|
|
|
dimasz: &self.ds_dimasz, dimcen: &self.ds_dimcen, dimtsz: &self.ds_dimtsz,
|
|
|
|
|
|
dimtxt: &self.ds_dimtxt, dimtxsty: &self.ds_dimtxsty, dimtad: &self.ds_dimtad,
|
|
|
|
|
|
dimtih: self.ds_dimtih, dimtoh: self.ds_dimtoh,
|
|
|
|
|
|
dimscale: &self.ds_dimscale, dimlfac: &self.ds_dimlfac,
|
|
|
|
|
|
dimlunit: &self.ds_dimlunit, dimdec: &self.ds_dimdec, dimpost: &self.ds_dimpost,
|
|
|
|
|
|
dimtol: self.ds_dimtol, dimlim: self.ds_dimlim,
|
|
|
|
|
|
dimtp: &self.ds_dimtp, dimtm: &self.ds_dimtm,
|
|
|
|
|
|
dimtdec: &self.ds_dimtdec, dimtfac: &self.ds_dimtfac,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.shortcuts_window {
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::shortcuts::view_window(&self.shortcut_overrides);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
2026-04-22 22:22:33 +03:00
|
|
|
|
if Some(window_id) == self.about_window {
|
|
|
|
|
|
return crate::ui::about::view_window();
|
|
|
|
|
|
}
|
2026-05-01 11:31:11 +03:00
|
|
|
|
if Some(window_id) == self.unsaved_dialog_window {
|
|
|
|
|
|
let tab_name = match &self.pending_close {
|
|
|
|
|
|
Some(super::PendingClose::Tab(idx)) => self
|
|
|
|
|
|
.tabs.get(*idx)
|
|
|
|
|
|
.map(|t| t.tab_display_name())
|
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
|
Some(super::PendingClose::Quit) => self
|
|
|
|
|
|
.tabs.iter().find(|t| t.dirty)
|
|
|
|
|
|
.map(|t| t.tab_display_name())
|
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
|
None => String::new(),
|
|
|
|
|
|
};
|
|
|
|
|
|
return unsaved_changes_dialog_window(&tab_name);
|
|
|
|
|
|
}
|
2026-05-01 16:12:32 +03:00
|
|
|
|
if Some(window_id) == self.save_dialog_window {
|
|
|
|
|
|
return save_as_dialog_window(
|
|
|
|
|
|
&self.save_dialog_filename,
|
|
|
|
|
|
&self.save_dialog_folder,
|
2026-05-01 16:20:38 +03:00
|
|
|
|
&self.save_dialog_entries,
|
2026-05-01 16:12:32 +03:00
|
|
|
|
&self.save_dialog_format,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let tab = &self.tabs[i];
|
|
|
|
|
|
let is_paper = tab.scene.current_layout != "Model";
|
2026-04-23 02:18:41 +03:00
|
|
|
|
let viewport_3d: Element<'_, Message> = if is_paper {
|
|
|
|
|
|
paper_canvas_view(tab)
|
|
|
|
|
|
} else {
|
2026-04-24 01:31:52 +03:00
|
|
|
|
shader(ViewportPane::model(&tab.scene, self.show_viewcube))
|
2026-04-23 02:18:41 +03:00
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
let selection_overlay = {
|
|
|
|
|
|
let sel = tab.scene.selection.borrow().clone();
|
|
|
|
|
|
let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type));
|
|
|
|
|
|
|
|
|
|
|
|
let grips: Vec<overlay::GripMarker> =
|
|
|
|
|
|
if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() {
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
let bounds = iced::Rectangle {
|
|
|
|
|
|
x: 0.0, y: 0.0, width: vw, height: vh,
|
|
|
|
|
|
};
|
|
|
|
|
|
let sel_h = tab.selected_handle;
|
2026-05-01 12:16:46 +03:00
|
|
|
|
let screen_grips = if is_paper {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
let aspect = if vh > 0.0 { vw / vh } else { 1.0 };
|
|
|
|
|
|
let half_h = cam.ortho_size();
|
|
|
|
|
|
let half_w = half_h * aspect;
|
|
|
|
|
|
let tx = cam.target.x;
|
|
|
|
|
|
let ty = cam.target.y;
|
|
|
|
|
|
drop(cam);
|
|
|
|
|
|
grips_to_screen_paper(&tab.selected_grips, tx, ty, half_w, half_h, bounds)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let vp_mat = tab.scene.camera.borrow().view_proj(bounds);
|
|
|
|
|
|
grips_to_screen(&tab.selected_grips, vp_mat, bounds)
|
|
|
|
|
|
};
|
|
|
|
|
|
screen_grips
|
2026-03-22 22:44:32 +03:00
|
|
|
|
.into_iter()
|
|
|
|
|
|
.filter(|(_, screen, _, _)| {
|
|
|
|
|
|
screen.x.is_finite()
|
|
|
|
|
|
&& screen.y.is_finite()
|
|
|
|
|
|
&& screen.x >= -bounds.width
|
|
|
|
|
|
&& screen.x <= bounds.width * 2.0
|
|
|
|
|
|
&& screen.y >= -bounds.height
|
|
|
|
|
|
&& screen.y <= bounds.height * 2.0
|
|
|
|
|
|
})
|
|
|
|
|
|
.map(|(grip_id, screen, _is_midpoint, shape)| {
|
|
|
|
|
|
let is_hot = tab
|
|
|
|
|
|
.active_grip
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id);
|
|
|
|
|
|
overlay::GripMarker { pos: screen, shape, is_hot }
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-03 19:25:55 +03:00
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
let vp_bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let grid = if self.show_grid {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
|
|
|
|
|
Some(overlay::GridParams {
|
2026-04-03 19:25:55 +03:00
|
|
|
|
view_proj: cam.view_proj(vp_bounds),
|
|
|
|
|
|
bounds: vp_bounds,
|
2026-03-22 22:44:32 +03:00
|
|
|
|
plane,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
2026-04-03 19:25:55 +03:00
|
|
|
|
|
|
|
|
|
|
let ucs_icon = if self.show_ucs_icon && !is_paper {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
Some(overlay::UcsIconParams {
|
|
|
|
|
|
view_proj: cam.view_proj(vp_bounds),
|
|
|
|
|
|
bounds: vp_bounds,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-08 00:26:54 +03:00
|
|
|
|
// OST tracking points → screen positions.
|
|
|
|
|
|
let ost_points: Vec<overlay::OstTrackPoint> = if self.snapper.otrack_enabled {
|
|
|
|
|
|
let vp_mat = tab.scene.camera.borrow().view_proj(vp_bounds);
|
|
|
|
|
|
self.snapper.tracking_points.iter().map(|&wp| {
|
|
|
|
|
|
let ndc = vp_mat.project_point3(wp);
|
|
|
|
|
|
overlay::OstTrackPoint {
|
|
|
|
|
|
screen: iced::Point::new(
|
|
|
|
|
|
(ndc.x + 1.0) * 0.5 * vp_bounds.width,
|
|
|
|
|
|
(1.0 - ndc.y) * 0.5 * vp_bounds.height,
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
}).collect()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-24 01:23:06 +03:00
|
|
|
|
overlay::selection_overlay(sel, snap_info, grips, grid, ucs_icon, ost_points, tab.last_cursor_screen, !is_paper && self.show_viewcube)
|
2026-03-22 22:44:32 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let info = container(overlay::info_bar(
|
|
|
|
|
|
if is_paper { &tab.scene.current_layout } else { "Custom View" },
|
|
|
|
|
|
&tab.visual_style,
|
|
|
|
|
|
))
|
|
|
|
|
|
.padding([4, 6]);
|
|
|
|
|
|
|
|
|
|
|
|
let viewport_mouse = mouse_area(container(
|
|
|
|
|
|
iced::widget::Space::new().width(Fill).height(Fill),
|
|
|
|
|
|
))
|
|
|
|
|
|
.on_move(Message::ViewportMove)
|
|
|
|
|
|
.on_press(Message::ViewportLeftPress)
|
|
|
|
|
|
.on_release(Message::ViewportLeftRelease)
|
|
|
|
|
|
.on_right_press(Message::ViewportRightPress)
|
|
|
|
|
|
.on_right_release(Message::ViewportRightRelease)
|
|
|
|
|
|
.on_middle_press(Message::ViewportMiddlePress)
|
|
|
|
|
|
.on_middle_release(Message::ViewportMiddleRelease)
|
|
|
|
|
|
.on_scroll(Message::ViewportScroll)
|
|
|
|
|
|
.on_exit(Message::ViewportExit);
|
|
|
|
|
|
|
|
|
|
|
|
let bg_color = if is_paper {
|
2026-04-23 14:05:07 +03:00
|
|
|
|
// Desk color — matches the DESK constant in paper_canvas.rs.
|
|
|
|
|
|
Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0 }
|
2026-03-22 22:44:32 +03:00
|
|
|
|
} else {
|
2026-04-06 23:26:26 +03:00
|
|
|
|
tab.bg_color
|
|
|
|
|
|
.map(|[r, g, b, a]| Color { r, g, b, a })
|
|
|
|
|
|
.unwrap_or(Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0 })
|
2026-03-22 22:44:32 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-08 00:12:09 +03:00
|
|
|
|
// Dynamic input overlay — shown when a command is active and DYN is on.
|
|
|
|
|
|
let dyn_input_overlay: Option<Element<'_, Message>> =
|
|
|
|
|
|
if self.dyn_input && tab.active_cmd.is_some() {
|
|
|
|
|
|
let w = tab.last_cursor_world;
|
|
|
|
|
|
let label = if let Some(base) = self.last_point {
|
|
|
|
|
|
// Show relative distance + angle when we have a base point.
|
|
|
|
|
|
let dx = (w.x - base.x) as f64;
|
|
|
|
|
|
let dy = (w.z - base.z) as f64;
|
|
|
|
|
|
let dist = (dx * dx + dy * dy).sqrt();
|
|
|
|
|
|
let ang = dy.atan2(dx).to_degrees();
|
|
|
|
|
|
format!("d={:.3} <{:.1}°", dist, ang)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
format!("X:{:.3} Y:{:.3}", w.x, w.z)
|
|
|
|
|
|
};
|
|
|
|
|
|
Some(overlay::dynamic_input_overlay(tab.last_cursor_screen, label))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let mut viewport_stack = stack![
|
2026-03-22 22:44:32 +03:00
|
|
|
|
container(viewport_3d)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(bg_color)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
container(info).width(Fill).height(Fill),
|
|
|
|
|
|
selection_overlay,
|
|
|
|
|
|
viewport_mouse,
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
2026-04-24 01:23:06 +03:00
|
|
|
|
|
|
|
|
|
|
if self.show_navbar {
|
|
|
|
|
|
let nav = container(overlay::nav_toolbar())
|
|
|
|
|
|
.align_right(Fill)
|
|
|
|
|
|
.align_top(Fill)
|
|
|
|
|
|
.padding(iced::Padding { top: 148.0, right: 8.0, bottom: 0.0, left: 0.0 });
|
|
|
|
|
|
viewport_stack = viewport_stack.push(nav);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if self.show_viewcube && !is_paper {
|
|
|
|
|
|
let cube_click: Element<'_, Message> = container(
|
|
|
|
|
|
mouse_area(container(
|
|
|
|
|
|
iced::widget::Space::new()
|
|
|
|
|
|
.width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE))
|
|
|
|
|
|
.height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)),
|
|
|
|
|
|
))
|
|
|
|
|
|
.on_move(Message::CursorMoved)
|
|
|
|
|
|
.on_press(Message::ViewportClick),
|
|
|
|
|
|
)
|
|
|
|
|
|
.align_right(Fill)
|
|
|
|
|
|
.align_top(Fill)
|
|
|
|
|
|
.padding(iced::Padding { top: VIEWCUBE_PAD, right: VIEWCUBE_PAD, bottom: 0.0, left: 0.0 })
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into();
|
|
|
|
|
|
viewport_stack = viewport_stack.push(cube_click);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 00:12:09 +03:00
|
|
|
|
if let Some(dyn_ol) = dyn_input_overlay {
|
|
|
|
|
|
viewport_stack = viewport_stack.push(dyn_ol);
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
2026-04-24 01:23:06 +03:00
|
|
|
|
let properties_el: Element<'_, Message> = if self.show_properties {
|
|
|
|
|
|
tab.properties.view()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Space::new().into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let center_stack = iced::widget::stack![
|
2026-04-24 01:23:06 +03:00
|
|
|
|
row![properties_el, viewport_stack]
|
2026-03-22 22:44:32 +03:00
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
2026-04-24 01:23:06 +03:00
|
|
|
|
let main_ui = container({
|
|
|
|
|
|
let mut col = column![self.ribbon.view(
|
|
|
|
|
|
is_paper,
|
|
|
|
|
|
self.tabs[self.active_tab].history.undo_stack.len(),
|
|
|
|
|
|
self.tabs[self.active_tab].history.redo_stack.len(),
|
|
|
|
|
|
)];
|
|
|
|
|
|
if self.show_file_tabs {
|
|
|
|
|
|
col = col.push(doc_tab_bar(&self.tabs, self.active_tab));
|
|
|
|
|
|
}
|
|
|
|
|
|
col.push(center_stack)
|
|
|
|
|
|
.push(self.command_line.view())
|
|
|
|
|
|
.push(self.status_bar.view(
|
2026-03-22 22:44:32 +03:00
|
|
|
|
&self.snapper,
|
|
|
|
|
|
self.snap_popup_open,
|
|
|
|
|
|
self.ortho_mode,
|
|
|
|
|
|
self.polar_mode,
|
2026-04-07 19:30:25 +03:00
|
|
|
|
self.polar_increment_deg,
|
2026-03-22 22:44:32 +03:00
|
|
|
|
self.show_grid,
|
2026-04-08 00:12:09 +03:00
|
|
|
|
self.dyn_input,
|
2026-04-08 00:26:54 +03:00
|
|
|
|
self.snapper.otrack_enabled,
|
2026-03-22 22:44:32 +03:00
|
|
|
|
tab.scene.layout_names(),
|
2026-03-31 07:13:36 +03:00
|
|
|
|
tab.scene.current_layout.clone(),
|
|
|
|
|
|
self.layout_rename_state.as_ref(),
|
2026-03-31 07:17:45 +03:00
|
|
|
|
tab.scene.first_viewport_scale(),
|
2026-04-02 23:52:35 +03:00
|
|
|
|
tab.scene.viewport_count(),
|
2026-04-23 09:18:01 +03:00
|
|
|
|
tab.scene.active_viewport.is_some(),
|
2026-04-24 01:23:06 +03:00
|
|
|
|
self.show_layout_tabs,
|
|
|
|
|
|
))
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
})
|
2026-03-22 22:44:32 +03:00
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0 })),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
let snap_layer: Element<'_, Message> = if self.snap_popup_open {
|
|
|
|
|
|
crate::ui::snap_popup::snap_popup_overlay(&self.snapper, 4.0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let dropdown_layer: Element<'_, Message> = self
|
|
|
|
|
|
.ribbon
|
|
|
|
|
|
.dropdown_overlay(
|
|
|
|
|
|
&history_dropdown_labels(&self.tabs[self.active_tab].history.undo_stack),
|
|
|
|
|
|
&history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack),
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into());
|
|
|
|
|
|
|
2026-03-31 07:13:36 +03:00
|
|
|
|
let layout_ctx_layer: Element<'_, Message> =
|
|
|
|
|
|
if let Some(name) = &self.layout_context_menu {
|
|
|
|
|
|
layout_context_menu_overlay(name)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-08 00:41:01 +03:00
|
|
|
|
// ── Viewport right-click context menu ─────────────────────────────
|
|
|
|
|
|
let viewport_ctx_layer: Element<'_, Message> = {
|
|
|
|
|
|
let ctx_pos = tab.scene.selection.borrow().context_menu;
|
|
|
|
|
|
if let Some(p) = ctx_pos {
|
|
|
|
|
|
let has_cmd = tab.active_cmd.is_some();
|
|
|
|
|
|
let has_selection = !tab.scene.selected.is_empty();
|
|
|
|
|
|
let last_cmds: Vec<String> = self.command_line.cmd_recall
|
|
|
|
|
|
.iter().rev().take(3).cloned().collect();
|
|
|
|
|
|
viewport_context_menu_overlay(p, has_cmd, has_selection, last_cmds)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-08 22:36:28 +03:00
|
|
|
|
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, viewport_ctx_layer].into()
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn subscription(&self) -> Subscription<Message> {
|
|
|
|
|
|
use iced::event;
|
|
|
|
|
|
iced::Subscription::batch([
|
|
|
|
|
|
window::frames().map(Message::Tick),
|
|
|
|
|
|
event::listen_with(|ev, status, win_id| {
|
|
|
|
|
|
use iced::event::Status;
|
|
|
|
|
|
match ev {
|
2026-05-01 11:31:11 +03:00
|
|
|
|
iced::Event::Window(window::Event::CloseRequested) => {
|
|
|
|
|
|
Some(Message::WindowCloseRequested(win_id))
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
iced::Event::Window(window::Event::Closed) => {
|
|
|
|
|
|
Some(Message::OsWindowClosed(win_id))
|
|
|
|
|
|
}
|
|
|
|
|
|
iced::Event::Window(window::Event::Resized(sz)) => {
|
|
|
|
|
|
Some(Message::WindowResized(sz.width as f32, sz.height as f32))
|
|
|
|
|
|
}
|
|
|
|
|
|
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
|
|
|
|
|
key, modifiers, ..
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
let ctrl = modifiers.control();
|
|
|
|
|
|
let shift = modifiers.shift();
|
|
|
|
|
|
match key {
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Enter)
|
|
|
|
|
|
| keyboard::Key::Named(keyboard::key::Named::Space)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::CommandFinalize)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Escape) => {
|
|
|
|
|
|
Some(Message::CommandEscape)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Delete)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::DeleteSelected)
|
|
|
|
|
|
}
|
2026-04-07 00:04:12 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
|
|
|
|
|
|
Some(Message::CommandHistoryPrev)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
|
|
|
|
|
|
Some(Message::CommandHistoryNext)
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F3) => {
|
|
|
|
|
|
Some(Message::ToggleSnapEnabled)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F7) => {
|
|
|
|
|
|
Some(Message::ToggleGrid)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F8) => {
|
|
|
|
|
|
Some(Message::ToggleOrtho)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F9) => {
|
|
|
|
|
|
Some(Message::ToggleGridSnap)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F10) => {
|
|
|
|
|
|
Some(Message::TogglePolar)
|
|
|
|
|
|
}
|
2026-04-08 00:26:54 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F11) => {
|
|
|
|
|
|
Some(Message::ToggleOTrack)
|
|
|
|
|
|
}
|
2026-04-08 00:12:09 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F12) => {
|
|
|
|
|
|
Some(Message::ToggleDynInput)
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
keyboard::Key::Character(c) if ctrl => match c.as_str() {
|
2026-05-03 19:55:38 +03:00
|
|
|
|
"n" => Some(Message::TabNew),
|
2026-03-22 22:44:32 +03:00
|
|
|
|
"o" => Some(Message::OpenFile),
|
|
|
|
|
|
"s" if !shift => Some(Message::SaveFile),
|
|
|
|
|
|
"s" if shift => Some(Message::SaveAs),
|
|
|
|
|
|
"z" if !shift => Some(Message::Undo),
|
|
|
|
|
|
"z" if shift => Some(Message::Redo),
|
|
|
|
|
|
"y" => Some(Message::Redo),
|
|
|
|
|
|
"c" => Some(Message::Command("COPYCLIP".to_string())),
|
|
|
|
|
|
"x" => Some(Message::Command("CUTCLIP".to_string())),
|
|
|
|
|
|
"v" => Some(Message::Command("PASTECLIP".to_string())),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
},
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}),
|
|
|
|
|
|
])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn focus_cmd_input(&self) -> Task<Message> {
|
|
|
|
|
|
iced::widget::operation::focus(iced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn blur_cmd_input(&self) -> Task<Message> {
|
|
|
|
|
|
let op = iced::advanced::widget::operation::focusable::unfocus::<Message>();
|
|
|
|
|
|
iced::advanced::widget::operate(op)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 02:18:41 +03:00
|
|
|
|
// ── Paper canvas ──────────────────────────────────────────────────────────
|
|
|
|
|
|
//
|
2026-04-23 03:19:15 +03:00
|
|
|
|
// PSPACE: single full-canvas PaperSheet widget — renders paper entities plus
|
|
|
|
|
|
// model content of all viewports via CPU projection.
|
2026-04-23 03:02:01 +03:00
|
|
|
|
//
|
2026-04-23 03:19:15 +03:00
|
|
|
|
// MSPACE (active viewport): PaperSheet widget (excludes the active viewport
|
|
|
|
|
|
// from its CPU projection) + a PaperViewportPane widget overlaid at the
|
|
|
|
|
|
// active viewport's screen-space position. PaperViewportPane uses a
|
|
|
|
|
|
// distinct pipeline type (PaperViewportPipeline) so Iced's per-type storage
|
|
|
|
|
|
// keeps the two prepare() calls from overwriting each other.
|
2026-04-23 02:18:41 +03:00
|
|
|
|
|
|
|
|
|
|
fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, Message> {
|
2026-04-23 03:19:15 +03:00
|
|
|
|
let scene = &tab.scene;
|
|
|
|
|
|
|
2026-04-23 09:48:53 +03:00
|
|
|
|
// 2-D canvas for the paper sheet — paper entities, viewport borders, and
|
|
|
|
|
|
// inactive viewport projections are rendered as vector paths. This lets
|
|
|
|
|
|
// users select/edit paper-space entities directly without entering MSPACE.
|
|
|
|
|
|
let paper_sheet = canvas(PaperCanvas::new(scene))
|
2026-04-23 02:18:41 +03:00
|
|
|
|
.width(Fill)
|
2026-04-23 03:19:15 +03:00
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(vp_handle) = scene.active_viewport {
|
|
|
|
|
|
let (canvas_w, canvas_h) = scene.selection.borrow().vp_size;
|
|
|
|
|
|
if let Some(rect) = scene.viewport_screen_rect(vp_handle, (canvas_w, canvas_h)) {
|
2026-04-23 13:57:34 +03:00
|
|
|
|
// Clamp to canvas bounds so Space widgets never get negative size.
|
|
|
|
|
|
let x = rect.x.max(0.0).min(canvas_w);
|
|
|
|
|
|
let y = rect.y.max(0.0).min(canvas_h);
|
|
|
|
|
|
let w = rect.width.clamp(1.0, canvas_w - x);
|
|
|
|
|
|
let h = rect.height.clamp(1.0, canvas_h - y);
|
2026-04-23 03:19:15 +03:00
|
|
|
|
|
|
|
|
|
|
let vp_widget = shader(PaperViewportPane::new(scene, vp_handle))
|
|
|
|
|
|
.width(iced::Length::Fixed(w))
|
|
|
|
|
|
.height(iced::Length::Fixed(h));
|
|
|
|
|
|
|
|
|
|
|
|
let positioned = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(y)),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(x)),
|
|
|
|
|
|
vp_widget,
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
2026-04-23 13:57:34 +03:00
|
|
|
|
// Blue border drawn on top of the 3-D overlay so the viewport
|
|
|
|
|
|
// boundary is always visible even when the shader fills the area.
|
|
|
|
|
|
const VP_BORDER: Color = Color { r: 0.18, g: 0.52, b: 0.95, a: 1.0 };
|
|
|
|
|
|
let border_frame = container(
|
|
|
|
|
|
Space::new()
|
|
|
|
|
|
.width(iced::Length::Fixed(w))
|
|
|
|
|
|
.height(iced::Length::Fixed(h)),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
border: iced::Border {
|
|
|
|
|
|
color: VP_BORDER,
|
|
|
|
|
|
width: 2.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let border_layer = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(y)),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(x)),
|
|
|
|
|
|
border_frame,
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
return stack![paper_sheet, positioned, border_layer]
|
2026-04-23 03:19:15 +03:00
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
paper_sheet.into()
|
2026-04-23 02:18:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
// ── Document tab bar ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
|
|
|
|
|
|
const BAR_BG: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 };
|
|
|
|
|
|
const TAB_ACTIVE: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 };
|
|
|
|
|
|
const TAB_HOVER: Color = Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0 };
|
|
|
|
|
|
const TAB_INACTIVE: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 };
|
|
|
|
|
|
const ACCENT: Color = Color { r: 0.20, g: 0.55, b: 0.90, a: 1.0 };
|
|
|
|
|
|
const TEXT_ACTIVE: Color = Color::WHITE;
|
|
|
|
|
|
const TEXT_INACTIVE: Color = Color { r: 0.60, g: 0.60, b: 0.60, a: 1.0 };
|
|
|
|
|
|
const CLOSE_HOVER: Color = Color { r: 0.70, g: 0.22, b: 0.22, a: 1.0 };
|
|
|
|
|
|
const BORDER_COLOR: Color = Color { r: 0.25, g: 0.25, b: 0.25, a: 1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
let mut bar = Row::new().spacing(0).align_y(iced::Center);
|
|
|
|
|
|
|
|
|
|
|
|
for (idx, tab) in tabs.iter().enumerate() {
|
|
|
|
|
|
let is_active = idx == active_tab;
|
|
|
|
|
|
let name = tab.tab_display_name();
|
|
|
|
|
|
let label = if tab.dirty { format!("● {}", name) } else { name };
|
|
|
|
|
|
|
|
|
|
|
|
let title_btn = button(text(label).size(12))
|
|
|
|
|
|
.on_press(Message::TabSwitch(idx))
|
|
|
|
|
|
.padding([5, 12])
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match (is_active, status) {
|
|
|
|
|
|
(true, _) => TAB_ACTIVE,
|
|
|
|
|
|
(false, button::Status::Hovered) => TAB_HOVER,
|
|
|
|
|
|
_ => TAB_INACTIVE,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: if is_active { TEXT_ACTIVE } else { TEXT_INACTIVE },
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: if is_active { ACCENT } else { Color::TRANSPARENT },
|
|
|
|
|
|
width: if is_active { 1.0 } else { 0.0 },
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let close_btn = button(text("×").size(11).color(Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }))
|
|
|
|
|
|
.on_press(Message::TabClose(idx))
|
|
|
|
|
|
.padding([3, 5])
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => CLOSE_HOVER,
|
|
|
|
|
|
_ => if is_active { TAB_ACTIVE } else { TAB_INACTIVE },
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border { radius: 3.0.into(), ..Default::default() },
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
bar = bar.push(
|
|
|
|
|
|
container(row![title_btn, close_btn].spacing(0).align_y(iced::Center))
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: if is_active { BORDER_COLOR } else { Color::TRANSPARENT },
|
|
|
|
|
|
width: if is_active { 1.0 } else { 0.0 },
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let new_btn = button(text("+").size(14).color(Color { r: 0.65, g: 0.65, b: 0.65, a: 1.0 }))
|
|
|
|
|
|
.on_press(Message::TabNew)
|
|
|
|
|
|
.padding([4, 10])
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => TAB_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border { radius: 0.0.into(), ..Default::default() },
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
bar = bar.push(new_btn);
|
|
|
|
|
|
bar = bar.push(iced::widget::Space::new().width(Fill));
|
|
|
|
|
|
|
|
|
|
|
|
container(bar)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(BAR_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER_COLOR,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.height(30)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.padding([0, 2])
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
2026-03-31 07:13:36 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Layout context-menu overlay ────────────────────────────────────────────
|
|
|
|
|
|
|
2026-04-08 00:41:01 +03:00
|
|
|
|
// ── Viewport right-click context menu ──────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
fn viewport_context_menu_overlay(
|
|
|
|
|
|
pos: iced::Point,
|
|
|
|
|
|
has_cmd: bool,
|
|
|
|
|
|
has_selection: bool,
|
|
|
|
|
|
last_cmds: Vec<String>,
|
|
|
|
|
|
) -> Element<'static, Message> {
|
|
|
|
|
|
const MENU_BG: Color = Color { r: 0.17, g: 0.17, b: 0.17, a: 1.0 };
|
|
|
|
|
|
const MENU_BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
|
|
|
|
|
const ITEM_HOVER: Color = Color { r: 0.25, g: 0.45, b: 0.70, a: 1.0 };
|
|
|
|
|
|
const TEXT_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
|
|
|
|
|
const SEP_COL: Color = Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
let item = |label: String, msg: Message| -> Element<'static, Message> {
|
|
|
|
|
|
button(text(label).size(12).color(TEXT_COL))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
|
border: Border::default(),
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 12])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let sep = || -> Element<'static, Message> {
|
|
|
|
|
|
container(iced::widget::Space::new().width(Fill).height(1))
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(SEP_COL)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(1)
|
|
|
|
|
|
.padding([0, 6])
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let mut items: Vec<Element<'static, Message>> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
if has_cmd {
|
|
|
|
|
|
items.push(item("Cancel".to_string(), Message::CommandEscape));
|
|
|
|
|
|
items.push(item("Enter".to_string(), Message::CommandFinalize));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if !last_cmds.is_empty() {
|
|
|
|
|
|
let last = last_cmds[0].clone();
|
|
|
|
|
|
items.push(item(
|
|
|
|
|
|
format!("Repeat {last}"),
|
|
|
|
|
|
Message::Command(last.to_uppercase()),
|
|
|
|
|
|
));
|
|
|
|
|
|
if last_cmds.len() > 1 {
|
|
|
|
|
|
for cmd in last_cmds.iter().skip(1) {
|
|
|
|
|
|
let c = cmd.clone();
|
|
|
|
|
|
items.push(item(c.clone(), Message::Command(c.to_uppercase())));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
items.push(sep());
|
|
|
|
|
|
}
|
|
|
|
|
|
if has_selection {
|
|
|
|
|
|
items.push(item("Delete".to_string(), Message::DeleteSelected));
|
|
|
|
|
|
items.push(item("Move".to_string(), Message::Command("MOVE".to_string())));
|
|
|
|
|
|
items.push(item("Copy".to_string(), Message::Command("COPY".to_string())));
|
|
|
|
|
|
items.push(sep());
|
|
|
|
|
|
}
|
|
|
|
|
|
items.push(item("Select All".to_string(), Message::Command("SELECTALL".to_string())));
|
|
|
|
|
|
items.push(item("Zoom Extents".to_string(), Message::Command("ZOOM".to_string())));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let menu_col = column(items).spacing(0).width(180);
|
|
|
|
|
|
|
|
|
|
|
|
let menu = container(menu_col)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(MENU_BG)),
|
|
|
|
|
|
border: Border { color: MENU_BORDER, width: 1.0, radius: 4.0.into() },
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 0]);
|
|
|
|
|
|
|
|
|
|
|
|
// Click-catcher to close the menu when clicking outside.
|
|
|
|
|
|
let catcher = mouse_area(
|
|
|
|
|
|
container(Space::new()).width(Fill).height(Fill),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::ViewportContextMenuClose)
|
|
|
|
|
|
.on_right_press(Message::ViewportContextMenuClose);
|
|
|
|
|
|
|
|
|
|
|
|
// Position using top/left spacing.
|
|
|
|
|
|
let positioned = column![
|
|
|
|
|
|
Space::new().height(pos.y),
|
|
|
|
|
|
row![Space::new().width(pos.x), menu],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
stack![catcher, positioned].into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 07:13:36 +03:00
|
|
|
|
/// A small right-click context menu rendered above the status bar.
|
|
|
|
|
|
/// The `name` is the layout tab that was right-clicked.
|
|
|
|
|
|
fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> {
|
|
|
|
|
|
const MENU_BG: Color = Color { r: 0.17, g: 0.17, b: 0.17, a: 1.0 };
|
|
|
|
|
|
const MENU_BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
|
|
|
|
|
const ITEM_HOVER: Color = Color { r: 0.25, g: 0.45, b: 0.70, a: 1.0 };
|
|
|
|
|
|
const TEXT_COLOR: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
let item = |label: &'static str, msg: Message| {
|
|
|
|
|
|
button(text(label).size(12).color(TEXT_COLOR))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT_COLOR,
|
|
|
|
|
|
border: Border::default(),
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 12])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let rename_name = name.to_string();
|
|
|
|
|
|
let delete_name = name.to_string();
|
|
|
|
|
|
|
|
|
|
|
|
let menu = container(
|
|
|
|
|
|
column![
|
2026-03-31 08:18:29 +03:00
|
|
|
|
item("Rename", Message::LayoutRenameStart(rename_name)),
|
|
|
|
|
|
item("Delete", Message::LayoutDelete(delete_name)),
|
2026-03-31 07:13:36 +03:00
|
|
|
|
]
|
|
|
|
|
|
.spacing(0)
|
|
|
|
|
|
.width(160),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(MENU_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: MENU_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 0]);
|
|
|
|
|
|
|
|
|
|
|
|
// Click-catcher fills the whole screen to close the menu when clicking outside.
|
|
|
|
|
|
let catcher = mouse_area(
|
|
|
|
|
|
container(iced::widget::Space::new().width(Fill).height(Fill))
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::LayoutContextMenuClose)
|
|
|
|
|
|
.on_right_press(Message::LayoutContextMenuClose);
|
|
|
|
|
|
|
|
|
|
|
|
// Position the menu above the status bar at the left.
|
|
|
|
|
|
let positioned = container(menu)
|
|
|
|
|
|
.align_bottom(Fill)
|
|
|
|
|
|
.align_left(Fill)
|
|
|
|
|
|
.padding(iced::Padding { top: 0.0, right: 0.0, bottom: 30.0, left: 8.0 });
|
|
|
|
|
|
|
|
|
|
|
|
stack![catcher, positioned].into()
|
|
|
|
|
|
}
|
2026-03-31 08:05:17 +03:00
|
|
|
|
|
2026-05-01 11:31:11 +03:00
|
|
|
|
/// Content for the floating "Unsaved Changes" OS window.
|
2026-05-01 16:12:32 +03:00
|
|
|
|
const SAVE_FORMAT_OPTIONS: &[&str] = &[
|
|
|
|
|
|
"DWG 2018", "DWG 2013", "DWG 2010", "DWG 2007", "DWG 2004", "DWG 2000", "DWG R14",
|
|
|
|
|
|
"DXF 2018", "DXF 2013", "DXF 2010", "DXF 2007", "DXF 2004", "DXF 2000", "DXF R14",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
fn save_as_dialog_window<'a>(
|
|
|
|
|
|
filename: &'a str,
|
2026-05-01 16:20:38 +03:00
|
|
|
|
folder: &'a std::path::Path,
|
|
|
|
|
|
entries: &'a [(String, bool, std::path::PathBuf)],
|
2026-05-01 16:12:32 +03:00
|
|
|
|
format: &'a str,
|
|
|
|
|
|
) -> Element<'a, Message> {
|
2026-05-01 16:20:38 +03:00
|
|
|
|
const BG: Color = Color { r: 0.15, g: 0.15, b: 0.17, a: 1.0 };
|
|
|
|
|
|
const LIST_BG: Color = Color { r: 0.11, g: 0.11, b: 0.13, a: 1.0 };
|
|
|
|
|
|
const BORDER: Color = Color { r: 0.32, g: 0.32, b: 0.36, a: 1.0 };
|
|
|
|
|
|
const TEXT: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
|
|
|
|
|
|
const DIM: Color = Color { r: 0.58, g: 0.58, b: 0.62, a: 1.0 };
|
|
|
|
|
|
const INPUT_BG: Color = Color { r: 0.10, g: 0.10, b: 0.12, a: 1.0 };
|
2026-05-01 16:12:32 +03:00
|
|
|
|
const BTN_OK: Color = Color { r: 0.20, g: 0.46, b: 0.80, a: 1.0 };
|
|
|
|
|
|
const BTN_HOV: Color = Color { r: 0.26, g: 0.55, b: 0.92, a: 1.0 };
|
2026-05-01 16:20:38 +03:00
|
|
|
|
const BTN_GREY: Color = Color { r: 0.26, g: 0.26, b: 0.29, a: 1.0 };
|
|
|
|
|
|
const BTN_GHOV: Color = Color { r: 0.34, g: 0.34, b: 0.38, a: 1.0 };
|
|
|
|
|
|
const DIR_COL: Color = Color { r: 0.75, g: 0.85, b: 1.00, a: 1.0 };
|
|
|
|
|
|
const FILE_COL: Color = TEXT;
|
|
|
|
|
|
const ROW_HOV: Color = Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0 };
|
2026-05-01 16:12:32 +03:00
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
|
let input_sty = |_: &Theme, _: iced::widget::text_input::Status| {
|
2026-05-01 16:12:32 +03:00
|
|
|
|
iced::widget::text_input::Style {
|
|
|
|
|
|
background: Background::Color(INPUT_BG),
|
2026-05-01 16:20:38 +03:00
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
|
|
|
|
|
icon: TEXT, placeholder: DIM, value: TEXT,
|
|
|
|
|
|
selection: Color { r: 0.20, g: 0.46, b: 0.80, a: 0.45 },
|
2026-05-01 16:12:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
|
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
|
|
|
|
|
|
button(text(lbl).size(12).color(TEXT))
|
2026-05-01 16:12:32 +03:00
|
|
|
|
.on_press(msg)
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.style(move |_: &Theme, st| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(
|
|
|
|
|
|
if matches!(st, button::Status::Hovered | button::Status::Pressed) { hov } else { base }
|
|
|
|
|
|
)),
|
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
|
|
|
|
|
..Default::default()
|
2026-05-01 16:12:32 +03:00
|
|
|
|
})
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.padding([4, 12])
|
2026-05-01 16:12:32 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
|
// ── Path bar ─────────────────────────────────────────────────────────
|
|
|
|
|
|
let path_str = folder.to_string_lossy().into_owned();
|
|
|
|
|
|
let up_path = folder.parent().map(|p| p.to_path_buf());
|
|
|
|
|
|
let path_bar = row![
|
|
|
|
|
|
{
|
|
|
|
|
|
let up_msg = up_path.map(Message::SaveDialogNavigate);
|
|
|
|
|
|
let b = button(text("↑").size(14).color(TEXT))
|
|
|
|
|
|
.style(|_: &Theme, st| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(
|
|
|
|
|
|
if matches!(st, button::Status::Hovered | button::Status::Pressed)
|
|
|
|
|
|
{ BTN_GHOV } else { BTN_GREY }
|
|
|
|
|
|
)),
|
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([3, 10]);
|
|
|
|
|
|
if let Some(msg) = up_msg { b.on_press(msg) } else { b }
|
|
|
|
|
|
},
|
|
|
|
|
|
Space::new().width(8),
|
|
|
|
|
|
container(
|
|
|
|
|
|
text(path_str.clone()).size(12).color(DIM)
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(INPUT_BG)),
|
|
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
|
.width(Fill),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center);
|
|
|
|
|
|
|
|
|
|
|
|
// ── File list ─────────────────────────────────────────────────────────
|
|
|
|
|
|
let file_list: Element<'_, Message> = {
|
|
|
|
|
|
let rows: Vec<Element<'_, Message>> = entries.iter().map(|(name, is_dir, path)| {
|
|
|
|
|
|
let icon = if *is_dir { "📁" } else { "📄" };
|
|
|
|
|
|
let color = if *is_dir { DIR_COL } else { FILE_COL };
|
|
|
|
|
|
let p = path.clone();
|
|
|
|
|
|
let d = *is_dir;
|
|
|
|
|
|
mouse_area(
|
|
|
|
|
|
container(
|
|
|
|
|
|
row![
|
|
|
|
|
|
text(icon).size(13),
|
|
|
|
|
|
Space::new().width(6),
|
|
|
|
|
|
text(name.as_str()).size(13).color(color),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(|_: &Theme| container::Style { ..Default::default() })
|
|
|
|
|
|
.padding([3, 8])
|
|
|
|
|
|
.width(Fill),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::SaveDialogEntryClicked(p, d))
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}).collect();
|
2026-05-01 16:12:32 +03:00
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
|
container(
|
|
|
|
|
|
iced::widget::scrollable(
|
|
|
|
|
|
column(rows).spacing(1).width(Fill)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(LIST_BG)),
|
|
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
let _ = ROW_HOV; // used conceptually, suppress warning
|
2026-05-01 16:12:32 +03:00
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
|
let sel_fmt = SAVE_FORMAT_OPTIONS.iter().copied().find(|&s| s == format);
|
|
|
|
|
|
let label = |s: &'static str| text(s).size(11).color(DIM);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Bottom controls ───────────────────────────────────────────────────
|
|
|
|
|
|
let bottom = column![
|
|
|
|
|
|
row![
|
|
|
|
|
|
label("File name:").width(90),
|
|
|
|
|
|
text_input("drawing.dwg", filename)
|
2026-05-01 16:12:32 +03:00
|
|
|
|
.on_input(Message::SaveDialogFilenameChanged)
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.style(input_sty)
|
2026-05-01 16:12:32 +03:00
|
|
|
|
.size(13)
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.padding([5, 8])
|
|
|
|
|
|
.width(Fill),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
|
.spacing(6),
|
|
|
|
|
|
Space::new().height(6),
|
|
|
|
|
|
row![
|
|
|
|
|
|
label("Format:").width(90),
|
|
|
|
|
|
pick_list(SAVE_FORMAT_OPTIONS, sel_fmt,
|
|
|
|
|
|
|s: &str| Message::SaveDialogFormatChanged(s.to_string()))
|
|
|
|
|
|
.width(Fill),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
|
.spacing(6),
|
|
|
|
|
|
Space::new().height(12),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(Fill),
|
|
|
|
|
|
btn("Save", Message::SaveDialogConfirm, BTN_OK, BTN_HOV),
|
|
|
|
|
|
Space::new().width(8),
|
|
|
|
|
|
btn("Cancel", Message::SaveDialogCancel, BTN_GREY, BTN_GHOV),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(0);
|
|
|
|
|
|
|
|
|
|
|
|
container(
|
|
|
|
|
|
column![
|
|
|
|
|
|
path_bar,
|
|
|
|
|
|
Space::new().height(8),
|
|
|
|
|
|
file_list,
|
|
|
|
|
|
Space::new().height(10),
|
|
|
|
|
|
bottom,
|
2026-05-01 16:12:32 +03:00
|
|
|
|
]
|
|
|
|
|
|
.spacing(0),
|
|
|
|
|
|
)
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.style(|_: &Theme| container::Style {
|
2026-05-01 16:12:32 +03:00
|
|
|
|
background: Some(Background::Color(BG)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.padding([14, 16])
|
2026-05-01 16:12:32 +03:00
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 11:31:11 +03:00
|
|
|
|
fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
|
|
|
|
|
|
const BG: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
|
|
|
|
|
|
const BORDER_COL: Color = Color { r: 0.38, g: 0.38, b: 0.42, a: 1.0 };
|
|
|
|
|
|
const TEXT_COL: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
|
|
|
|
|
|
const BTN_SAVE: Color = Color { r: 0.20, g: 0.46, b: 0.80, a: 1.0 };
|
|
|
|
|
|
const BTN_HOVER: Color = Color { r: 0.26, g: 0.55, b: 0.92, a: 1.0 };
|
|
|
|
|
|
const BTN_DISC: Color = Color { r: 0.28, g: 0.28, b: 0.30, a: 1.0 };
|
|
|
|
|
|
const BTN_DHOV: Color = Color { r: 0.36, g: 0.36, b: 0.40, a: 1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
let body_text = format!("Do you want to save changes to \"{}\"?", name);
|
|
|
|
|
|
|
|
|
|
|
|
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
|
|
|
|
|
|
button(text(label).size(13).color(TEXT_COL))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => hov,
|
|
|
|
|
|
_ => base,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
|
border: Border { color: BORDER_COL, width: 1.0, radius: 4.0.into() },
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([6, 18])
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
container(
|
|
|
|
|
|
column![
|
|
|
|
|
|
text(body_text).size(13).color(TEXT_COL),
|
|
|
|
|
|
iced::widget::Space::new().height(20),
|
|
|
|
|
|
row![
|
|
|
|
|
|
btn("Save", Message::UnsavedDialogSave, BTN_SAVE, BTN_HOVER),
|
|
|
|
|
|
iced::widget::Space::new().width(8),
|
|
|
|
|
|
btn("Discard", Message::UnsavedDialogDiscard, BTN_DISC, BTN_DHOV),
|
|
|
|
|
|
iced::widget::Space::new().width(8),
|
|
|
|
|
|
btn("Cancel", Message::UnsavedDialogCancel, BTN_DISC, BTN_DHOV),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(0),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(BG)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.center(Fill)
|
|
|
|
|
|
.padding([24, 28])
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|