feat(viewport): hide ViewCube then render bar as the viewport narrows
When the viewport got too narrow the top-left render-mode bar and the top-right ViewCube overlapped. Hide them adaptively, measuring the render bar's real width rather than guessing: - Wrap the render bar in DensitySwap, which measures its natural width every frame and swaps it for an empty spacer once it no longer fits the tile. Add report_width0 so it publishes that measured width to render_bar_w. - Compute viewcube_visible from that measured width: the cube shows only while bar_w + gap + cube region fits. This one flag drives the ViewCube nav/UCS widgets, the hover hit-test, and — threaded through build_viewports / build_viewport_for_pane / viewport_data_for — the GPU cube itself, which previously ignored the pane flag (show_viewcube was inst.active only), so the cube stayed while its surrounding widgets vanished. Result: the ViewCube gives way first, then the render bar once it too has no room — no overlap, sized to the bar's real width (which varies with the render-mode label, split buttons and tile count). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
07eb9dcb9a
commit
7cba5c8679
5 changed files with 128 additions and 27 deletions
|
|
@ -398,6 +398,11 @@ pub(super) struct OpenCADStudio {
|
|||
ucs_icon_at_origin: bool,
|
||||
/// Whether the ViewCube 3D gizmo is visible in model space (NAVVCUBE).
|
||||
show_viewcube: bool,
|
||||
/// Measured natural width (px, as `f32` bits) of the active viewport's
|
||||
/// render-mode control bar, written each frame by its `DensitySwap` and read
|
||||
/// next frame to decide whether the ViewCube still has room beside it — so
|
||||
/// the two corner widgets adapt to the bar's real width, not an estimate.
|
||||
render_bar_w: std::sync::Arc<std::sync::atomic::AtomicU32>,
|
||||
/// Whether the Properties panel is shown on the left (PROPERTIES).
|
||||
show_properties: bool,
|
||||
/// Whether the document file tabs are shown at the top (FILETAB).
|
||||
|
|
@ -2145,6 +2150,7 @@ impl OpenCADStudio {
|
|||
show_ucs_icon: true,
|
||||
ucs_icon_at_origin: true,
|
||||
show_viewcube: true,
|
||||
render_bar_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
||||
show_properties: true,
|
||||
show_file_tabs: true,
|
||||
show_layout_tabs: true,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use super::{Message, OpenCADStudio};
|
|||
use crate::scene::pick::grip::{grips_to_screen, grips_to_screen_paper, grips_to_screen_rte};
|
||||
use crate::scene::view::viewport_pane::ViewportPane;
|
||||
use crate::scene::{VIEWCUBE_PAD, VIEWCUBE_REGION_PX};
|
||||
use crate::ui::wrap_bar::DensitySwap;
|
||||
use crate::ui::wrap_bar::WrapFlow;
|
||||
use iced::widget::{
|
||||
button, canvas, column, container, mouse_area, pane_grid, responsive, row, shader, stack, text,
|
||||
|
|
@ -32,6 +33,18 @@ pub(in crate::app) use overlay::{MTEXT_TEXT_ID, TEXT_INLINE_ID};
|
|||
|
||||
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_REGION_PX;
|
||||
|
||||
/// Clear gap (px) kept between the render-mode bar (top-left) and the ViewCube
|
||||
/// (top-right) before the cube is judged to collide and hides.
|
||||
const VIEWCUBE_GAP: f32 = 12.0;
|
||||
|
||||
/// True when a viewport `tile_w` px wide still has room for the ViewCube beside
|
||||
/// a render-mode bar of measured width `bar_w`. When it doesn't, the cube hides
|
||||
/// first (the bar keeps priority); the bar itself hides separately, only when it
|
||||
/// no longer fits at all (its `DensitySwap`).
|
||||
fn viewcube_has_room(bar_w: f32, tile_w: f32) -> bool {
|
||||
bar_w + VIEWCUBE_GAP + VIEWCUBE_REGION_PX + VIEWCUBE_PAD <= tile_w
|
||||
}
|
||||
|
||||
/// `ViewportRenderMode` enum carries the raw DXF integers, not a label,
|
||||
/// so wrap it locally with a friendly name renderer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
|
@ -69,6 +82,30 @@ impl OpenCADStudio {
|
|||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
let is_paper = tab.scene.current_layout != "Model";
|
||||
// Adaptive corner widgets: the ViewCube shows only while the active
|
||||
// viewport is wide enough to hold it *beside* the render-mode bar, whose
|
||||
// real width is measured each frame by its `DensitySwap` and read back
|
||||
// here (from the previous frame). This drives the GPU cube (via the pane
|
||||
// flag), the ViewCube nav/UCS widgets, and the hover hit-test alike, so
|
||||
// they never overlap. The bar itself hides independently (its own
|
||||
// DensitySwap) only when it no longer fits at all.
|
||||
let render_bar_w =
|
||||
f32::from_bits(self.render_bar_w.load(std::sync::atomic::Ordering::Relaxed));
|
||||
let active_vp_w = if is_paper {
|
||||
tab.scene
|
||||
.active_viewport
|
||||
.and_then(|h| {
|
||||
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
||||
tab.scene.viewport_screen_rect(h, (cw, ch))
|
||||
})
|
||||
.map(|r| r.width)
|
||||
.unwrap_or(f32::INFINITY)
|
||||
} else {
|
||||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||
tab.scene.active_model_tile_bounds(vw, vh).width
|
||||
};
|
||||
let viewcube_visible =
|
||||
self.show_viewcube && !tab.is_start && viewcube_has_room(render_bar_w, active_vp_w);
|
||||
// Start tab: render welcome page in place of the viewport.
|
||||
// Surrounding chrome (tab bar, status bar) stays; the welcome widget
|
||||
// returned here also flags the rest of `view` to skip drawing-only
|
||||
|
|
@ -89,7 +126,7 @@ impl OpenCADStudio {
|
|||
} else if is_paper {
|
||||
shader(ViewportPane::model(
|
||||
&tab.scene,
|
||||
self.show_viewcube,
|
||||
viewcube_visible,
|
||||
tab.render_mode,
|
||||
))
|
||||
.width(Fill)
|
||||
|
|
@ -106,7 +143,7 @@ impl OpenCADStudio {
|
|||
// current (building the pane_grid inside `responsive` resets the
|
||||
// mouse_areas' hover state and drops their move events).
|
||||
let scene = &tab.scene;
|
||||
let show_viewcube = self.show_viewcube;
|
||||
let show_viewcube = viewcube_visible;
|
||||
let render_mode = tab.render_mode;
|
||||
let size_probe: Element<'_, Message> = responsive(move |size| {
|
||||
{
|
||||
|
|
@ -468,7 +505,8 @@ impl OpenCADStudio {
|
|||
ost_points,
|
||||
otrack_line,
|
||||
parallel_ref_marker,
|
||||
!is_paper && self.show_viewcube,
|
||||
// ViewCube hover region matches the drawn cube — gone when hidden.
|
||||
!is_paper && viewcube_visible,
|
||||
dividers,
|
||||
pane_move_rect,
|
||||
pane_drop_rect,
|
||||
|
|
@ -657,6 +695,8 @@ impl OpenCADStudio {
|
|||
// viewport drawing / selection is unaffected. In a paper layout
|
||||
// the active viewport gets its own picker (below) instead.
|
||||
if !is_paper && !tab.is_start {
|
||||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||
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 bar = viewport_controls(
|
||||
|
|
@ -666,16 +706,28 @@ impl OpenCADStudio {
|
|||
true,
|
||||
tab.scene.model_tiles.borrow().len(),
|
||||
);
|
||||
// Position the bar at the active model tile's top-left corner so
|
||||
// it follows the active panel in a tiled layout (full canvas when
|
||||
// a single tile fills the window). Leading Spaces offset it.
|
||||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||
let rect = tab.scene.active_model_tile_bounds(vw, vh);
|
||||
// Adaptive: DensitySwap measures the bar's real width every frame
|
||||
// (reported into `render_bar_w`, which the ViewCube reads to decide
|
||||
// overlap) and swaps it for an empty spacer only when it no longer
|
||||
// fits the tile. The fixed-width container bounds that fit decision
|
||||
// to the tile, not the whole canvas.
|
||||
let adaptive: Element<'_, Message> = DensitySwap::new(vec![
|
||||
iced::widget::opaque(bar),
|
||||
Space::new()
|
||||
.width(iced::Length::Fixed(0.0))
|
||||
.height(iced::Length::Fixed(0.0))
|
||||
.into(),
|
||||
])
|
||||
.report_width0(self.render_bar_w.clone())
|
||||
.into();
|
||||
// Position the bar at the active model tile's top-left corner so it
|
||||
// follows the active panel in a tiled layout (full canvas when a
|
||||
// single tile fills the window). Leading Spaces offset it.
|
||||
let bar_layer = column![
|
||||
Space::new().height(iced::Length::Fixed(rect.y.max(0.0))),
|
||||
row![
|
||||
Space::new().width(iced::Length::Fixed(rect.x.max(0.0))),
|
||||
iced::widget::opaque(bar),
|
||||
container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))),
|
||||
],
|
||||
]
|
||||
.width(Fill)
|
||||
|
|
@ -740,24 +792,38 @@ impl OpenCADStudio {
|
|||
.scene
|
||||
.active_viewport_render_mode()
|
||||
.unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D);
|
||||
// Adaptive (same as model): the picker measures its real width into
|
||||
// `render_bar_w` and swaps to an empty spacer only when the viewport
|
||||
// can't hold it; the ViewCube reads that width to decide overlap.
|
||||
let bar = viewport_controls(
|
||||
vp_mode,
|
||||
self.show_grid,
|
||||
self.snapper.grid_snap(),
|
||||
false,
|
||||
0,
|
||||
);
|
||||
let adaptive: Element<'_, Message> = DensitySwap::new(vec![
|
||||
iced::widget::opaque(bar),
|
||||
Space::new()
|
||||
.width(iced::Length::Fixed(0.0))
|
||||
.height(iced::Length::Fixed(0.0))
|
||||
.into(),
|
||||
])
|
||||
.report_width0(self.render_bar_w.clone())
|
||||
.into();
|
||||
let picker_layer = column![
|
||||
Space::new().height(iced::Length::Fixed(y + 4.0)),
|
||||
row![
|
||||
Space::new().width(iced::Length::Fixed(x + 4.0)),
|
||||
iced::widget::opaque(viewport_controls(
|
||||
vp_mode,
|
||||
self.show_grid,
|
||||
self.snapper.grid_snap(),
|
||||
false,
|
||||
0,
|
||||
)),
|
||||
container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))),
|
||||
],
|
||||
]
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
viewport_stack = viewport_stack.push(picker_layer);
|
||||
|
||||
if self.show_viewcube {
|
||||
// Hide the ViewCube first — before the render bar — when they collide.
|
||||
if viewcube_visible {
|
||||
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
||||
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
||||
|
||||
|
|
@ -799,7 +865,7 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
|
||||
if self.show_viewcube && !is_paper && !tab.is_start {
|
||||
if viewcube_visible && !is_paper {
|
||||
// Place the ViewCube hit area in the active model tile's top-right
|
||||
// corner so it tracks the active panel in a tiled layout. The hit
|
||||
// test in update.rs already maps clicks through the active tile.
|
||||
|
|
|
|||
|
|
@ -846,6 +846,7 @@ impl Scene {
|
|||
bounds: Rectangle,
|
||||
model_render_mode: acadrust::entities::ViewportRenderMode,
|
||||
_hover_region: Option<usize>,
|
||||
show_viewcube: bool,
|
||||
) -> Primitive {
|
||||
// Hover comes from the scene cell driven by the app-level
|
||||
// `CursorMoved` handler — the cube overlay sits above the shader
|
||||
|
|
@ -864,7 +865,7 @@ impl Scene {
|
|||
let bg_color = [0.0, 0.0, 0.0, 0.0];
|
||||
let viewports: Vec<ViewportData> = instances
|
||||
.iter()
|
||||
.filter_map(|inst| self.viewport_data_for(inst, canvas, hover_region))
|
||||
.filter_map(|inst| self.viewport_data_for(inst, canvas, hover_region, show_viewcube))
|
||||
.collect();
|
||||
// Empty viewports → blit nothing; the container background (model bg
|
||||
// or the paper desk colour) stays visible.
|
||||
|
|
@ -886,6 +887,7 @@ impl Scene {
|
|||
bounds: Rectangle,
|
||||
tile_idx: usize,
|
||||
model_render_mode: acadrust::entities::ViewportRenderMode,
|
||||
show_viewcube: bool,
|
||||
) -> Primitive {
|
||||
let hover_region = self.viewcube_hover.get();
|
||||
let canvas = (bounds.width.max(1.0), bounds.height.max(1.0));
|
||||
|
|
@ -926,7 +928,7 @@ impl Scene {
|
|||
paper_sheet: false,
|
||||
};
|
||||
let viewports = self
|
||||
.viewport_data_for(&inst, canvas, hover_region)
|
||||
.viewport_data_for(&inst, canvas, hover_region, show_viewcube)
|
||||
.into_iter()
|
||||
.collect();
|
||||
Primitive {
|
||||
|
|
@ -945,6 +947,7 @@ impl Scene {
|
|||
inst: &ViewportInstance,
|
||||
canvas: (f32, f32),
|
||||
hover_region: Option<usize>,
|
||||
show_viewcube: bool,
|
||||
) -> Option<ViewportData> {
|
||||
let flags = render_mode_flags(inst.render_mode);
|
||||
let view_wireframe = !flags.face3d_fill;
|
||||
|
|
@ -1159,7 +1162,10 @@ impl Scene {
|
|||
compass_rotation: inst.camera.view_rotation_mat(),
|
||||
// Only the active viewport gets the hovered-region highlight.
|
||||
hover_region: if inst.active { hover_region } else { None },
|
||||
show_viewcube: inst.active,
|
||||
// The cube shows only on the active viewport, and only while the
|
||||
// caller (the widget) says there is room for it beside the render
|
||||
// bar — so it hides adaptively when the viewport gets narrow.
|
||||
show_viewcube: inst.active && show_viewcube,
|
||||
fill_mode: self.document.header.fill_mode,
|
||||
view_wireframe,
|
||||
mesh_fill: flags.mesh_fill,
|
||||
|
|
|
|||
|
|
@ -69,12 +69,18 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a>
|
|||
bounds: Rectangle,
|
||||
) -> Self::Primitive {
|
||||
match self.pane {
|
||||
Some(idx) => self
|
||||
.scene
|
||||
.build_viewport_for_pane(bounds, idx, self.render_mode),
|
||||
None => self
|
||||
.scene
|
||||
.build_viewports(bounds, self.render_mode, state.hover_region),
|
||||
Some(idx) => self.scene.build_viewport_for_pane(
|
||||
bounds,
|
||||
idx,
|
||||
self.render_mode,
|
||||
self.show_viewcube,
|
||||
),
|
||||
None => self.scene.build_viewports(
|
||||
bounds,
|
||||
self.render_mode,
|
||||
state.hover_region,
|
||||
self.show_viewcube,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -670,6 +670,11 @@ pub struct DensitySwap<'a> {
|
|||
variants: Vec<Element<'a, Message>>,
|
||||
chosen: Cell<usize>,
|
||||
height_out: Option<Arc<AtomicU32>>,
|
||||
/// Receives the FIRST (widest) variant's natural width in bits of an `f32`,
|
||||
/// measured every layout regardless of which variant is shown — so a caller
|
||||
/// can place a neighbouring widget relative to the full-size content even
|
||||
/// while a narrower variant is on screen.
|
||||
width0_out: Option<Arc<AtomicU32>>,
|
||||
}
|
||||
|
||||
impl<'a> DensitySwap<'a> {
|
||||
|
|
@ -678,6 +683,7 @@ impl<'a> DensitySwap<'a> {
|
|||
variants,
|
||||
chosen: Cell::new(0),
|
||||
height_out: None,
|
||||
width0_out: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -687,6 +693,12 @@ impl<'a> DensitySwap<'a> {
|
|||
self.height_out = Some(out);
|
||||
self
|
||||
}
|
||||
|
||||
/// Report the first variant's natural (unconstrained) width — see `width0_out`.
|
||||
pub fn report_width0(mut self, out: Arc<AtomicU32>) -> Self {
|
||||
self.width0_out = Some(out);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget<Message, Theme, Renderer> for DensitySwap<'a> {
|
||||
|
|
@ -718,6 +730,11 @@ impl<'a> Widget<Message, Theme, Renderer> for DensitySwap<'a> {
|
|||
let mut pick = self.variants.len().saturating_sub(1);
|
||||
for (i, v) in self.variants.iter_mut().enumerate() {
|
||||
let n = v.as_widget_mut().layout(&mut tree.children[i], renderer, &natural);
|
||||
if i == 0 {
|
||||
if let Some(out) = &self.width0_out {
|
||||
out.store(n.size().width.to_bits(), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
if n.size().width <= max_w {
|
||||
pick = i;
|
||||
break;
|
||||
|
|
|
|||
Loading…
Reference in a new issue