diff --git a/crates/openpencil-shell-core/src/document/mutators.rs b/crates/openpencil-shell-core/src/document/mutators.rs index fe7adc49d..6fd62b0c1 100644 --- a/crates/openpencil-shell-core/src/document/mutators.rs +++ b/crates/openpencil-shell-core/src/document/mutators.rs @@ -387,6 +387,18 @@ impl Document { } } + /// True when ANY widget occupies the right rail today — + /// PropertyPanel (gated on selection) or VariablesPanel + /// (gated on a non-empty var table). `canvas_region` uses + /// this to size the canvas so it doesn't paint over the + /// rail content (codex BLOCK: `no-selection VariablesPanel + /// is painted under the canvas` — without this gate the + /// canvas extended full-width when nothing was selected, + /// hiding the Variables panel). + pub fn right_rail_visible(&self) -> bool { + self.property_panel_visible() || !self.var_table.variables.is_empty() + } + /// Union of `aggregate_bounds` across selected nodes on the /// active page. Backs the multi-select panel's X/Y/W/H. pub fn selection_bounds(&self) -> Option { diff --git a/crates/openpencil-shell-core/src/document/tests_geometry.rs b/crates/openpencil-shell-core/src/document/tests_geometry.rs index e428befb6..7c16f7ecd 100644 --- a/crates/openpencil-shell-core/src/document/tests_geometry.rs +++ b/crates/openpencil-shell-core/src/document/tests_geometry.rs @@ -238,6 +238,34 @@ fn property_panel_visible_for_single_and_multi_selection() { assert!(doc.property_panel_visible()); } +#[test] +fn right_rail_visible_tracks_property_panel_and_variables() { + use crate::document::{Variable, VariableKind, VariableScalar, VariableValue}; + // No selection + empty var table → no right rail. + let mut doc = Document::empty(); + assert!(!doc.right_rail_visible()); + + // Variables-only document (typical for `.op` files with themes + // loaded but nothing selected on first paint) — rail occupied, + // canvas_region must shrink so the panel isn't painted over. + // Codex BLOCK: canvas previously extended full-width here. + doc.var_table.variables.push(Variable { + name: "color-1".into(), + kind: VariableKind::Color, + value: VariableValue::Scalar(VariableScalar::Str("#ff0000".into())), + }); + assert!(doc.right_rail_visible()); + + // Selection-only document → rail occupied (legacy gate path). + let mut doc2 = Document::sample(); + assert!(doc2.right_rail_visible()); + + // Clearing both selection + vars hides the rail again. + doc2.clear_selection(); + doc2.var_table.variables.clear(); + assert!(!doc2.right_rail_visible()); +} + #[test] fn property_panel_visible_hides_stale_single_anchor() { // Codex stop-hook BLOCK guard: the panel paint gate diff --git a/crates/openpencil-shell-native/src/widget_host/geometry.rs b/crates/openpencil-shell-native/src/widget_host/geometry.rs index 4991cc278..abaab2a74 100644 --- a/crates/openpencil-shell-native/src/widget_host/geometry.rs +++ b/crates/openpencil-shell-native/src/widget_host/geometry.rs @@ -257,8 +257,12 @@ impl WidgetHostNative { } else { 0.0 }; - let has_property = self.document.property_panel_visible(); - let canvas_right = if has_property { + // Any right-rail widget (PropertyPanel OR VariablesPanel) + // claims the rail width. Using `right_rail_visible` keeps + // canvas + rail mutually exclusive so the canvas never paints + // over an open Variables panel (codex BLOCK fix). + let rail_occupied = self.document.right_rail_visible(); + let canvas_right = if rail_occupied { viewport_w - self.document.ui.property_panel_width } else { viewport_w