diff --git a/src/core/window.rs b/src/core/window.rs index 9c4974ed..c38f9c74 100644 --- a/src/core/window.rs +++ b/src/core/window.rs @@ -12,6 +12,80 @@ pub enum SplitDirection { Vertical, // split left/right } +/// Map vimcode's `SplitDirection` (`Horizontal` = stacked top/bottom) to +/// quadraui's `primitives::split_tree::SplitDirection` (`Horizontal` = +/// side-by-side) — the two crates name the same divider orientation +/// oppositely (see that module's doc comment). Centralised here so +/// `WindowLayout`/`GroupLayout`'s `SplitTree` conversion and `render.rs`'s +/// `divider_to_split` (#818) share one mapping instead of two hand-written +/// copies of the same 2-arm match drifting apart. +pub(crate) fn to_quadraui_direction(d: SplitDirection) -> quadraui::SplitDirection { + match d { + SplitDirection::Vertical => quadraui::SplitDirection::Horizontal, + SplitDirection::Horizontal => quadraui::SplitDirection::Vertical, + } +} + +/// Inverse of [`to_quadraui_direction`]. +pub(crate) fn from_quadraui_direction(d: quadraui::SplitDirection) -> SplitDirection { + match d { + quadraui::SplitDirection::Horizontal => SplitDirection::Vertical, + quadraui::SplitDirection::Vertical => SplitDirection::Horizontal, + } +} + +/// Encode a [`WindowId`] as a `quadraui::WidgetId` for round-tripping +/// through `quadraui::SplitTree` — the id only ever needs to survive one +/// `to_split_tree` → `layout` round trip within a single call, never +/// painted or persisted, so the encoding is an implementation detail. +fn window_widget_id(id: WindowId) -> quadraui::WidgetId { + quadraui::WidgetId::new(format!("w{}", id.0)) +} + +/// Panics if `id` isn't a well-formed `"w{usize}"` — the only strings +/// [`window_widget_id`] ever produces, and the only producer `SplitTree` +/// ever hands back to this function within a single `to_split_tree` → +/// `layout` round trip. A parse failure here means `quadraui::SplitTree` +/// mutated or fabricated a `WidgetId` this code never gave it, which would +/// otherwise silently resolve to window `0` — a wrong-window bug far harder +/// to spot than a panic naming the offending id. +fn window_id_from_widget(id: &quadraui::WidgetId) -> WindowId { + WindowId( + id.as_str()[1..] + .parse() + .unwrap_or_else(|e| panic!("malformed window WidgetId {id:?} from SplitTree: {e}")), + ) +} + +/// See [`window_widget_id`]. +fn group_widget_id(id: GroupId) -> quadraui::WidgetId { + quadraui::WidgetId::new(format!("g{}", id.0)) +} + +/// See [`window_id_from_widget`] — same contract, for `GroupId`. +fn group_id_from_widget(id: &quadraui::WidgetId) -> GroupId { + GroupId( + id.as_str()[1..] + .parse() + .unwrap_or_else(|e| panic!("malformed group WidgetId {id:?} from SplitTree: {e}")), + ) +} + +/// Convert a resolved `quadraui::SplitTreeDivider` back into vimcode's +/// [`GroupDivider`] (f32 → f64, direction re-mapped via +/// [`from_quadraui_direction`]). +fn group_divider_from_split_tree(d: &quadraui::SplitTreeDivider) -> GroupDivider { + GroupDivider { + split_index: d.split_index, + direction: from_quadraui_direction(d.direction), + position: d.position as f64, + axis_start: d.axis_start as f64, + axis_size: d.axis_size as f64, + cross_start: d.cross_start as f64, + cross_size: d.cross_size as f64, + } +} + /// A window is a viewport into a buffer. /// Multiple windows can display the same buffer with independent cursors/scroll. #[derive(Debug, Clone)] @@ -205,53 +279,52 @@ impl WindowRect { } } +impl From for quadraui::Rect { + fn from(r: WindowRect) -> Self { + quadraui::Rect::new(r.x as f32, r.y as f32, r.width as f32, r.height as f32) + } +} + +impl From for WindowRect { + fn from(r: quadraui::Rect) -> Self { + WindowRect::new(r.x as f64, r.y as f64, r.width as f64, r.height as f64) + } +} + impl WindowLayout { - /// Calculate the pixel rectangles for each window in the layout. - pub fn calculate_rects(&self, bounds: WindowRect) -> Vec<(WindowId, WindowRect)> { + /// Convert to a `quadraui::SplitTree` so `calculate_rects`/`dividers` + /// can compute leaf rects and divider geometry in `SplitTree::layout`'s + /// single recursive pass, rather than two hand-rolled passes over this + /// tree that could (and per #582/#452, once did) diverge (#818). + fn to_split_tree(&self) -> quadraui::SplitTree { match self { - WindowLayout::Leaf(id) => vec![(*id, bounds)], + WindowLayout::Leaf(id) => quadraui::SplitTree::leaf(window_widget_id(*id)), WindowLayout::Split { direction, ratio, first, second, - } => { - let (first_bounds, second_bounds) = match direction { - SplitDirection::Horizontal => { - let first_height = bounds.height * ratio; - let second_height = bounds.height - first_height; - ( - WindowRect::new(bounds.x, bounds.y, bounds.width, first_height), - WindowRect::new( - bounds.x, - bounds.y + first_height, - bounds.width, - second_height, - ), - ) - } - SplitDirection::Vertical => { - let first_width = bounds.width * ratio; - let second_width = bounds.width - first_width; - ( - WindowRect::new(bounds.x, bounds.y, first_width, bounds.height), - WindowRect::new( - bounds.x + first_width, - bounds.y, - second_width, - bounds.height, - ), - ) - } - }; - - let mut rects = first.calculate_rects(first_bounds); - rects.extend(second.calculate_rects(second_bounds)); - rects - } + } => quadraui::SplitTree::split( + to_quadraui_direction(*direction), + *ratio as f32, + first.to_split_tree(), + second.to_split_tree(), + ), } } + /// Calculate the pixel rectangles for each window in the layout. + pub fn calculate_rects(&self, bounds: WindowRect) -> Vec<(WindowId, WindowRect)> { + let layout = self + .to_split_tree() + .layout(bounds.into(), quadraui::SplitTreeMeasure::new(0.0)); + layout + .leaves + .into_iter() + .map(|(id, rect)| (window_id_from_widget(&id), rect.into())) + .collect() + } + /// Collect all split dividers with pre-order `split_index`. /// /// Mirrors `GroupLayout::dividers` (used by editor-group splits) — see @@ -259,67 +332,22 @@ impl WindowLayout { /// group-specific fields, so it's reused here for window-split dividers /// too (backends wrap it in `WindowDivider` when they need to know which /// editor group's tab a divider belongs to). + /// + /// `counter` is only ever called with `&mut 0` by every caller in this + /// codebase (a `WindowLayout` is never itself nested inside a larger + /// numbered tree) — it is advanced here purely to preserve the pre-#818 + /// signature for any future caller that does thread a running counter + /// through. pub fn dividers(&self, bounds: WindowRect, counter: &mut usize) -> Vec { - match self { - WindowLayout::Leaf(_) => vec![], - WindowLayout::Split { - direction, - ratio, - first, - second, - } => { - let idx = *counter; - *counter += 1; - let divider = match direction { - SplitDirection::Vertical => { - let pos = bounds.x + bounds.width * ratio; - GroupDivider { - split_index: idx, - direction: *direction, - position: pos, - axis_start: bounds.x, - axis_size: bounds.width, - cross_start: bounds.y, - cross_size: bounds.height, - } - } - SplitDirection::Horizontal => { - let pos = bounds.y + bounds.height * ratio; - GroupDivider { - split_index: idx, - direction: *direction, - position: pos, - axis_start: bounds.y, - axis_size: bounds.height, - cross_start: bounds.x, - cross_size: bounds.width, - } - } - }; - let (first_bounds, second_bounds) = match direction { - SplitDirection::Horizontal => { - let first_h = bounds.height * ratio; - let second_h = bounds.height - first_h; - ( - WindowRect::new(bounds.x, bounds.y, bounds.width, first_h), - WindowRect::new(bounds.x, bounds.y + first_h, bounds.width, second_h), - ) - } - SplitDirection::Vertical => { - let first_w = bounds.width * ratio; - let second_w = bounds.width - first_w; - ( - WindowRect::new(bounds.x, bounds.y, first_w, bounds.height), - WindowRect::new(bounds.x + first_w, bounds.y, second_w, bounds.height), - ) - } - }; - let mut divs = vec![divider]; - divs.extend(first.dividers(first_bounds, counter)); - divs.extend(second.dividers(second_bounds, counter)); - divs - } - } + let layout = self + .to_split_tree() + .layout(bounds.into(), quadraui::SplitTreeMeasure::new(0.0)); + *counter += layout.dividers.len(); + layout + .dividers + .iter() + .map(group_divider_from_split_tree) + .collect() } /// Find the Nth split node in pre-order and set its ratio (clamped to 0.1..0.9). @@ -663,6 +691,25 @@ impl GroupLayout { ids.get(n).copied() } + /// Convert to a `quadraui::SplitTree` — see `WindowLayout::to_split_tree` + /// for why (#818). + fn to_split_tree(&self) -> quadraui::SplitTree { + match self { + GroupLayout::Leaf(id) => quadraui::SplitTree::leaf(group_widget_id(*id)), + GroupLayout::Split { + direction, + ratio, + first, + second, + } => quadraui::SplitTree::split( + to_quadraui_direction(*direction), + *ratio as f32, + first.to_split_tree(), + second.to_split_tree(), + ), + } + } + /// Calculate the pixel rectangles for each group in the layout. /// Each leaf gets `y += tab_bar_height, height -= tab_bar_height` to reserve /// space for the tab bar drawn at the top of each group. @@ -671,111 +718,41 @@ impl GroupLayout { bounds: WindowRect, tab_bar_height: f64, ) -> Vec<(GroupId, WindowRect)> { - match self { - GroupLayout::Leaf(id) => { - vec![( - *id, + let layout = self + .to_split_tree() + .layout(bounds.into(), quadraui::SplitTreeMeasure::new(0.0)); + layout + .leaves + .into_iter() + .map(|(id, rect)| { + let r: WindowRect = rect.into(); + ( + group_id_from_widget(&id), WindowRect::new( - bounds.x, - bounds.y + tab_bar_height, - bounds.width, - (bounds.height - tab_bar_height).max(0.0), + r.x, + r.y + tab_bar_height, + r.width, + (r.height - tab_bar_height).max(0.0), ), - )] - } - GroupLayout::Split { - direction, - ratio, - first, - second, - } => { - let (first_bounds, second_bounds) = match direction { - SplitDirection::Horizontal => { - let first_h = bounds.height * ratio; - let second_h = bounds.height - first_h; - ( - WindowRect::new(bounds.x, bounds.y, bounds.width, first_h), - WindowRect::new(bounds.x, bounds.y + first_h, bounds.width, second_h), - ) - } - SplitDirection::Vertical => { - let first_w = bounds.width * ratio; - let second_w = bounds.width - first_w; - ( - WindowRect::new(bounds.x, bounds.y, first_w, bounds.height), - WindowRect::new(bounds.x + first_w, bounds.y, second_w, bounds.height), - ) - } - }; - let mut rects = first.calculate_group_rects(first_bounds, tab_bar_height); - rects.extend(second.calculate_group_rects(second_bounds, tab_bar_height)); - rects - } - } + ) + }) + .collect() } /// Collect all split dividers with pre-order `split_index`. + /// + /// See `WindowLayout::dividers`'s doc for why `counter` is only ever + /// `&mut 0` in practice. pub fn dividers(&self, bounds: WindowRect, counter: &mut usize) -> Vec { - match self { - GroupLayout::Leaf(_) => vec![], - GroupLayout::Split { - direction, - ratio, - first, - second, - } => { - let idx = *counter; - *counter += 1; - let divider = match direction { - SplitDirection::Vertical => { - let pos = bounds.x + bounds.width * ratio; - GroupDivider { - split_index: idx, - direction: *direction, - position: pos, - axis_start: bounds.x, - axis_size: bounds.width, - cross_start: bounds.y, - cross_size: bounds.height, - } - } - SplitDirection::Horizontal => { - let pos = bounds.y + bounds.height * ratio; - GroupDivider { - split_index: idx, - direction: *direction, - position: pos, - axis_start: bounds.y, - axis_size: bounds.height, - cross_start: bounds.x, - cross_size: bounds.width, - } - } - }; - let (first_bounds, second_bounds) = match direction { - SplitDirection::Horizontal => { - let first_h = bounds.height * ratio; - let second_h = bounds.height - first_h; - ( - WindowRect::new(bounds.x, bounds.y, bounds.width, first_h), - WindowRect::new(bounds.x, bounds.y + first_h, bounds.width, second_h), - ) - } - SplitDirection::Vertical => { - let first_w = bounds.width * ratio; - let second_w = bounds.width - first_w; - ( - WindowRect::new(bounds.x, bounds.y, first_w, bounds.height), - WindowRect::new(bounds.x + first_w, bounds.y, second_w, bounds.height), - ) - } - }; - let mut divs = vec![divider]; - divs.extend(first.dividers(first_bounds, counter)); - divs.extend(second.dividers(second_bounds, counter)); - divs - } - } + let layout = self + .to_split_tree() + .layout(bounds.into(), quadraui::SplitTreeMeasure::new(0.0)); + *counter += layout.dividers.len(); + layout + .dividers + .iter() + .map(group_divider_from_split_tree) + .collect() } /// Find the Nth split node in pre-order and set its ratio (clamped to 0.1..0.9). diff --git a/src/gtk/testing.rs b/src/gtk/testing.rs index a62e4628..031a41a4 100644 --- a/src/gtk/testing.rs +++ b/src/gtk/testing.rs @@ -657,6 +657,103 @@ mod tests { ); } + /// #818: two nested `:vsplit`s paint **two** window-divider lines whose + /// `split_index` differs (0 for the outer split, 1 for the one nested + /// inside its second window). This is the exact shape #582/#452 warned + /// two independently hand-rolled recursive passes over `WindowLayout` + /// (`calculate_rects` and `dividers`) could number or position + /// inconsistently — #818 replaced both passes with the leaves/dividers + /// `quadraui::SplitTree::layout` computes together in one pass. Dragging + /// only the inner divider must move exactly that line and leave the + /// outer, sibling divider's column untouched; a split_index/rect mixup + /// reintroduced by a future change to `WindowLayout::to_split_tree`/ + /// `dividers` would move the wrong one (or both). + #[test] + fn nested_window_split_dividers_move_independently_when_dragged() { + let mut engine = engine_with_long_buffer(); + engine.split_window(SplitDirection::Vertical, None); + engine.split_window(SplitDirection::Vertical, None); + let mut h = harness(engine, 1400, 900); + h.driver.render(); + + // Select by `split_index` rather than by comparing painted x — the + // nested (inner) divider's *position* can land on either side of the + // outer divider's depending on which child the nesting happened in, + // so magnitude is not a reliable way to tell them apart. `dividers()` + // numbers the outer (top-level) split `0` in pre-order and the split + // nested inside one of its children `1`. + let (outer_x, inner_x, mid_y) = { + let layout = h.screen_layout.borrow(); + let layout = layout.as_ref().expect("a frame must have been painted"); + assert_eq!( + layout.window_dividers.len(), + 2, + "two nested `:vsplit`s must paint two dividers, got {:?}", + layout.window_dividers + ); + let outer = layout + .window_dividers + .iter() + .find(|d| d.split_index == 0) + .expect("the outer split must be split_index 0"); + let inner = layout + .window_dividers + .iter() + .find(|d| d.split_index == 1) + .expect("the nested split must be split_index 1"); + ( + outer.position as i32, + inner.position as i32, + (inner.cross_start + inner.cross_size / 2.0) as i32, + ) + }; + assert_ne!( + outer_x, inner_x, + "the outer and inner dividers must paint at different columns" + ); + + let line_colour = h.driver.pixel(outer_x, mid_y); + let background = h.driver.pixel(outer_x - 40, mid_y); + assert_ne!( + line_colour, background, + "the divider line must be visually distinct from the pane behind it, \ + or this test cannot tell whether it moved" + ); + + // Drag only the inner (rightmost) divider. + let target_x = inner_x - 50; + h.driver.mouse_down(inner_x as f32, mid_y as f32); + h.driver.mouse_move(target_x as f32, mid_y as f32); + h.driver.mouse_up(target_x as f32, mid_y as f32); + h.driver.render(); + + let moved_inner = painted_divider_x(&mut h, target_x, mid_y, 8, line_colour) + .unwrap_or_else(|| panic!("no divider line found near the drag column {target_x}")); + assert!( + moved_inner.abs_diff(target_x) <= 2, + "the dragged (inner) divider must repaint at {target_x}, found it at {moved_inner}" + ); + assert!( + painted_divider_x(&mut h, inner_x, mid_y, 4, line_colour).is_none(), + "the dragged divider must no longer paint at its old column ({inner_x})" + ); + + // The OUTER divider — the one NOT dragged — must still repaint at (or + // within a couple of AA/rounding pixels of) its original column. A + // real split_index/rect mixup would move it by tens or hundreds of + // pixels (or make it vanish entirely, like the dragged divider's old + // column above), not by an AA rounding pixel or two — so a tight but + // non-zero tolerance still catches the bug class this test targets. + let moved_outer = painted_divider_x(&mut h, outer_x, mid_y, 4, line_colour) + .unwrap_or_else(|| panic!("the outer divider must still be painted near {outer_x}")); + assert!( + moved_outer.abs_diff(outer_x) <= 2, + "dragging the inner divider must not move the outer, sibling divider \ + (a split_index/rect mixup between the two would move both): outer \ + divider was at {outer_x}, now painted at {moved_outer}" + ); + } + /// #753, GTK half of the tab-drag rung: dragging one tab past another must /// reorder the **painted** tab bar. /// diff --git a/src/render.rs b/src/render.rs index 1b400764..3a10417a 100644 --- a/src/render.rs +++ b/src/render.rs @@ -19484,6 +19484,33 @@ pub fn screen_zone_hit_test( } // ─── Divider hit-test / drag (shared by GroupLayout and WindowLayout, #582) ── +// +// #818 adopted `quadraui::SplitTree::layout` for the geometry *computation* +// this hit-test code consumes — `GroupLayout`/`WindowLayout::calculate_rects` +// and `::dividers` in `core/window.rs` used to re-derive the same split math +// in two separate hand-rolled recursive passes (the exact "second source of +// truth" risk `SplitTree`'s module docs call out); both now build a +// `quadraui::SplitTree` and read leaf rects + divider geometry off one +// `layout()` call. +// +// The hit-test/drag code below (`DividerGeometry`, `divider_hit_test`, +// `divider_ratio_from_pos`, `DividerMetrics`/`GTK_DIVIDER_METRICS`, +// `route_divider_grab`, `apply_divider_drag`) deliberately stays local +// rather than also moving onto `quadraui::SplitTreeLayout::hit_test_divider`/ +// `hit_test_divider_cell`: those two methods only support a *symmetric* +// tolerance band (continuous) or an *exact single-cell* match (quantized). +// Neither can express the asymmetric multi-cell bands both backends need — +// GTK asks for `(6.0, 6.0)` uniformly, but TUI's `tol_before`/`tol_after` +// differ per divider (`(1.0, 1.0)` for most, `(0.0, tab_bar_rows)` for a +// horizontal group divider, whose grabbable band *is* the neighbouring +// group's whole tab-bar block — see `tui_main::mouse`'s call site). Faking +// that through `SplitTreeDivider`'s `thickness` field would require +// constructing a divider whose `thickness` differs from what was actually +// painted, defeating the "one number describes what was drawn" contract +// `cell_position()`'s doc comment relies on. This is the concrete gap #818 +// asks to be filed against quadraui (asymmetric/multi-cell tolerance bands +// on `SplitTreeLayout::hit_test_divider`/`hit_test_divider_cell`) rather +// than papered over here. /// Common geometry accessor so [`divider_hit_test`] and /// [`divider_ratio_from_pos`] work identically over `GroupDivider` @@ -19627,28 +19654,24 @@ pub fn divider_to_split( id: quadraui::WidgetId, ) -> (quadraui::Split, quadraui::Rect) { let ratio = ((div.position() - div.axis_start()) / div.axis_size()) as f32; - let (direction, rect) = match div.direction() { - // vimcode's `Vertical` = side-by-side panes = quadraui's `Horizontal` - // (their `Split::direction` names the divider's own orientation - // relative to "panes side by side" vs "panes stacked", the inverse - // of vimcode's "divider direction" naming — see primitives/split.rs). - SplitDirection::Vertical => ( - quadraui::SplitDirection::Horizontal, - quadraui::Rect::new( - div.axis_start() as f32, - div.cross_start() as f32, - div.axis_size() as f32, - div.cross_size() as f32, - ), + // vimcode's `Vertical` = side-by-side panes = quadraui's `Horizontal` + // (their `Split::direction` names the divider's own orientation relative + // to "panes side by side" vs "panes stacked", the inverse of vimcode's + // "divider direction" naming — see `core::window::to_quadraui_direction` + // and primitives/split.rs). + let direction = crate::core::window::to_quadraui_direction(div.direction()); + let rect = match div.direction() { + SplitDirection::Vertical => quadraui::Rect::new( + div.axis_start() as f32, + div.cross_start() as f32, + div.axis_size() as f32, + div.cross_size() as f32, ), - SplitDirection::Horizontal => ( - quadraui::SplitDirection::Vertical, - quadraui::Rect::new( - div.cross_start() as f32, - div.axis_start() as f32, - div.cross_size() as f32, - div.axis_size() as f32, - ), + SplitDirection::Horizontal => quadraui::Rect::new( + div.cross_start() as f32, + div.axis_start() as f32, + div.cross_size() as f32, + div.axis_size() as f32, ), }; let split = quadraui::Split { diff --git a/src/tui_main/shell_app.rs b/src/tui_main/shell_app.rs index 2be5380b..043dbbf0 100644 --- a/src/tui_main/shell_app.rs +++ b/src/tui_main/shell_app.rs @@ -5206,6 +5206,79 @@ mod tests { ); } + /// Terminal column of a divider glyph within `+/- span` cells of + /// `target`, for asserting a divider repainted near a drag target (or + /// stayed exactly at a column, with `span == 0`) without hardcoding the + /// exact cell. The TUI twin of `gtk::testing`'s `painted_divider_x`. + fn divider_near_col(cells: &[(char, S)], target: usize, span: usize) -> Option { + let lo = target.saturating_sub(span); + let hi = (target + span).min(cells.len().saturating_sub(1)); + (lo..=hi).find(|&i| cells[i].0 == '\u{2502}') + } + + /// #818: two nested `:vsplit`-style editor-group splits paint **two** + /// divider glyphs on the same row, whose `split_index` differs (0 for + /// the outer split, 1 for the one nested inside its second group). This + /// is the exact shape #582/#452 warned two independently hand-rolled + /// recursive passes over `GroupLayout` (`calculate_group_rects` and + /// `dividers`) could number or position inconsistently — #818 replaced + /// both passes with the leaves/dividers `quadraui::SplitTree::layout` + /// computes together in one pass. Dragging only the inner divider must + /// move exactly that glyph and leave the outer, sibling divider's column + /// untouched; a split_index/rect mixup reintroduced by a future change + /// to `GroupLayout::to_split_tree`/`dividers` would move the wrong one + /// (or both). + #[test] + fn nested_group_split_dividers_move_independently_when_dragged() { + let mut app = TuiShellApp::new(None); + app.engine.buffer_mut().insert(0, "short\n"); + app.engine.open_editor_group(SplitDirection::Vertical); + app.engine.open_editor_group(SplitDirection::Vertical); + let mut driver = driver_with_shell(app, config(), 120, 24); + // See the sibling tests above for why one dispatched no-op event is + // needed before the layout is settled enough to measure. + driver.mouse_up(1.0, 1.0); + + let (tab_x, _) = driver + .find("[No Name]") + .expect("each pane paints its own tab label"); + let after = tab_x as usize; + let row = 5_usize; + let cells = driver.styled_row(row as u16); + let outer = divider_col_on_row(&cells, after) + .expect("the outer vertical split must paint a divider glyph"); + let inner = divider_col_on_row(&cells, outer + 1) + .expect("the nested vertical split must paint a second divider glyph"); + assert_ne!( + outer, inner, + "the outer and inner dividers must paint at different columns" + ); + + // Drag only the inner divider a few cells left. + let target = inner - 5; + driver.mouse_down(inner as f32, row as f32); + driver.mouse_move(target as f32, row as f32); + driver.mouse_up(target as f32, row as f32); + + let moved_cells = driver.styled_row(row as u16); + let screen = driver.screen(); + let moved_inner = divider_near_col(&moved_cells, target, 1).unwrap_or_else(|| { + panic!("no divider glyph found near the drag column {target}; screen:\n{screen}") + }); + assert!( + moved_inner.abs_diff(target) <= 1, + "the dragged inner divider should track the drag column ({target}) \ + within a cell of rounding; screen:\n{screen}" + ); + assert_eq!( + divider_near_col(&moved_cells, outer, 0), + Some(outer), + "dragging the inner divider must not move the outer, sibling \ + divider (a split_index/rect mixup between the two would move \ + both); screen:\n{screen}" + ); + } + /// #609: `render_content` must also paint the tab-drag ghost overlay — /// `render_tab_drag_overlay`, ported from a raw-`Frame`-write tail /// (the ghost label) to `Backend::draw_status_bar` (see its doc