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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-10 14:58:48 +03:00
1 parent 07eb9dcb9a
commit 7cba5c8679
5 files changed
+128 -27

No files matched your search

+6
View File
@@ -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,
+84 -18
View File
@@ -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.