Merge main into Select-Color-Redesign

Keep both sides: main's ViewBackground colour target and PREVIEW icon
next to this branch's Interface target and LIST/SEARCH icons.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
Claude committed 2026-10-14 19:10:33 +00:00
commit 451b0f5d78
231 files changed
+32217 -3423

No files matched your search

+4 -2
View File
@@ -30,6 +30,7 @@ pub(super) fn viewport_controls<'a>(
tile_count: usize,
render_mode_menu_open: bool,
render_mode_preview: Option<codec::entities::ViewportRenderMode>,
style_name: Option<String>,
) -> Element<'a, Message> {
let render_modes: Vec<RenderModeChoice> = crate::modules::view::visual_style::VISUAL_STYLES
.iter()
@@ -98,7 +99,7 @@ pub(super) fn viewport_controls<'a>(
// flyout keeps the current style unchanged while hovering; a click commits.
let picker_head = button(
row![
text(RenderModeChoice(render_mode).to_string()).size(11),
text(style_name.unwrap_or_else(|| RenderModeChoice(render_mode).to_string())).size(11),
crate::ui::icons::themed_arrow_toggle(render_mode_menu_open, 9.0),
]
.spacing(6)
@@ -379,7 +380,7 @@ mod tests {
#[test]
fn test_viewport_controls_construction() {
for style in crate::modules::view::visual_style::VISUAL_STYLES {
let _el = viewport_controls(style.mode, true, true, true, 1, false, None);
let _el = viewport_controls(style.mode, true, true, true, 1, false, None, None);
let _el_open = viewport_controls(
style.mode,
false,
@@ -388,6 +389,7 @@ mod tests {
2,
true,
Some(M::Wireframe3D),
None,
);
}
}
+331 -14
View File
@@ -15,6 +15,46 @@ use iced::widget::{
use iced::window;
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
use iced_aw::ContextMenu;
/// Invisible event surface for accurate point picking. MouseArea emits a
/// positionless press message; capturing the cursor in the actual button
/// event avoids using the previous hover location when clicking quickly.
#[derive(Debug, Clone, Copy)]
struct ViewportPickSurface;
impl iced::widget::canvas::Program<Message> for ViewportPickSurface {
type State = ();
fn update(
&self,
_state: &mut Self::State,
event: &iced::Event,
bounds: iced::Rectangle,
cursor: iced::mouse::Cursor,
) -> Option<iced::widget::canvas::Action<Message>> {
if let iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)) =
event
{
return cursor.position_in(bounds).map(|position| {
iced::widget::canvas::Action::publish(Message::ViewportLeftPressAt(position))
.and_capture()
});
}
None
}
fn draw(
&self,
_state: &Self::State,
_renderer: &iced::Renderer,
_theme: &iced::Theme,
_bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry> {
Vec::new()
}
}
use std::sync::Arc;
mod controls;
@@ -182,6 +222,24 @@ impl std::fmt::Display for RenderModeChoice {
}
}
/// A named style (Realistic, …) the active viewport draws as, by name.
fn named_visual_style(
scene: &crate::scene::Scene,
mode: codec::entities::ViewportRenderMode,
) -> Option<String> {
match scene.document.objects.get(&scene.active_visual_style()) {
Some(codec::objects::ObjectType::VisualStyle(style)) => {
match crate::modules::view::visual_style::mode_for_style_name(&style.description) {
Some((named_mode, true)) if named_mode == mode => {
Some(crate::t!(style.description.as_str()).into_owned())
}
_ => None,
}
}
_ => None,
}
}
impl OpenCADStudio {
#[cfg(not(target_arch = "wasm32"))]
pub fn view(&self, window_id: window::Id) -> Element<'_, Message> {
@@ -321,6 +379,7 @@ impl OpenCADStudio {
&self.recent_thumbs,
self.recent_limit,
&self.recent_limit_input,
self.recent_view,
self.start_action_w.clone(),
self.start_section,
)
@@ -857,10 +916,16 @@ bg={bg_ms:.1}ms n={view_count}"
.hover_highlight
.map(|h| tab.scene.is_layer_locked(h))
.unwrap_or(false);
let point_cursor = tab
.active_cmd
.as_ref()
.is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering());
// Keep the hollow pickbox at rest. Use the standard LINE/RECTANG
// dot crosshair only after the first window/crossing corner.
// No OSNAP or selection behavior changes.
let selecting_window = tab.active_cmd.is_none()
&& tab.scene.selection.borrow().gesture.box_anchor.is_some();
let point_cursor = selecting_window
|| tab
.active_cmd
.as_ref()
.is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering());
let constraint_cursor_badge = if is_paper {
None
} else {
@@ -939,7 +1004,13 @@ bg={bg_ms:.1}ms n={view_count}"
|| c.is_selection_gathering()
}),
snap_color: self.snap_marker_color,
snap_size: self.model_space.snap_marker_size as f32,
},
// MATCHPROP paints properties onto destinations; it gets the
// paintbrush cursor instead of the crosshair.
tab.active_cmd
.as_ref()
.is_some_and(|c| c.name() == "MATCHPROP"),
crate::ui::overlay::SelectionVisualOptions {
area: self.model_space.selection_area,
opacity: self.model_space.selection_opacity,
@@ -961,9 +1032,15 @@ bg={bg_ms:.1}ms n={view_count}"
mark("viewport_mouse");
let viewport_mouse = mouse_area(container(
iced::widget::Space::new().width(Fill).height(Fill),
iced::widget::canvas(ViewportPickSurface)
.width(Fill)
.height(Fill),
))
.on_move(Message::ViewportMove)
// Left mouse presses are captured by the canvas with their exact
// event-time position. MouseArea::on_press has no coordinate, so
// queued cursor moves could otherwise make quick clicks use a stale
// snap position. Touch presses still use this fallback.
.on_press(Message::ViewportLeftPress)
.on_release(Message::ViewportLeftRelease)
.on_right_press(Message::ViewportRightPress)
@@ -1011,6 +1088,20 @@ bg={bg_ms:.1}ms n={view_count}"
{
let w = tab.last_cursor_world;
let base = tab.dyn_anchor.or(self.last_point);
// A single distance field on an acquired tracking ray is measured
// from the acquired OSNAP/OTRACK point, not from the command's
// previous point (or from WCS zero before the first pick).
// Keep typed fields and ordinary polar distance inputs unchanged.
let tracking_distance = if tab.dyn_fields.len() == 1
&& tab.dyn_fields[0].component == DynComponent::Distance
{
self.active_distance_ray(self.active_tab).and_then(|(origin, direction)| {
let axis = direction.try_normalize()?;
Some((w - origin).dot(axis).abs())
})
} else {
None
};
let label_screen = tab
.active_cmd
.as_ref()
@@ -1160,6 +1251,16 @@ bg={bg_ms:.1}ms n={view_count}"
format!("{lv:.4}")
}
}
// The displayed live distance must use the same
// acquired tracking origin as the guide and typed
// distance placement, not the last picked point.
(None, _) if f.component == DynComponent::Distance
&& tracking_distance.is_some() =>
{
crate::entities::common::format_length(
tracking_distance.unwrap() * f.role.value_scale() as f64,
)
}
_ => dyn_component_value(
f,
w,
@@ -1297,6 +1398,7 @@ bg={bg_ms:.1}ms n={view_count}"
let rect = tab.scene.active_model_tile_bounds(vw, vh);
// Unified control chip: split buttons + render-mode picker +
// grid / grid-snap toggles, for the active Model tile.
let style_name = named_visual_style(&tab.scene, tab.render_mode);
let bar = viewport_controls(
tab.render_mode,
self.show_grid,
@@ -1305,6 +1407,7 @@ bg={bg_ms:.1}ms n={view_count}"
tab.scene.model_tiles.borrow().len(),
self.render_mode_menu_open,
self.render_mode_preview,
style_name,
);
// Adaptive: DensitySwap measures the bar's real width every frame
// (reported into `render_bar_w`, which the ViewCube reads to decide
@@ -1397,6 +1500,7 @@ bg={bg_ms:.1}ms n={view_count}"
0,
self.render_mode_menu_open,
self.render_mode_preview,
named_visual_style(&tab.scene, vp_mode),
);
let adaptive: Element<'_, Message> = DensitySwap::new(vec![
iced::widget::opaque(bar),
@@ -1660,9 +1764,18 @@ bg={bg_ms:.1}ms n={view_count}"
// In-place block edit (REFEDIT): right-edge toolbar with Save / Discard
// so the edit can be finished by clicking. (#136)
if tab.refedit_session.is_some() && !tab.is_start {
if let Some(tb) = crate::ui::side_toolbar::view(
&crate::modules::draw::modify::refedit::refedit_tools(),
if let (Some(session), false) = (tab.refedit_session.as_ref(), tab.is_start) {
use crate::modules::draw::modify::refedit::{refedit_set_tools, refedit_tools};
let set_tools = refedit_set_tools();
let close_tools = refedit_tools();
let header = crate::ui::side_toolbar::refedit_header(
&session.block_name,
refedit_working_set_count(session, &tab.scene),
);
if let Some(tb) = crate::ui::side_toolbar::view_sections(
Some(header),
&[&set_tools, &close_tools],
&|_| false,
) {
viewport_stack = viewport_stack.push(tb);
}
@@ -1999,7 +2112,10 @@ bg={bg_ms:.1}ms n={view_count}"
// Clean-screen mode drops the ribbon for a full-canvas view; the
// status bar stays so the mode can be toggled back off.
let mut col = column![];
if !self.clean_screen {
if !self.clean_screen && self.show_menu_bar {
col = col.push(crate::ui::menu_bar::view());
}
if !self.clean_screen && !self.ribbon_closed {
col = col.push(self.ribbon.view(
is_paper,
self.tabs[self.active_tab].is_start,
@@ -2222,12 +2338,23 @@ bg={bg_ms:.1}ms n={view_count}"
iced::widget::Space::new().width(0).height(0).into()
};
// Key tips: the hint bar under the dropdowns, the codes above all.
let (keytip_bar, keytip_layer): (Element<'_, Message>, Element<'_, Message>) =
self.ribbon.keytip_layers().unwrap_or_else(|| {
(
iced::widget::Space::new().width(0).height(0).into(),
iced::widget::Space::new().width(0).height(0).into(),
)
});
let composed = stack![
main_ui,
keytip_bar,
dropdown_layer,
qselect_layer,
snap_override_layer,
open_progress_layer,
keytip_layer,
];
// If the Plot Style editor was launched from PLOT, keep the Plot dialog
@@ -2440,8 +2567,10 @@ impl OpenCADStudio {
let web_fonts = Subscription::none();
// Periodic autosave (SAVETIME): a `.sv$` recovery copy beside the file
// on native, an OPFS snapshot on web. SAVETIME is the interval in
// minutes; 0 disables it.
let autosave = if self.savetime_min > 0 {
// minutes; 0 disables it. The web build checks the deadline from the
// `PollWebFonts` tick instead: a long `time::every` there stalls every
// other timer for its whole period (#1775).
let autosave = if self.savetime_min > 0 && cfg!(not(target_arch = "wasm32")) {
iced::time::every(std::time::Duration::from_secs(
self.savetime_min as u64 * 60,
))
@@ -2495,9 +2624,18 @@ impl OpenCADStudio {
event::listen_with(|ev, status, win_id| {
use iced::event::Status;
match ev {
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(_))
if crate::ui::ribbon::keytips_active() =>
{
crate::ui::ribbon::keytip_pointer_pressed();
Some(Message::KeyTip(crate::ui::ribbon::KeyTipInput::Hide))
}
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(
iced::mouse::Button::Left,
)) => Some(Message::PropPointerPressed),
)) => {
crate::ui::ribbon::keytip_pointer_pressed();
Some(Message::PropPointerPressed)
}
iced::Event::Window(window::Event::CloseRequested) => {
Some(Message::WindowCloseRequested(win_id))
}
@@ -2510,6 +2648,9 @@ impl OpenCADStudio {
iced::Event::Window(window::Event::FileDropped(path)) => {
Some(Message::FileDropped(path))
}
iced::Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
crate::ui::ribbon::keytip_key_released(&key)
}
iced::Event::Keyboard(keyboard::Event::ModifiersChanged(m)) => {
// `command()` is Cmd on macOS, Ctrl elsewhere — the
// platform multi-select modifier for the layer list.
@@ -2523,8 +2664,14 @@ impl OpenCADStudio {
physical_key,
modifiers,
text,
repeat,
..
}) => {
if let Some(keytip) =
crate::ui::ribbon::keytip_key_pressed(&key, modifiers, repeat)
{
return keytip;
}
#[cfg(target_arch = "wasm32")]
let accel = modifiers.command();
// AltGr reaches Windows apps as Ctrl+Alt, and a macOS
@@ -2670,6 +2817,7 @@ impl OpenCADStudio {
.map(|_| Message::SpaceMouseWake),
crate::input::trackpad::subscription().map(Message::TrackpadPinch),
event::listen_with(|event, _, id| match event {
iced::Event::Touch(touch) => Some(Message::Touch(touch)),
iced::Event::Window(window::Event::Focused) => {
Some(Message::SpaceMouseFocus(id, true))
}
@@ -3052,6 +3200,7 @@ pub(super) fn start_page_view<'a>(
thumbs: &'a std::collections::HashMap<std::path::PathBuf, Option<iced::widget::image::Handle>>,
recent_limit: usize,
recent_limit_input: &'a str,
recent_view: super::RecentView,
action_width_out: std::sync::Arc<std::sync::atomic::AtomicU32>,
active: super::StartSection,
) -> Element<'a, Message> {
@@ -3067,6 +3216,7 @@ pub(super) fn start_page_view<'a>(
thumbs,
recent_limit,
recent_limit_input,
recent_view,
size.width,
action_width_out.clone(),
active,
@@ -3086,6 +3236,7 @@ fn start_page_content<'a>(
thumbs: &'a std::collections::HashMap<std::path::PathBuf, Option<iced::widget::image::Handle>>,
recent_limit: usize,
recent_limit_input: &'a str,
recent_view: super::RecentView,
avail_w: f32,
action_width_out: std::sync::Arc<std::sync::atomic::AtomicU32>,
active: super::StartSection,
@@ -3309,6 +3460,7 @@ fn start_page_content<'a>(
thumbs,
recent_limit,
recent_limit_input,
recent_view,
match start_layout {
StartLayout::AllPanels
| StartLayout::WithoutVideos
@@ -3746,16 +3898,149 @@ fn start_page_content<'a>(
// Properties panel normally occupies, but only when the active tab is the
// Start tab. The list is restored from disk at boot and re-saved on every
// open — entries persist across sessions.
/// Recent files as tiles: small thumbnails wrapping across the panel, or one
/// large preview per row. A tile opens its file; the list view keeps the
/// remove buttons.
fn recent_tiles<'a>(
recents: &'a [std::path::PathBuf],
thumbs: &'a std::collections::HashMap<std::path::PathBuf, Option<iced::widget::image::Handle>>,
large: bool,
) -> iced::widget::Column<'a, Message> {
let (thumb_w, thumb_h, name_chars) = if large {
(Fill, iced::Length::Fixed(180.0), 40)
} else {
(iced::Length::Fixed(72.0), iced::Length::Fixed(54.0), 11)
};
let tile = |path: &'a std::path::PathBuf| -> Element<'a, Message> {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string_lossy().into_owned());
let picture: Element<'a, Message> = match thumbs.get(path).and_then(|o| o.as_ref()) {
Some(h) => iced::widget::image(h.clone())
.content_fit(iced::ContentFit::Contain)
.width(Fill)
.height(Fill)
.into(),
None => crate::ui::icons::themed_secondary(crate::ui::icons::DOC, 28.0),
};
let picture = container(picture)
.width(thumb_w)
.height(thumb_h)
.center_x(thumb_w)
.center_y(thumb_h)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.base.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
});
let mut label = column![text(crate::ui::text_util::elide(&name, name_chars)).size(12)];
if large {
let dir = path
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
label = label.push(
text(crate::ui::text_util::elide(&dir, 52))
.size(10)
.style(start_muted_style),
);
}
button(column![picture, label.spacing(2)].spacing(4))
.on_press(Message::OpenRecent(path.clone()))
.padding(6)
.width(if large { Fill } else { iced::Length::Shrink })
.style(|theme: &Theme, status| {
let palette = theme.palette();
button::Style {
background: matches!(status, button::Status::Hovered)
.then_some(Background::Color(palette.background.strong.color)),
text_color: palette.background.base.text,
border: Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
}
})
.into()
};
if large {
column(recents.iter().map(tile)).spacing(6)
} else {
column![iced::widget::Row::with_children(recents.iter().map(tile))
.spacing(4)
.wrap()]
.width(Fill)
}
}
pub(super) fn recent_files_panel<'a>(
recents: &'a [std::path::PathBuf],
thumbs: &'a std::collections::HashMap<std::path::PathBuf, Option<iced::widget::image::Handle>>,
limit: usize,
limit_input: &'a str,
view: super::RecentView,
width: iced::Length,
) -> Element<'a, Message> {
use super::RecentView;
// Title mirrors the Supporters rail: size 15 in the bright text colour,
// followed by a 12px gap before the content.
let title = text(crate::tr!("start", "recent-documents")).size(15);
// followed by a 12px gap before the content. The view-mode toggles sit
// on the title's right.
let mode_button = |icon: &'static [u8], mode: RecentView, tip: String| {
let active = view == mode;
iced::widget::tooltip(
button(crate::ui::icons::themed(icon, 13.0))
.on_press(Message::SetRecentView(mode))
.padding([3, 5])
.style(move |theme: &Theme, status| {
let palette = theme.palette();
let background = if active {
Some(palette.primary.weak.color)
} else {
matches!(status, button::Status::Hovered)
.then_some(palette.background.strong.color)
};
button::Style {
background: background.map(Background::Color),
text_color: palette.background.base.text,
border: Border {
radius: 3.0.into(),
..Default::default()
},
..Default::default()
}
}),
container(text(tip).size(11)).padding([2, 6]).style(container::rounded_box),
iced::widget::tooltip::Position::Bottom,
)
};
let title = row![
text(crate::tr!("start", "recent-documents"))
.size(15)
.width(Fill),
mode_button(
crate::ui::icons::MENU,
RecentView::List,
crate::tr!("start", "view-list"),
),
mode_button(
crate::ui::icons::GRID,
RecentView::Icons,
crate::tr!("start", "view-icons"),
),
mode_button(
crate::ui::icons::PREVIEW,
RecentView::Preview,
crate::tr!("start", "view-preview"),
),
]
.spacing(2)
.align_y(iced::Center);
let body: Element<'a, Message> = if recents.is_empty() {
container(
@@ -3765,6 +4050,18 @@ pub(super) fn recent_files_panel<'a>(
)
.height(Fill)
.into()
} else if view != RecentView::List {
iced::widget::scrollable(
recent_tiles(recents, thumbs, view == RecentView::Preview).padding(
iced::Padding {
right: 12.0,
..iced::Padding::ZERO
},
),
)
.width(Fill)
.height(Fill)
.into()
} else {
// Right padding reserves a gutter for the scrollbar so it doesn't sit on
// top of the row's ✕ remove button.
@@ -3974,9 +4271,29 @@ mod tests {
&std::collections::HashMap::new(),
50,
"18446744073709551615",
super::super::RecentView::default(),
800.0,
std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
super::super::StartSection::default(),
);
}
}
/// The objects in a REFEDIT session's working set: the checked-out copies and
/// objects added with REFSET still in it, and everything created in the
/// space since.
// ponytail: walks the document per frame while a session is open; keep a
// running count on the session if large drawings feel it.
fn refedit_working_set_count(
session: &crate::modules::draw::modify::refedit::RefEditSession,
scene: &crate::scene::Scene,
) -> usize {
let space = scene.current_layout_block_handle_pub();
scene
.document
.entities()
.filter(|entity| {
entity.common().owner_handle == space && session.in_working_set(entity.common().handle)
})
.count()
}
+94 -8
View File
@@ -35,6 +35,13 @@ impl OpenCADStudio {
Some(K::PointCloudAttach) => crate::t!("Attach Point Cloud").into_owned(),
Some(K::PointCloudColorMap) => crate::t!("Point Cloud Color Map").into_owned(),
Some(K::PcSection) => crate::t!("Extract Section Lines from Point Cloud").into_owned(),
Some(K::ViewBackground) => format!(
"{} — {}",
crate::tr!("view-background", "title"),
self.view_background.as_ref().map_or("", |s| s.view.as_str())
),
Some(K::ImageAdjust) => crate::tr!("image-adjust", "title"),
Some(K::RefEdit) => crate::tr!("ref-edit", "dialog-title"),
Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(),
Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(),
Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(),
@@ -71,6 +78,9 @@ impl OpenCADStudio {
Some(K::ExternalChange) => crate::tr!("modal", "drawing-changed"),
Some(K::LayerDeleteWarning) => crate::tr!("modal", "delete-layer"),
Some(K::Unsaved) => crate::tr!("modal", "unsaved-changes"),
Some(K::BlockEditChanges) => crate::t!("Block Editor").into_owned(),
Some(K::BlockEditPicker) => crate::t!("Edit Block Definition").into_owned(),
Some(K::BlockSaveAs) => crate::t!("Save Block As").into_owned(),
Some(K::PointStyle) => crate::tr!("modal", "point-style"),
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
Some(K::SaveDialog) => crate::tr!("modal", "save-drawing-as"),
@@ -294,6 +304,10 @@ impl OpenCADStudio {
textfill: crate::scene::text::sdf_atlas::textfill(),
cliprompt_lines: self.cliprompt_lines,
commandline_fade_ms: self.commandline_fade_ms,
command_line_opacity: self.command_line.opacity_percent(),
show_menu_bar: self.show_menu_bar,
show_ribbon: !self.ribbon_closed,
hatch_associative: self.hatch_associative,
zoom_wheel_reversed: self.zoom_wheel_reversed,
touchpad_pan: self.touchpad_pan,
ui_scale_percent: self.ui_scale_percent,
@@ -488,6 +502,18 @@ impl OpenCADStudio {
crate::ui::window::block_definition::view_window(state, flow)
})
}
super::super::ModalKind::BlockEditPicker => {
let state = self.block_edit_picker.as_ref()?;
sized_flow(ex, 540, 380, |flow| {
crate::ui::window::block_edit_picker::view(state, flow)
})
}
super::super::ModalKind::BlockSaveAs => {
let state = self.block_save_as.as_ref()?;
sized_flow(ex, 430, 370, |flow| {
crate::ui::window::block_save_as::view(state, flow)
})
}
super::super::ModalKind::PdfAttach => {
let state = self.pdf_attach.as_ref()?;
sized_flow(ex, 880, 540, |flow| {
@@ -512,6 +538,24 @@ impl OpenCADStudio {
crate::ui::window::pdf_dialogs::view_pc_section(state, flow)
})
}
super::super::ModalKind::ViewBackground => {
let state = self.view_background.as_ref()?;
sized_flow(ex, 420, 560, |flow| {
crate::ui::window::pdf_dialogs::view_background(state, flow)
})
}
super::super::ModalKind::ImageAdjust => {
let state = self.image_adjust.as_ref()?;
sized_flow(ex, 520, 560, |flow| {
crate::ui::window::pdf_dialogs::image_adjust(state, flow)
})
}
super::super::ModalKind::RefEdit => {
let state = self.refedit_dialog.as_ref()?;
sized_flow(ex, 560, 470, |flow| {
crate::ui::window::refedit_dialog::view(state, flow)
})
}
super::super::ModalKind::UnderlayLayers => {
let state = self.underlay_layers.as_ref()?;
sized_flow(ex, 460, 520, |flow| {
@@ -1727,7 +1771,19 @@ impl OpenCADStudio {
.unwrap_or_default(),
None => String::new(),
};
automatic_flow(ex, |flow| unsaved_changes_dialog_window(&tab_name, flow))
automatic_flow(ex, |flow| {
unsaved_changes_dialog_window(&tab_name, flow, false)
})
}
super::super::ModalKind::BlockEditChanges => {
let name = self
.pending_block_close
.as_ref()
.and_then(|(id, _)| self.tabs.iter().find(|tab| tab.id == *id))
.and_then(|tab| tab.active_block_edit_session())
.map(|session| session.block_name.clone())
.unwrap_or_default();
automatic_flow(ex, |flow| unsaved_changes_dialog_window(&name, flow, true))
}
super::super::ModalKind::PointStyle => sized_flow(ex, 360, 470, |flow| {
crate::ui::style::point_style::view_window(
@@ -2045,23 +2101,53 @@ fn save_as_dialog_window<'a>(
fn unsaved_changes_dialog_window(
name: &str,
sizing: crate::ui::modal::ModalSizing,
block_edit: bool,
) -> Element<'static, Message> {
container(
column![
text(name.to_owned()).size(14),
text(if block_edit {
crate::tf!("Save changes to {}?", name).into_owned()
} else {
name.to_owned()
})
.size(14),
iced::widget::Space::new().height(4),
text(crate::tr!("modal", "unsaved-save-prompt"))
.size(12)
.style(dialog_muted_text_style),
text(if block_edit {
String::new()
} else {
crate::tr!("modal", "unsaved-save-prompt")
})
.size(12)
.style(dialog_muted_text_style),
iced::widget::Space::new().height(20),
row![
dialog_button(t!("Save"), Message::UnsavedDialogSave, button::primary),
dialog_button(
t!("Save"),
if block_edit {
Message::BlockEditChangesSave
} else {
Message::UnsavedDialogSave
},
button::primary
),
iced::widget::Space::new().width(8),
dialog_button(t!("Discard"), Message::UnsavedDialogDiscard, button::danger),
dialog_button(
t!("Discard"),
if block_edit {
Message::BlockEditChangesDiscard
} else {
Message::UnsavedDialogDiscard
},
button::danger
),
iced::widget::Space::new().width(8),
dialog_button(
t!("Cancel"),
Message::UnsavedDialogCancel,
if block_edit {
Message::BlockEditChangesCancel
} else {
Message::UnsavedDialogCancel
},
button::secondary
),
],
+105 -5
View File
@@ -1488,8 +1488,13 @@ pub(super) fn qselect_overlay<'a>(
candidate_count,
crate::ui::modal::ModalSizing::FILL,
);
let content =
crate::ui::modal::intrinsic(measurement, content, iced::Size::INFINITE, modal_resize);
// Long imported values must not make the natural dialog span the viewport.
let content = crate::ui::modal::intrinsic(
measurement,
content,
iced::Size::new(720.0, f32::INFINITY),
modal_resize,
);
crate::ui::modal::modal(
Space::new().width(Fill).height(Fill),
@@ -1501,6 +1506,15 @@ pub(super) fn qselect_overlay<'a>(
)
}
/// Object type label for the selection filter; INSERT reads as a block reference.
fn qselect_type_label(name: &str) -> String {
if name == "Insert" {
t!("Block Reference").into_owned()
} else {
t!(name).into_owned()
}
}
fn qselect_content<'a>(
state: &'a crate::app::QSelectState,
types: &[String],
@@ -1616,7 +1630,7 @@ fn qselect_content<'a>(
} else {
None
};
let error = state.error.clone().or(derived_error);
let error = state.error.clone().or(derived_error.clone());
let scope_options = vec![
crate::app::QSelectScope::CurrentSpace,
@@ -1635,10 +1649,72 @@ fn qselect_content<'a>(
let cancel = dialog_button(t!("Cancel"), Message::QSelectClose, false);
let apply = dialog_button_styled_opt(
t!("Apply"),
error.is_none().then_some(Message::QSelectApply),
(candidate_count > 0
&& state.editing_condition.is_none()
&& (state.conditions.is_empty() && error.is_none() || !state.conditions.is_empty()))
.then_some(Message::QSelectApply),
button::primary,
);
let mut condition_rows = column![].spacing(5);
if state.conditions.is_empty() {
condition_rows = condition_rows.push(text(t!("No conditions added.")).size(12));
}
for (index, condition) in state.conditions.iter().enumerate() {
let type_label = condition
.type_filter
.as_deref()
.map(qselect_type_label)
.unwrap_or_else(|| t!(QSELECT_ANY_TYPE).into_owned());
let property_label = condition
.property
.as_ref()
.map(|property| property.to_string())
.unwrap_or_else(|| t!(QSELECT_ANY_PROP).into_owned());
let summary = format!(
"{}. {} | {} | {} | {}",
index + 1,
type_label,
property_label,
condition.operator,
condition.value
);
condition_rows = condition_rows.push(
row![
// The measuring copy must not fill the viewport's available width.
text(summary).size(12).width(flex_width),
dialog_button(t!("Edit"), Message::QSelectEditCondition(index), false),
dialog_button(t!("Delete"), Message::QSelectRemoveCondition(index), false),
]
.spacing(6)
.align_y(iced::Alignment::Center),
);
}
let add_condition = dialog_button_styled_opt(
if state.editing_condition.is_some() {
t!("Update condition")
} else {
t!("Add condition")
},
(derived_error.is_none() && (state.type_filter.is_some() || state.property.is_some()))
.then_some(Message::QSelectAddCondition),
button::secondary,
);
let condition_actions = row![
add_condition,
dialog_button_styled_opt(
t!("Cancel edit"),
state.editing_condition.map(|_| Message::QSelectCancelEdit),
button::secondary
),
dialog_button_styled_opt(
t!("Clear conditions"),
(!state.conditions.is_empty()).then_some(Message::QSelectClearConditions),
button::secondary
),
]
.spacing(6);
let panel_body = column![
section_label(t!("Scope")),
Space::new().height(5),
@@ -1659,7 +1735,9 @@ fn qselect_content<'a>(
Space::new().height(5),
row![
label(t!("Object type:")),
iced::widget::pick_list(Some(type_sel), type_options, |value| t!(value).into_owned(),)
iced::widget::pick_list(Some(type_sel), type_options, |value| {
qselect_type_label(value)
})
.on_select(|s: String| {
if s == QSELECT_ANY_TYPE {
Message::QSelectSetType(None)
@@ -1706,6 +1784,28 @@ fn qselect_content<'a>(
Space::new().height(10),
container(rule::horizontal(1)).width(flex_width),
Space::new().height(10),
condition_actions,
Space::new().height(8),
dialog_button_styled_opt(
t!("Select object"),
state
.editing_condition
.is_none()
.then_some(Message::QSelectPickObject),
button::secondary,
),
Space::new().height(8),
section_label(t!("Conditions (all must match)")),
Space::new().height(5),
iced::widget::scrollable(condition_rows)
.height(120)
.width(sizing.width),
Space::new().height(5),
text(t!(
"When the list is nonempty, only listed conditions are applied."
))
.size(11),
Space::new().height(10),
section_label(t!("Result")),
Space::new().height(5),
column![