forked from 1b-agent/opencadstudio-upstream
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:
231 files changed
+32217
-3423
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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![
|
||||
|
||||
Reference in new issue
Block a user