fix(window): wire the side-window split guard and scope the divider drag

PR #155 review round 1.

Finding 1 (must fix): `try_split_active` had no production caller —
`pmacs.window.split_horizontal` / `split_vertical`, and therefore
`C-x 2` / `C-x 3`, still went through plain `split_active`. Splitting a
focused panel made the root wrapper's final child a split rather than
`Leaf(side)`, which both `Layout::compute`'s fixed pass and
`document_subtree` key on: the panel band reverted to 1:1 weight
division and an ordinary window ended up living inside it. Both bindings
now route through the guard, and acc26 asserts through the real Lua
path — a direct core call passes with the guard unwired, which is how it
survived the first round.

Finding 2: the armed-drag early return now checks the arming frontend,
so one frontend's in-flight gesture cannot cancel or swallow another's
mouse events. New acc30c.

Finding 3: `paint_mode_line_graphemes`'s doc block was left heading
`paint_divider_segment`; moved back.

Finding 4: a recompile carries no `display`, so it took the raw switch
and duplicated a panel-placed `*compilation*` into the document window.
`start_run` now detects that the buffer already owns the panel slot.
`pmacs.window.buffer` gained an optional window argument so an adopter
can ask without selecting the panel first. New acc19b.

Stage-2 hazard pins the review asked for, both in `src/daemon.rs`:
a fresh attach while LOCAL is focused in a panel inherits LOCAL's
document buffer, and an initial-target bootstrap whose `after-load`
hook creates and selects a panel still reasserts into a document window.

Minor: dropped listview's dead `p.side`; documented `focus_window`'s
caller-validates contract; `jump_back` restores through `focus_window`
so the "every focus change" contract holds; `params` / `resize` default
to the acting frontend's selected window rather than the ambient one;
widened the flexible-division math to u64 intermediates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-24 15:07:35 -04:00
parent 2a9c11461c
commit 90fc7a913e
9 changed files with 354 additions and 37 deletions

View File

@ -732,6 +732,17 @@ end)
-- Start a run in `slot`. Shared by compile and shell-command; grep -- Start a run in `slot`. Shared by compile and shell-command; grep
-- has its own worker path. -- has its own worker path.
-- Whether `buf` is currently the acting frontend's side-window buffer
-- (bottom-panel arc). Used so a recompile re-displays into the panel it
-- is already in rather than duplicating itself into the document window.
local function already_in_panel(buf)
if not buf then return false end
local panel = pmacs.window.panel()
if not panel then return false end
local ok, shown = pcall(pmacs.window.buffer, panel)
return ok and shown == buf
end
local function start_run(slot, cmdline, opts) local function start_run(slot, cmdline, opts)
opts = opts or {} opts = opts or {}
-- Bottom-panel arc (Q#BP11b): validate placement BEFORE the run -- Bottom-panel arc (Q#BP11b): validate placement BEFORE the run
@ -818,9 +829,14 @@ local function start_run(slot, cmdline, opts)
-- The FIRST display of this run is the side-affine one (Q#BP3): a -- The FIRST display of this run is the side-affine one (Q#BP3): a
-- persistent *compilation* already visible in a document window must -- persistent *compilation* already visible in a document window must
-- not preempt the requested panel. Compile output is passive, so it -- not preempt the requested panel. Compile output is passive, so it
-- takes `select = false` explicitly; a recompile simply reuses the -- takes `select = false` explicitly.
-- panel it is already in. --
if display == "panel" then -- A recompile reaches here with NO `display` (only cmdline/cwd are
-- stored in `_last`), so the raw switch below would put this buffer in
-- the selected DOCUMENT window while the panel still shows it — the
-- duplicate presentation this arc removes elsewhere. Detect that the
-- buffer already owns the panel slot and keep it there.
if display == "panel" or already_in_panel(slot.buf) then
pmacs.window.display(slot.buf, { side = "bottom", select = false }) pmacs.window.display(slot.buf, { side = "bottom", select = false })
else else
pmacs.window.switch_buffer(slot.buf) pmacs.window.switch_buffer(slot.buf)

View File

@ -136,10 +136,8 @@ function pmacs.listview.open(spec)
tostring(display))) tostring(display)))
end end
if display == "panel" then if display == "panel" then
p.side = true
pmacs.window.display(p.buffer, { side = "bottom", select = true }) pmacs.window.display(p.buffer, { side = "bottom", select = true })
else else
p.side = false
pmacs.window.switch_buffer(p.buffer) pmacs.window.switch_buffer(p.buffer)
end end
seat_cursor(p, 1) seat_cursor(p, 1)

