From 2a808c45260474a9ad0f1430b7e11c79c6a1d948 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:05:01 +0300 Subject: [PATCH] Background dialog preview: mirrored images, device pixels; faint inactive values Co-Authored-By: Claude Opus 5.5 --- src/app/commands/view_presets.rs | 6 ++-- src/scene/view/mod.rs | 1 + src/scene/view/render.rs | 9 ++++++ src/ui/window/pdf_dialogs.rs | 54 ++++++++++++++++++++++++++++---- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/app/commands/view_presets.rs b/src/app/commands/view_presets.rs index 879198625..0ce257051 100644 --- a/src/app/commands/view_presets.rs +++ b/src/app/commands/view_presets.rs @@ -453,7 +453,7 @@ impl OpenCADStudio { offset: ["0".into(), "0".into()], scale: ["1".into(), "1".into()], scale_base: [1.0, 1.0], - image_size: None, + preview: None, viewport: { let (width, height) = self.tabs[i].scene.selection.borrow().view.vp_size; [width as f32, height as f32] @@ -487,7 +487,7 @@ impl OpenCADStudio { state.offset = [value.offset.x, value.offset.y].map(crate::ui::window::pdf_dialogs::short); state.scale = [value.scale.x, value.scale.y].map(crate::ui::window::pdf_dialogs::short); state.scale_base = [value.scale.x, value.scale.y]; - state.image_size = image::image_dimensions(&value.filename).ok().map(|(w, h)| [w, h]); + state.preview = crate::ui::window::pdf_dialogs::bg_preview(&value.filename); } _ => {} } @@ -513,7 +513,7 @@ impl OpenCADStudio { M::BgThree(on) => state.three = on, M::BgRotation(value) => state.rotation = value, M::BgImage(value) => { - state.image_size = image::image_dimensions(value.trim()).ok().map(|(w, h)| [w, h]); + state.preview = crate::ui::window::pdf_dialogs::bg_preview(&value); state.image = value; } M::BgPosition(value) => state.position = value, diff --git a/src/scene/view/mod.rs b/src/scene/view/mod.rs index cae922acb..e8cef8c99 100644 --- a/src/scene/view/mod.rs +++ b/src/scene/view/mod.rs @@ -3,3 +3,4 @@ pub mod dispatch; pub mod render; pub mod transform; pub mod viewport_pane; +pub use render::device_scale; diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 442f6415a..9cb39f5c5 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -385,6 +385,7 @@ impl shader::Primitive for Primitive { pipeline.gpu_error_epoch = errors_at_entry; } let scale = viewport.scale_factor() as f32; + DEVICE_SCALE.store(scale.to_bits(), std::sync::atomic::Ordering::Relaxed); let instance_ids: Vec = self.viewports.iter().map(|vp| vp.instance_id).collect(); let slots = pipeline.resolve_slots(device, queue, &instance_ids); for (i, vp) in self.viewports.iter().enumerate() { @@ -5440,3 +5441,11 @@ mod layer0_inherit_tests { assert!(scene.document_render_env_cache.borrow().is_some()); } } + +/// The display's device pixels per logical pixel, as the viewport last drew. +static DEVICE_SCALE: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0x3f80_0000); + +/// Device pixels per logical pixel of the drawing viewport. +pub fn device_scale() -> f32 { + f32::from_bits(DEVICE_SCALE.load(std::sync::atomic::Ordering::Relaxed)) +} diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index 72869aad3..a9d83eeff 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -2185,6 +2185,25 @@ impl fmt::Display for BgPosition { } } +/// A background image as the preview draws it. +pub struct BgPreview { + pub size: [u32; 2], + /// Unmirrored, mirrored in x, in y, in both. + pub handles: [image::Handle; 4], +} + +/// Loads a background image for the preview, at most 512 pixels a side. +pub fn bg_preview(path: &str) -> Option { + let decoded = ::image::open(path.trim()).ok()?; + let size = [decoded.width(), decoded.height()]; + let small = if size[0].max(size[1]) > 512 { decoded.thumbnail(512, 512) } else { decoded }.to_rgba8(); + let handle = |pixels: ::image::RgbaImage| image::Handle::from_rgba(pixels.width(), pixels.height(), pixels.into_raw()); + let flip_x = ::image::imageops::flip_horizontal(&small); + let flip_y = ::image::imageops::flip_vertical(&small); + let flip_xy = ::image::imageops::flip_vertical(&flip_x); + Some(BgPreview { size, handles: [handle(small), handle(flip_x), handle(flip_y), handle(flip_xy)] }) +} + /// Image offsets the reference's Adjust Image sliders reach, in pixels. pub const BG_OFFSET_LIMIT: f64 = 2000.0; /// Image scales the reference's sliders reach (slider value / 10). @@ -2222,8 +2241,9 @@ pub struct ViewBackgroundState { pub offset: [String; 2], pub scale: [String; 2], pub scale_base: [f64; 2], - /// The chosen image's size in pixels, for the preview. - pub image_size: Option<[u32; 2]>, + /// The chosen image for the preview: its size in pixels and a small copy + /// as it is and mirrored (x, y, both), for negative scales. + pub preview: Option, /// The drawing viewport's size, which the preview stands for. pub viewport: [f32; 2], /// The drawing area colour, shown for no background. @@ -2285,12 +2305,15 @@ pub fn view_background<'a>( } BgKind::Image => { if std::path::Path::new(state.image.trim()).is_file() { - let handle = image::Handle::from_path(state.image.trim()); let canvas = Background::Color(rgb(state.canvas)); if state.position == BgPosition::Stretch { // Filling the view; keeping the aspect ratio it covers // the view and the overflow is cut off. let fit = if state.aspect { iced::ContentFit::Cover } else { iced::ContentFit::Fill }; + let handle = state.preview.as_ref().map_or_else( + || image::Handle::from_path(state.image.trim()), + |preview| preview.handles[0].clone(), + ); container(image(handle).content_fit(fit).width(Fill).height(Fill)) .center(Fill) .clip(true) @@ -2305,11 +2328,20 @@ pub fn view_background<'a>( let v = parse(&state.scale[k], state.scale_base[k]); if v.abs() < 1e-9 { state.scale_base[k] } else { v } }); - let [iw, ih] = state.image_size.unwrap_or([1, 1]); + let [iw, ih] = state.preview.as_ref().map_or([1, 1], |preview| preview.size); + // A negative scale mirrors the image. + let mirrored = usize::from(scale[0] < 0.0) | usize::from(scale[1] < 0.0) << 1; + let handle = state.preview.as_ref().map_or_else( + || image::Handle::from_path(state.image.trim()), + |preview| preview.handles[mirrored].clone(), + ); let viewport = state.viewport; + // The image's pixels and offsets are device pixels; the + // viewport is measured in logical ones. + let device = crate::scene::view::device_scale().max(0.1); let tile = state.position == BgPosition::Tile; container(iced::widget::responsive(move |size| { - let factor = size.height / viewport[1].max(1.0); + let factor = size.height / viewport[1].max(1.0) / device; let w = (iw as f32 * scale[0].abs() as f32 * factor).max(1.0); let h = (ih as f32 * scale[1].abs() as f32 * factor).max(1.0); let x = size.width / 2.0 + offset[0] as f32 * factor - w / 2.0; @@ -2502,7 +2534,17 @@ fn adjust_card<'a>(state: &'a ViewBackgroundState) -> Element<'a, Message> { }; let spin = |field: u8| -> Element<'a, Message> { let value = if field < 2 { &state.offset[field as usize] } else { &state.scale[field as usize - 2] }; - let mut input = text_input("", value).size(11).padding([4, 8]).width(Length::Fixed(64.0)).style(field_style); + let mut input = text_input("", value) + .size(11) + .padding([4, 8]) + .width(Length::Fixed(64.0)) + .style(move |theme: &Theme, status| { + let mut style = field_style(theme, status); + if !enabled { + style.value = theme.palette().background.strong.color; + } + style + }); if enabled { input = input.on_input(move |v| { if field < 2 {