View File

@ -4190,4 +4190,133 @@ mod tests {
"key must self-insert into the displayed buffer, not the attach-time scratch" "key must self-insert into the displayed buffer, not the attach-time scratch"
); );
} }
/// Bottom-panel arc, §1.3 #22 (framing acceptance 51's Stage-1 half).
///
/// A fresh no-target attach clones `LOCAL`'s **primary document**
/// buffer, not `local_view.active`. Stage 1 makes a TUI panel a real
/// focus target, so `LOCAL` can legitimately own focus in a panel at
/// attach time — and panel content must never become a newly attached
/// frontend's full-window document.
#[test]
fn fresh_attach_inherits_locals_document_buffer_not_its_focused_panel() {
let mut editor = EditorState::new();
let document_buffer = editor.core.borrow().active_buffer_id();
let panel_buffer = editor.core.borrow().registry.borrow_mut().create("*panel*");
// Open a bottom panel on LOCAL and focus it.
let panel = {
let mut core = editor.core.borrow_mut();
let mut request = crate::editor_core::DisplayRequest::new(panel_buffer);
request.side = Some(crate::window::Side::Bottom);
request.height = Some(5);
request.select = Some(true);
let outcome = core
.display_buffer(FrontendId::LOCAL, &request)
.expect("panel placement");
core.focus_window(FrontendId::LOCAL, outcome.target);
outcome.target
};
assert_eq!(
editor.core.borrow().views[&FrontendId::LOCAL].active,
panel,
"LOCAL really is focused in the panel"
);
let fid = FrontendId(123);
let view = build_fresh_frontend_view(&mut editor, false, false);
editor.core.borrow_mut().register_frontend_view(fid, view);
assert_eq!(
editor
.core
.borrow()
.active_window_for(fid)
.expect("fresh view window")
.buffer_id,
document_buffer,
"the new frontend inherited LOCAL's DOCUMENT buffer; inheriting \
`local_view.active` would have made the panel its document"
);
assert_ne!(document_buffer, panel_buffer);
}
/// Bottom-panel arc, Q#BP11b / R4-B4 (framing acceptance 55's
/// Stage-1 half).
///
/// Stage 1 lets a startup hook create and select a side window. The
/// initial-target bootstrap must still reassert the requested buffer
/// in — and activate — a **non-side** document window, rather than
/// overwriting the panel merely because it became `view.active`.
#[test]
fn initial_target_reasserts_a_document_window_when_a_hook_selects_a_panel() {
use std::os::unix::ffi::OsStrExt as _;
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("target.txt");
std::fs::write(&target, b"target contents\n").expect("write target");
let mut editor = EditorState::new();
editor
.lua_host
.lua()
.load(
r#"
pmacs.lsp.config = {}
pmacs.hook.add("buffer.after-load", function()
if HOOK_RAN then return end
HOOK_RAN = true
HOOK_PANEL = pmacs.window.display(
pmacs.buffer.create("*hook-panel*"),
{ side = "bottom", height = 4, select = true })
end)
"#,
)
.exec()
.expect("install hook");
// A GRID session (panel-capable), which is the realistic shape
// for a hook-created panel in Stage 1 — and real geometry, so
// the panel is genuinely VISIBLE and focused when the reassert
// runs. Without the declaration, reconciliation would hide the
// panel and move focus out on its own, and the assertions below
// would pass without exercising the reassert at all.
let fid = FrontendId(124);
let view = build_fresh_frontend_view(&mut editor, true, true);
editor.core.borrow_mut().register_frontend_view(fid, view);
editor.sync_frame_geometry(fid, CellSize::new(24, 80));
let opened = open_initial_target(
&mut editor,
fid,
InitialTarget {
path: target.as_os_str().as_bytes().to_vec(),
cwd: dir.path().as_os_str().as_bytes().to_vec(),
},
)
.expect("bootstrap succeeds despite the panel-creating hook");
let core = editor.core.borrow();
assert!(
!core.views[&fid].panel_hidden,
"the hook's panel is visible, so focus really was on it when \
the reassert ran"
);
let active = core.views[&fid].active;
let active_window = core.windows.get(&active).expect("active window live");
assert!(
!active_window.is_side(),
"bootstrap activated a DOCUMENT window, not the hook's panel"
);
assert_eq!(
active_window.buffer_id, opened.buffer_id,
"…showing the requested target"
);
let panel = core
.side_window_for(fid)
.expect("the hook's panel survived");
assert_ne!(
core.windows[&panel].buffer_id, opened.buffer_id,
"the panel was not overwritten with the target"
);
}
} }

View File

@ -1925,7 +1925,15 @@ impl EditorState {
// pointer for the whole gesture, INCLUDING rows outside any // pointer for the whole gesture, INCLUDING rows outside any
// window — otherwise tracking would stop the moment the pointer // window — otherwise tracking would stop the moment the pointer
// crossed the frame's status row. // crossed the frame's status row.
if self.window_drag.is_some() { //
// Scoped to the ARMING frontend. The daemon routes every attached
// grid frontend through this same dispatcher, so an unscoped
// check would let one frontend's in-flight drag cancel and
// swallow another frontend's clicks.
if self
.window_drag
.is_some_and(|drag| drag.frontend_id == frontend_id)
{
match ev.kind { match ev.kind {
MouseEventKind::Drag(MouseButton::Left) => { MouseEventKind::Drag(MouseButton::Left) => {
self.drag_window_boundary(frontend_id, cell_row, term_size); self.drag_window_boundary(frontend_id, cell_row, term_size);
@ -3860,9 +3868,6 @@ fn mode_line_grapheme_width(graphemes: &[ModeLineGrapheme]) -> u32 {
graphemes.iter().map(|grapheme| grapheme.width).sum() graphemes.iter().map(|grapheme| grapheme.width).sum()
} }
/// Paint complete graphemes at a logical signed origin. A grapheme that
/// straddles either clip edge is omitted wholesale, so a wide glyph can never
/// leave a dangling half-cell at a window or left/right collision boundary.
/// Restyle one exposed segment of a horizontal split boundary and stamp /// Restyle one exposed segment of a horizontal split boundary and stamp
/// its grip (Q#BP5a). /// its grip (Q#BP5a).
/// ///
@ -3890,6 +3895,9 @@ fn paint_divider_segment(
cell.glyph = crate::cell::Glyph::Char(DIVIDER_HANDLE_GLYPH); cell.glyph = crate::cell::Glyph::Char(DIVIDER_HANDLE_GLYPH);
} }
/// Paint complete graphemes at a logical signed origin. A grapheme that
/// straddles either clip edge is omitted wholesale, so a wide glyph can never
/// leave a dangling half-cell at a window or left/right collision boundary.
fn paint_mode_line_graphemes( fn paint_mode_line_graphemes(
grid: &mut crate::cell::CellGrid<'_>, grid: &mut crate::cell::CellGrid<'_>,
rect: &crate::window::Rect, rect: &crate::window::Rect,

View File

@ -1073,7 +1073,12 @@ impl EditorCore {
.is_some_and(|window| window.buffer_id == entry.buffer_id) .is_some_and(|window| window.buffer_id == entry.buffer_id)
&& !self.side_window_is_hidden(fid, entry.window_id); && !self.side_window_is_hidden(fid, entry.window_id);
if origin_valid { if origin_valid {
self.set_active_window_id(entry.window_id); // Through `focus_window`, not `set_active_window_id`:
// returning INTO a panel from a document window is a
// focus transition like any other, so it refreshes
// `origin_document` and a later `window.quit` returns to
// the window the jump came from.
self.focus_window(fid, entry.window_id);
} else { } else {
// A stale SIDE origin is skipped outright: switching a // A stale SIDE origin is skipped outright: switching a
// panel's buffer into the document window is exactly the // panel's buffer into the document window is exactly the
@ -2667,6 +2672,12 @@ impl EditorCore {
/// Focus an explicit window in the acting frontend, refreshing the /// Focus an explicit window in the acting frontend, refreshing the
/// panel's remembered document origin on the way (Q#BP2c). /// panel's remembered document origin on the way (Q#BP2c).
///
/// **The caller must have validated `target`** — that it is live and
/// belongs to `fid`'s layout. Every Lua path does so through
/// `lookup_window` or the display transaction's own revalidation;
/// this function only `debug_assert!`s it, so a release-mode caller
/// passing a foreign or dead id would leave `view.active` dangling.
pub fn focus_window(&mut self, fid: FrontendId, target: WindowId) { pub fn focus_window(&mut self, fid: FrontendId, target: WindowId) {
let Some(view) = self.views.get_mut(&fid) else { let Some(view) = self.views.get_mut(&fid) else {
return; return;

View File

@ -12218,14 +12218,20 @@ fn install_window_module(lua: &Lua, core: &SharedCore) -> mlua::Result<Table> {
window_panel::install(lua, core, &win)?; window_panel::install(lua, core, &win)?;
{ {
// Bottom-panel arc (Q#BP6): `try_split_active` refuses a side
// window. This binding is what `C-x 2` reaches, so the refusal
// has to live on THIS path — splitting the panel leaf would make
// the root wrapper's final child a split rather than
// `Leaf(side)`, and both `Layout::compute`'s fixed pass and
// `document_subtree` key on exactly that shape.
let cc = core.clone(); let cc = core.clone();
win.set( win.set(
"split_horizontal", "split_horizontal",
lua.create_function(move |_, ()| { lua.create_function(move |_, ()| {
let new_id = cc cc.borrow_mut()
.borrow_mut() .try_split_active(crate::window::Orientation::Horizontal, true)
.split_active(crate::window::Orientation::Horizontal, true); .map(crate::window::WindowId::raw)
Ok(new_id.raw()) .map_err(mlua::Error::runtime)
})?, })?,
)?; )?;
} }
@ -12235,10 +12241,10 @@ fn install_window_module(lua: &Lua, core: &SharedCore) -> mlua::Result<Table> {
win.set( win.set(
"split_vertical", "split_vertical",
lua.create_function(move |_, ()| { lua.create_function(move |_, ()| {
let new_id = cc cc.borrow_mut()
.borrow_mut() .try_split_active(crate::window::Orientation::Vertical, true)
.split_active(crate::window::Orientation::Vertical, true); .map(crate::window::WindowId::raw)
Ok(new_id.raw()) .map_err(mlua::Error::runtime)
})?, })?,
)?; )?;
} }
@ -12351,9 +12357,38 @@ fn install_window_module(lua: &Lua, core: &SharedCore) -> mlua::Result<Table> {
{ {
let cc = core.clone(); let cc = core.clone();
// With no argument: the selected window's buffer (unchanged).
// With an explicit window id: that window's buffer, validated
// against the acting frontend's layout like every other
// `WindowId`-taking operation (bottom-panel arc, Q#BP11) — an
// adopter has to be able to ask "is my buffer the one in the
// panel" without first selecting the panel.
win.set( win.set(
"buffer", "buffer",
lua.create_function(move |_, ()| Ok(BufferIdLua(cc.borrow().active_buffer_id())))?, lua.create_function(move |lua, target: Option<u64>| -> mlua::Result<BufferIdLua> {
let Some(raw) = target else {
return Ok(BufferIdLua(cc.borrow().active_buffer_id()));
};
let fid = window_panel::acting_frontend(lua, &cc);
let core = cc.borrow();
let view = core.views.get(&fid).ok_or_else(|| {
mlua::Error::runtime("pmacs.window.buffer: acting frontend has no layout")
})?;
let id = view
.layout
.iter_ids()
.into_iter()
.find(|id| id.raw() == raw)
.ok_or_else(|| {
mlua::Error::runtime(format!(
"pmacs.window.buffer: window {raw} is not live in this frontend's layout"
))
})?;
core.windows
.get(&id)
.map(|window| BufferIdLua(window.buffer_id))
.ok_or_else(|| mlua::Error::runtime("pmacs.window.buffer: window not live"))
})?,
)?; )?;
} }

View File

@ -184,6 +184,19 @@ fn parse_request(
Ok(request) Ok(request)
} }
/// The ACTING frontend's selected window.
///
/// Not `active_window_id()`, which resolves through the ambient active
/// frontend: every other id in this module is `fid`-scoped, and the two
/// only coincide because dispatch happens to set `active_frontend` first.
fn selected_window(core: &SharedCore, fid: FrontendId) -> mlua::Result<WindowId> {
core.borrow()
.views
.get(&fid)
.map(|view| view.active)
.ok_or_else(|| mlua::Error::runtime("pmacs.window: acting frontend has no layout"))
}
/// Resolve a raw Lua window id, refusing one that is not live in the /// Resolve a raw Lua window id, refusing one that is not live in the
/// acting frontend's layout (Q#BP11). /// acting frontend's layout (Q#BP11).
fn lookup_window(core: &SharedCore, fid: FrontendId, raw: u64) -> mlua::Result<WindowId> { fn lookup_window(core: &SharedCore, fid: FrontendId, raw: u64) -> mlua::Result<WindowId> {
@ -484,7 +497,7 @@ pub(crate) fn install(lua: &Lua, core: &SharedCore, win: &Table) -> mlua::Result
let fid = acting_frontend(lua, &cc); let fid = acting_frontend(lua, &cc);
let id = match target { let id = match target {
Some(raw) => lookup_window(&cc, fid, raw)?, Some(raw) => lookup_window(&cc, fid, raw)?,
None => cc.borrow().active_window_id(), None => selected_window(&cc, fid)?,
}; };
let core = cc.borrow(); let core = cc.borrow();
let window = core let window = core
@ -577,7 +590,7 @@ pub(crate) fn install(lua: &Lua, core: &SharedCore, win: &Table) -> mlua::Result
let fid = acting_frontend(lua, &cc); let fid = acting_frontend(lua, &cc);
let id = match target { let id = match target {
Some(raw) => lookup_window(&cc, fid, raw)?, Some(raw) => lookup_window(&cc, fid, raw)?,
None => cc.borrow().active_window_id(), None => selected_window(&cc, fid)?,
}; };
let area_rows = cc.borrow().frontend_area_rows(fid).ok_or_else(|| { let area_rows = cc.borrow().frontend_area_rows(fid).ok_or_else(|| {
mlua::Error::runtime( mlua::Error::runtime(

View File

@ -979,14 +979,18 @@ fn compute_node(
rows rows
} else { } else {
let w = weights.get(i).copied().unwrap_or(1).max(1); let w = weights.get(i).copied().unwrap_or(1).max(1);
// u64 intermediates: `remainder * w` is the only
// place this arithmetic could overflow a u32, and a
// saturating fallback there would hand a non-last
// child the whole remainder and underflow the last
// one. Widening deletes the case outright.
let e = if Some(i) == last_flexible { let e = if Some(i) == last_flexible {
remainder - flexible_used remainder - flexible_used
} else if total == 0 {
0
} else { } else {
remainder u32::try_from(u64::from(remainder) * u64::from(w) / u64::from(total))
.checked_mul(w)
.unwrap_or(remainder) .unwrap_or(remainder)
.checked_div(total)
.unwrap_or(0)
}; };
flexible_used += e; flexible_used += e;
e e

View File

@ -1300,6 +1300,55 @@ fn acc19_adopters_place_side_affinely_through_real_entry_points() {
assert_ne!(panel, document); assert_ne!(panel, document);
} }
/// A recompile carries no `display` (only cmdline/cwd are stored), so
/// the raw switch would put `*compilation*` in the selected DOCUMENT
/// window while the panel still shows it — the duplicate presentation
/// this arc removes elsewhere.
#[test]
fn acc19b_recompile_reuses_the_panel_instead_of_duplicating_into_the_document() {
let s = editor();
exec(&s, "pmacs.window.split_horizontal()");
exec(&s, "pmacs.compile.run(\"true\", { display = \"panel\" })");
let panel = side_window(&s).expect("compile opened a panel");
let compilation = s.core.borrow().windows[&panel].buffer_id;
// Focus a document window, then recompile — which reaches
// `start_run` with no `display` at all.
let document = s
.core
.borrow()
.non_side_target(FrontendId::LOCAL)
.expect("document");
s.core
.borrow_mut()
.focus_window(FrontendId::LOCAL, document);
let document_buffer = s.core.borrow().windows[&document].buffer_id;
exec(&s, "pmacs.command.invoke(\"compile.recompile\")");
assert_eq!(
s.core.borrow().windows[&panel].buffer_id,
compilation,
"the recompile stayed in the panel"
);
assert_eq!(
s.core.borrow().windows[&document].buffer_id,
document_buffer,
"…and did not duplicate itself into the document window"
);
// A compilation that is NOT in a panel keeps the pre-arc raw switch.
let s = editor();
exec(&s, "pmacs.compile.run(\"true\")");
assert!(side_window(&s).is_none());
let target = active_window(&s);
exec(&s, "pmacs.command.invoke(\"compile.recompile\")");
assert_eq!(active_window(&s), target);
assert!(
side_window(&s).is_none(),
"no panel is created out of nowhere"
);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 20 / 23 — quit: delete, restore chains, revalidation, and the cap // 20 / 23 — quit: delete, restore chains, revalidation, and the cap
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -1549,27 +1598,30 @@ fn acc26_close_others_and_split_respect_the_side_window() {
let s = editor(); let s = editor();
exec(&s, "pmacs.window.split_horizontal()"); exec(&s, "pmacs.window.split_horizontal()");
let panel = open_panel(&s, "*panel*", 5); let panel = open_panel(&s, "*panel*", 5);
// From a side window both are pointed errors. // From a side window both are pointed errors — asserted through the
// REAL Lua bindings, which is what `C-x 1` / `C-x 2` / `C-x 3`
// reach. A direct `core.try_split_active(..)` call would pass even
// with the guard unwired, which is exactly how an unwired guard
// survives review.
exec(&s, "pmacs.window.focus_next()"); exec(&s, "pmacs.window.focus_next()");
while active_window(&s) != panel { while active_window(&s) != panel {
exec(&s, "pmacs.window.focus_next()"); exec(&s, "pmacs.window.focus_next()");
} }
assert!(s.core.borrow_mut().close_others().is_err()); let before = structure(&layout_root(&s));
assert!( assert!(try_exec(&s, "pmacs.window.close_others()").is_err());
s.core assert!(try_exec(&s, "pmacs.window.split_horizontal()").is_err());
.borrow_mut() assert!(try_exec(&s, "pmacs.window.split_vertical()").is_err());
.try_split_active(Orientation::Horizontal, true)
.is_err()
);
assert!(side_window(&s).is_some(), "nothing was mutated"); assert!(side_window(&s).is_some(), "nothing was mutated");
assert_eq!(
before,
structure(&layout_root(&s)),
"the wrapper's final child is still Leaf(side)"
);
// From a document window, close_others deletes the panel too. // From a document window, close_others deletes the panel too.
exec(&s, "pmacs.window.focus_next()"); exec(&s, "pmacs.window.focus_next()");
assert_ne!(active_window(&s), panel); assert_ne!(active_window(&s), panel);
s.core exec(&s, "pmacs.window.close_others()");
.borrow_mut()
.close_others()
.expect("document may close others");
assert!(side_window(&s).is_none()); assert!(side_window(&s).is_none());
assert_eq!( assert_eq!(
s.core.borrow().views[&FrontendId::LOCAL] s.core.borrow().views[&FrontendId::LOCAL]
@ -1728,6 +1780,57 @@ fn acc30_divider_drag_writes_fixed_rows_and_weights_and_creates_no_selection() {
assert!(doubled > after, "the ratio scales with the frame"); assert!(doubled > after, "the ratio scales with the frame");
} }
/// An armed drag owns the pointer for its OWN frontend only. The daemon
/// routes every attached grid frontend through one `dispatch_mouse`, so
/// an unscoped guard would let one frontend's in-flight gesture cancel
/// and swallow another frontend's clicks.
#[test]
fn acc30c_an_armed_drag_does_not_swallow_another_frontends_mouse_events() {
let mut s = editor();
let panel = open_panel(&s, "*panel*", 6);
let document = s
.core
.borrow()
.non_side_target(FrontendId::LOCAL)
.expect("document");
let other = FrontendId(30);
let other_window = attach_frontend(&s, other, true);
let rects = render(&s);
let divider_row = u16::try_from(rects[&document].origin.row + rects[&document].size.rows - 1)
.expect("row fits");
s.dispatch_mouse(
FrontendId::LOCAL,
mouse(MouseEventKind::Down(MouseButton::Left), divider_row, 3),
CellSize::new(ROWS, COLS),
);
let armed_rows = fixed_rows_of(&s, panel);
// A click from the OTHER frontend must be dispatched normally…
s.dispatch_mouse(
other,
mouse(MouseEventKind::Down(MouseButton::Left), 1, 2),
CellSize::new(ROWS, COLS),
);
assert_eq!(
s.core.borrow().views[&other].active,
other_window,
"the peer's click reached its own window instead of being swallowed"
);
// …and LOCAL's gesture must still be armed and still work.
s.dispatch_mouse(
FrontendId::LOCAL,
mouse(MouseEventKind::Drag(MouseButton::Left), divider_row + 2, 3),
CellSize::new(ROWS, COLS),
);
assert_eq!(
fixed_rows_of(&s, panel),
Some(armed_rows.expect("armed rows") - 2),
"the peer's event did not cancel LOCAL's in-flight drag"
);
}
#[test] #[test]
fn acc30b_ui_divider_face_resolves_and_paints_every_exposed_segment() { fn acc30b_ui_divider_face_resolves_and_paints_every_exposed_segment() {
let s = editor(); let s = editor();