R5.3: ElyDoc drags the cursor's table borders, one kernel command per drop at the read's revision; three rounds used, last fixes tested
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d07f0ddb7c
commit
e6e5aea7a1
21 files changed
+832
-15
No files matched your search
@@ -32,6 +32,7 @@ windows-sys.workspace = true
|
||||
tempfile.workspace = true
|
||||
ely-ui.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
ely-gpui-component = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[[example]]
|
||||
name = "lokview"
|
||||
|
||||
@@ -48,6 +48,7 @@ fn unpack_pushed(body: &[u8]) -> io::Result<(u64, Events)> {
|
||||
selection: marked,
|
||||
};
|
||||
let cell = crate::events::Cell::unpack(&mut unpack)?;
|
||||
let table = crate::table::Table::unpack(&mut unpack)?;
|
||||
let events = Events {
|
||||
seq,
|
||||
revision,
|
||||
@@ -72,6 +73,7 @@ fn unpack_pushed(body: &[u8]) -> io::Result<(u64, Events)> {
|
||||
a11y,
|
||||
tabs,
|
||||
cell,
|
||||
table,
|
||||
};
|
||||
Ok((id, events))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
||||
use super::*;
|
||||
|
||||
mod analysis;
|
||||
mod borders;
|
||||
mod charts;
|
||||
mod check;
|
||||
mod commands;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
use super::input::open;
|
||||
use super::menus::{DOC, find};
|
||||
use super::*;
|
||||
|
||||
const WAIT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
/// The first inner column line, once it differs from `from`.
|
||||
fn column(worker: &mut Worker, doc: &Doc, seen: &Events, from: i32) -> Events {
|
||||
poll_until(worker, doc, seen, WAIT, |seen| {
|
||||
seen.table.as_ref().is_some_and(|table| table.columns.inner[0].position != from)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_border_reports_and_moves_in_one_step() {
|
||||
if install().is_none() {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
}
|
||||
let (_dir, mut worker, doc) = open(DOC);
|
||||
click(&mut worker, &doc, 150, 150);
|
||||
let mut seen = Events::default();
|
||||
let (x, y) = find(&mut worker, &doc, &mut seen, "left");
|
||||
click(&mut worker, &doc, x, y);
|
||||
seen = poll_until(&mut worker, &doc, &seen, WAIT, |seen| seen.table.is_some());
|
||||
let table = seen.table.clone().expect("the cursor's table");
|
||||
let (columns, rows) = (&table.columns, &table.rows);
|
||||
assert_eq!(columns.inner.len(), 1, "two columns, one inner line");
|
||||
assert_eq!(rows.inner.len(), 1, "two rows, one inner line");
|
||||
let caret = seen.cursor.expect("a caret");
|
||||
let across = (columns.offset + columns.left, columns.offset + columns.inner[0].position);
|
||||
let down = (rows.offset + rows.left, rows.offset + rows.inner[0].position);
|
||||
assert!(across.0 < caret.0 && caret.0 < across.1, "the caret is in column one: {across:?} {caret:?}");
|
||||
assert!(down.0 <= caret.1 && caret.1 < down.1, "the caret is in row one: {down:?} {caret:?}");
|
||||
|
||||
let read = worker.now(&doc).expect("the revision");
|
||||
let line = columns.inner[0].position;
|
||||
worker.run(&read, "table.border", &["column-middle", "0", "500"]).expect("the line moves");
|
||||
seen = column(&mut worker, &doc, &seen, line);
|
||||
let moved = seen.table.clone().expect("still in the table");
|
||||
assert_eq!(moved.columns.inner[0].position, line + 500, "500 twips right");
|
||||
let stale = worker.run(&read, "table.border", &["column-middle", "0", "500"]);
|
||||
assert!(stale.is_err_and(|error| crate::is_stale(&error)), "an old read is refused");
|
||||
|
||||
worker.run(&doc, "edit.undo", &[]).expect("undo");
|
||||
seen = column(&mut worker, &doc, &seen, line + 500);
|
||||
let undone = seen.table.clone().expect("still in the table");
|
||||
assert_eq!(undone.columns.inner[0].position, line, "one undo restores it");
|
||||
|
||||
let bottom = undone.rows.right;
|
||||
let read = worker.now(&doc).expect("the revision");
|
||||
worker.run(&read, "table.border", &["row-right", "0", "400"]).expect("the bottom moves");
|
||||
seen = poll_until(&mut worker, &doc, &seen, WAIT, |seen| {
|
||||
seen.table.as_ref().is_some_and(|table| table.rows.right != bottom)
|
||||
});
|
||||
let taller = seen.table.expect("still in the table");
|
||||
assert_eq!(taller.rows.right, bottom + 400, "the last row grows");
|
||||
}
|
||||
@@ -143,7 +143,7 @@ fn every_mapped_command_reaches_the_kernel_and_reports_state() {
|
||||
assert!(text.contains("fo:font-weight=\"bold\""), "bold landed");
|
||||
assert_eq!(
|
||||
commands::all().count(),
|
||||
246,
|
||||
247,
|
||||
"the rows of text, layout, refs, tools, sheet, calc, data, analysis, page, show and draw"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,6 +159,16 @@ pub(super) const ROWS: &[Command] = &[
|
||||
&[],
|
||||
Check::Undoable,
|
||||
),
|
||||
command(
|
||||
"table.border",
|
||||
".uno:TableChangeCurrentBorderPosition",
|
||||
&[
|
||||
Arg::Text("BorderType"),
|
||||
Arg::Int("Index", "unsigned short", 0, 999),
|
||||
Arg::Int("Offset", "long", -600_000, 600_000),
|
||||
],
|
||||
Check::Undoable,
|
||||
),
|
||||
command(
|
||||
"table.borders_all",
|
||||
".uno:SetBorderStyle",
|
||||
|
||||
@@ -54,6 +54,7 @@ const CELL_ADDRESS: i32 = 34;
|
||||
const REFERENCE_MARKS: i32 = 45;
|
||||
const FUNCTION_LIST: i32 = 47;
|
||||
const TOOLTIP: i32 = 71;
|
||||
const TABLE: i32 = 44;
|
||||
|
||||
/// A mixed state; kernel values never hold NUL.
|
||||
pub const MIXED: &str = "\u{0}mixed";
|
||||
@@ -103,6 +104,7 @@ pub struct Events {
|
||||
/// The paragraph's own tab stops, in 1/100 mm.
|
||||
pub tabs: Vec<i32>,
|
||||
pub cell: Cell,
|
||||
pub table: Option<crate::table::Table>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -144,7 +146,8 @@ fn signal() -> &'static Condvar {
|
||||
SIGNAL.get_or_init(Condvar::new)
|
||||
}
|
||||
|
||||
const KINDS: [i32; 34] = [
|
||||
const KINDS: [i32; 35] = [
|
||||
TABLE,
|
||||
MOUSE_POINTER,
|
||||
REFERENCE_MARKS,
|
||||
FUNCTION_LIST,
|
||||
@@ -266,6 +269,7 @@ fn record(slot: &mut Slot, kind: i32, text: &str) -> Option<u32> {
|
||||
.push(format!("an unreadable search result: {text}")),
|
||||
},
|
||||
MENU => batch.menu = Some(text.to_string()),
|
||||
TABLE => crate::table::read(text).map_or_else(|fault| batch.faults.push(fault), |table| batch.table = table),
|
||||
CELL_CURSOR => batch.cell.cursor = rect_in(batch, "cell cursor", text),
|
||||
CELL_FORMULA => batch.cell.formula = text.to_string(),
|
||||
CELL_ADDRESS => batch.cell.address = text.to_string(),
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod menu;
|
||||
pub mod review;
|
||||
pub mod sheet;
|
||||
pub mod show;
|
||||
mod table;
|
||||
mod worker;
|
||||
|
||||
pub use client::{
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
//! The cursor's table borders, as Writer reports them (DOC-07).
|
||||
|
||||
use std::io;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::ipc::{Pack, Unpack};
|
||||
|
||||
/// One axis in twips: columns run across, rows down.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Lines {
|
||||
/// The first line, from `offset`.
|
||||
pub left: i32,
|
||||
/// The last line, from `offset`.
|
||||
pub right: i32,
|
||||
pub offset: i32,
|
||||
pub inner: Vec<Line>,
|
||||
}
|
||||
|
||||
/// An inner line and the kernel's bounds for it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Line {
|
||||
pub position: i32,
|
||||
pub min: i32,
|
||||
pub max: i32,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
/// The table holding the cursor.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Table {
|
||||
pub columns: Lines,
|
||||
pub rows: Lines,
|
||||
}
|
||||
|
||||
/// Reads a `LOK_CALLBACK_TABLE_SELECTED` payload; `{}` is no table.
|
||||
pub fn read(text: &str) -> Result<Option<Table>, String> {
|
||||
let fault = || format!("an unreadable table: {text}");
|
||||
let json: Value = serde_json::from_str(text).map_err(|_| fault())?;
|
||||
let object = json.as_object().ok_or_else(fault)?;
|
||||
if object.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let int = |value: Option<&Value>| i32::try_from(value?.as_i64()?).ok();
|
||||
let lines = |key: &str| -> Option<Lines> {
|
||||
let axis = object.get(key)?;
|
||||
let inner = axis.get("entries")?.as_array()?.iter().map(|entry| {
|
||||
Some(Line {
|
||||
position: int(entry.get("position"))?,
|
||||
min: int(entry.get("min"))?,
|
||||
max: int(entry.get("max"))?,
|
||||
hidden: entry.get("hidden")?.as_bool()?,
|
||||
})
|
||||
});
|
||||
Some(Lines {
|
||||
left: int(axis.get("left"))?,
|
||||
right: int(axis.get("right"))?,
|
||||
offset: int(axis.get("tableOffset"))?,
|
||||
inner: inner.collect::<Option<_>>()?,
|
||||
})
|
||||
};
|
||||
let columns = lines("columns").ok_or_else(fault)?;
|
||||
let rows = lines("rows").ok_or_else(fault)?;
|
||||
Ok(Some(Table { columns, rows }))
|
||||
}
|
||||
|
||||
impl Table {
|
||||
pub(crate) fn pack(table: Option<&Table>, pack: Pack) -> Pack {
|
||||
let Some(table) = table else {
|
||||
return pack.u64(0);
|
||||
};
|
||||
[&table.columns, &table.rows].into_iter().fold(pack.u64(1), |pack, axis| {
|
||||
let pack = pack
|
||||
.i64(axis.left.into())
|
||||
.i64(axis.right.into())
|
||||
.i64(axis.offset.into())
|
||||
.u64(axis.inner.len() as u64);
|
||||
axis.inner.iter().fold(pack, |pack, line| {
|
||||
pack.i64(line.position.into())
|
||||
.i64(line.min.into())
|
||||
.i64(line.max.into())
|
||||
.u64(line.hidden as u64)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn unpack(unpack: &mut Unpack) -> io::Result<Option<Table>> {
|
||||
if unpack.u64()? == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut lines = || -> io::Result<Lines> {
|
||||
let mut int = || i32::try_from(unpack.i64()?).map_err(io::Error::other);
|
||||
let (left, right, offset) = (int()?, int()?, int()?);
|
||||
let inner = (0..unpack.u64()?)
|
||||
.map(|_| {
|
||||
let mut int = || i32::try_from(unpack.i64()?).map_err(io::Error::other);
|
||||
let (position, min, max) = (int()?, int()?, int()?);
|
||||
Ok(Line { position, min, max, hidden: unpack.u64()? != 0 })
|
||||
})
|
||||
.collect::<io::Result<_>>()?;
|
||||
Ok(Lines { left, right, offset, inner })
|
||||
};
|
||||
let columns = lines()?;
|
||||
let rows = lines()?;
|
||||
Ok(Some(Table { columns, rows }))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SEEN: &str = r#"{ "columns": { "left": 0, "right": 9638, "tableOffset": 1135, "entries": [ { "position": 4819, "min": 300, "max": 9338, "hidden": false } ] }, "rows": { "left": 0, "right": 552, "tableOffset": 1418, "entries": [ { "position": 276, "min": 100, "max": 552, "hidden": false } ] } }"#;
|
||||
|
||||
#[test]
|
||||
fn a_table_reads_and_crosses_the_wire() {
|
||||
assert_eq!(read("{ }"), Ok(None), "no table");
|
||||
let table = read(SEEN).expect("readable").expect("a table");
|
||||
assert_eq!(table.columns.offset, 1135);
|
||||
assert_eq!(table.rows.inner[0].position, 276);
|
||||
let text = SEEN.replace("\"left\": 0,", "\"left\": \"0\",");
|
||||
assert!(read(&text).is_err(), "a number as text is a fault");
|
||||
assert!(read("[]").is_err());
|
||||
let bytes = Table::pack(Some(&table), Pack::new()).bytes();
|
||||
let back = Table::unpack(&mut Unpack::new(&bytes)).expect("unpacks");
|
||||
assert_eq!(back, Some(table));
|
||||
let none = Table::pack(None, Pack::new()).bytes();
|
||||
assert_eq!(Table::unpack(&mut Unpack::new(&none)).expect("unpacks"), None);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use image::{Frame, ImageBuffer, Rgba};
|
||||
|
||||
use crate::{Doc, Events, Rect, Worker};
|
||||
|
||||
mod borders;
|
||||
mod clip;
|
||||
mod clock;
|
||||
mod formula;
|
||||
@@ -130,6 +131,7 @@ pub struct LokView {
|
||||
marked: Option<ime::Marked>,
|
||||
dragging: bool,
|
||||
grip: Option<handles::Grip>,
|
||||
borders: borders::Borders,
|
||||
handle_size: Pixels,
|
||||
states: HashMap<String, String>,
|
||||
graphic: Option<Rect>,
|
||||
@@ -164,7 +166,6 @@ pub fn layout(worker: &mut Worker, doc: &Doc) -> io::Result<Layout> {
|
||||
Ok((doc.size(worker)?, doc.pages(worker)?))
|
||||
}
|
||||
|
||||
|
||||
impl EventEmitter<LokError> for LokView {}
|
||||
|
||||
impl EventEmitter<Reported> for LokView {}
|
||||
@@ -192,10 +193,7 @@ impl LokView {
|
||||
let mut view = Self {
|
||||
worker,
|
||||
doc,
|
||||
geom: Geom {
|
||||
zoom: 1.0,
|
||||
dpi: 1.0,
|
||||
},
|
||||
geom: Geom { zoom: 1.0, dpi: 1.0 },
|
||||
scroll: (0, 0),
|
||||
doc_size: (width as i32, height as i32),
|
||||
generation: 0,
|
||||
@@ -234,6 +232,7 @@ impl LokView {
|
||||
marked: None,
|
||||
dragging: false,
|
||||
grip: None,
|
||||
borders: Default::default(),
|
||||
handle_size: px(6.0),
|
||||
states: HashMap::new(),
|
||||
graphic: None,
|
||||
@@ -400,6 +399,7 @@ impl LokView {
|
||||
}
|
||||
self.cell = batch.cell;
|
||||
self.graphic = batch.graphic;
|
||||
self.read_borders(batch.table, batch.revision);
|
||||
self.review = batch.review;
|
||||
self.search = (batch.searched, batch.found);
|
||||
self.fonts = batch.fonts;
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
//! Dragging the cursor's table borders (DOC-07).
|
||||
|
||||
use gpui::{Bounds, Context, CursorStyle, Pixels, Point, px, size};
|
||||
|
||||
use super::LokView;
|
||||
use crate::table::{Lines, Table};
|
||||
use crate::{Doc, LokError};
|
||||
|
||||
/// Least gap the kernel keeps between lines, in twips.
|
||||
const GAP: i32 = 40;
|
||||
|
||||
/// A border, as `.uno:TableChangeCurrentBorderPosition` names it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(super) enum Side {
|
||||
ColumnLeft,
|
||||
Column(u16),
|
||||
ColumnRight,
|
||||
Row(u16),
|
||||
RowBottom,
|
||||
}
|
||||
|
||||
impl Side {
|
||||
/// Column lines stand upright and move across.
|
||||
fn upright(self) -> bool {
|
||||
matches!(self, Side::ColumnLeft | Side::Column(_) | Side::ColumnRight)
|
||||
}
|
||||
|
||||
fn kind(self) -> &'static str {
|
||||
match self {
|
||||
Side::ColumnLeft => "column-left",
|
||||
Side::Column(_) => "column-middle",
|
||||
Side::ColumnRight => "column-right",
|
||||
Side::Row(_) => "row-middle",
|
||||
Side::RowBottom => "row-right",
|
||||
}
|
||||
}
|
||||
|
||||
fn index(self) -> u16 {
|
||||
match self {
|
||||
Side::Column(index) | Side::Row(index) => index,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A held border, with the table read it came from.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Held {
|
||||
side: Side,
|
||||
/// The pointer's offset from the line when pressed.
|
||||
grab: i32,
|
||||
from: i32,
|
||||
to: i32,
|
||||
table: Table,
|
||||
doc: Doc,
|
||||
}
|
||||
|
||||
/// The cursor's table as last read, and a held border.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Borders {
|
||||
table: Option<(Table, Doc)>,
|
||||
/// The revision of the batch the table came from.
|
||||
read: u64,
|
||||
held: Option<Held>,
|
||||
hover: Option<CursorStyle>,
|
||||
}
|
||||
|
||||
/// Each border: side, place, span and room, in twips.
|
||||
fn lines(table: &Table) -> Vec<(Side, i32, (i32, i32), i32)> {
|
||||
let (columns, rows) = (&table.columns, &table.rows);
|
||||
let down = (rows.offset + rows.left, rows.offset + rows.right);
|
||||
let across = (columns.offset + columns.left, columns.offset + columns.right);
|
||||
let mut upright = vec![(Side::ColumnLeft, across.0)];
|
||||
upright.extend(inner(columns).map(|(index, at)| (Side::Column(index), at)));
|
||||
upright.push((Side::ColumnRight, across.1));
|
||||
let mut level: Vec<_> = inner(rows).map(|(index, at)| (Side::Row(index), at)).collect();
|
||||
level.push((Side::RowBottom, down.1));
|
||||
let mut lines = spaced(&upright, None, down);
|
||||
lines.extend(spaced(&level, Some(down.0), across));
|
||||
lines
|
||||
}
|
||||
|
||||
/// Gives each line its span and nearest neighbour's distance.
|
||||
fn spaced(stops: &[(Side, i32)], before: Option<i32>, span: (i32, i32)) -> Vec<(Side, i32, (i32, i32), i32)> {
|
||||
let places: Vec<i32> = before.into_iter().chain(stops.iter().map(|&(_, at)| at)).collect();
|
||||
let skip = places.len() - stops.len();
|
||||
let room = |i: usize| {
|
||||
let prev = i.checked_sub(1).map(|j| places[i] - places[j]);
|
||||
prev.into_iter().chain(places.get(i + 1).map(|next| next - places[i])).min().unwrap_or(i32::MAX)
|
||||
};
|
||||
stops.iter().enumerate().map(|(i, &(side, at))| (side, at, span, room(i + skip))).collect()
|
||||
}
|
||||
|
||||
fn inner(lines: &Lines) -> impl Iterator<Item = (u16, i32)> + '_ {
|
||||
lines.inner.iter().enumerate().filter(|(_, line)| !line.hidden).map(|(index, line)| (index as u16, lines.offset + line.position))
|
||||
}
|
||||
|
||||
/// `offset` as the kernel will clamp it.
|
||||
fn clamp(table: &Table, side: Side, offset: i32) -> i32 {
|
||||
let axis = if side.upright() { &table.columns } else { &table.rows };
|
||||
let (first, last) = (axis.inner.first(), axis.inner.last());
|
||||
let (at, low, high) = match side {
|
||||
Side::ColumnLeft => (axis.left, i32::MIN, first.map_or(axis.right, |line| line.position) - GAP),
|
||||
Side::ColumnRight | Side::RowBottom => (axis.right, last.map_or(axis.left, |line| line.position) + GAP, i32::MAX),
|
||||
Side::Column(index) | Side::Row(index) => {
|
||||
let line = axis.inner[usize::from(index)];
|
||||
(line.position, line.min, (line.max - GAP).max(line.min))
|
||||
}
|
||||
};
|
||||
(at.saturating_add(offset)).clamp(low, high) - at
|
||||
}
|
||||
|
||||
impl LokView {
|
||||
/// Takes the cursor's table from a batch at `revision`.
|
||||
pub(super) fn read_borders(&mut self, table: Option<Table>, revision: u64) {
|
||||
if revision < self.borders.read {
|
||||
log::debug!("lok: table read at {revision} is older than {}", self.borders.read);
|
||||
return;
|
||||
}
|
||||
if table.is_none() {
|
||||
self.borders.hover = None;
|
||||
}
|
||||
let mut doc = self.doc.clone();
|
||||
doc.rev = revision;
|
||||
self.borders.read = revision;
|
||||
self.borders.table = table.map(|table| (table, doc));
|
||||
}
|
||||
|
||||
/// The border under `position`; short cells keep their middles.
|
||||
fn border_at(&self, position: Point<Pixels>) -> Option<(Side, i32)> {
|
||||
let (table, _) = self.borders.table.as_ref()?;
|
||||
lines(table).into_iter().find_map(|(side, at, (start, end), room)| {
|
||||
let reach = self.handle_size.min(px(self.geom.px_of(room) / 4.0));
|
||||
let (from, to) = if side.upright() {
|
||||
(self.window_point((at, start)), self.window_point((at, end)))
|
||||
} else {
|
||||
(self.window_point((start, at)), self.window_point((end, at)))
|
||||
};
|
||||
let (near, along, low, high) = if side.upright() {
|
||||
((from.x - position.x).abs(), position.y, from.y, to.y)
|
||||
} else {
|
||||
((from.y - position.y).abs(), position.x, from.x, to.x)
|
||||
};
|
||||
(near <= reach && along >= low - reach && along <= high + reach).then_some((side, at))
|
||||
})
|
||||
}
|
||||
|
||||
/// Holds the border under `position`, if any.
|
||||
pub(super) fn hold_border(&mut self, position: Point<Pixels>) -> bool {
|
||||
let Some(((side, at), (table, doc))) = self.border_at(position).zip(self.borders.table.clone()) else {
|
||||
return false;
|
||||
};
|
||||
let (x, y) = self.doc_point(position);
|
||||
let grab = if side.upright() { x } else { y } - at;
|
||||
log::info!("lok: table border {} {} held", side.kind(), side.index());
|
||||
self.borders.held = Some(Held { side, grab, from: at, to: at, table, doc });
|
||||
true
|
||||
}
|
||||
|
||||
/// A held border follows the pointer; a hovered one shows.
|
||||
pub(super) fn move_border(&mut self, position: Point<Pixels>) -> bool {
|
||||
let Some(held) = &self.borders.held else {
|
||||
let side = self.border_at(position).map(|(side, _)| side);
|
||||
self.borders.hover = side.map(|side| if side.upright() { CursorStyle::ResizeColumn } else { CursorStyle::ResizeRow });
|
||||
return false;
|
||||
};
|
||||
let (x, y) = self.doc_point(position);
|
||||
let want = if held.side.upright() { x } else { y } - held.grab;
|
||||
let to = held.from + clamp(&held.table, held.side, want - held.from);
|
||||
if let Some(held) = self.borders.held.as_mut() {
|
||||
held.to = to;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Lets go at `position`: one command at the read's revision.
|
||||
pub(super) fn drop_border(&mut self, position: Point<Pixels>, cx: &mut Context<Self>) -> bool {
|
||||
if !self.move_border(position) {
|
||||
return false;
|
||||
}
|
||||
let Some(held) = self.borders.held.take() else {
|
||||
return false;
|
||||
};
|
||||
let offset = held.to - held.from;
|
||||
if offset == 0 {
|
||||
log::info!("lok: table border let go in place");
|
||||
return true;
|
||||
}
|
||||
log::info!("lok: table border {} {} moves {offset}", held.side.kind(), held.side.index());
|
||||
let (index, offset) = (held.side.index().to_string(), offset.to_string());
|
||||
if let Err(error) = self.run_at(&held.doc, "table.border", &[held.side.kind(), &index, &offset]) {
|
||||
cx.emit(LokError(format!("the table border did not move: {error}")));
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn holds_border(&self) -> bool {
|
||||
self.borders.held.is_some()
|
||||
}
|
||||
|
||||
/// The pointer a border asks for, while held or hovered.
|
||||
pub(super) fn border_pointer(&self) -> Option<CursorStyle> {
|
||||
match &self.borders.held {
|
||||
Some(held) if held.side.upright() => Some(CursorStyle::ResizeColumn),
|
||||
Some(_) => Some(CursorStyle::ResizeRow),
|
||||
None => self.borders.hover,
|
||||
}
|
||||
}
|
||||
|
||||
/// The guide line of a held border, in window pixels.
|
||||
pub(super) fn guide(&self) -> Option<Bounds<Pixels>> {
|
||||
let held = self.borders.held.as_ref()?;
|
||||
let (_, _, (start, end), _) = lines(&held.table).into_iter().find(|&(side, ..)| side == held.side)?;
|
||||
Some(if held.side.upright() {
|
||||
let (top, bottom) = (self.window_point((held.to, start)), self.window_point((held.to, end)));
|
||||
Bounds { origin: top, size: size(px(1.0), bottom.y - top.y) }
|
||||
} else {
|
||||
let (left, right) = (self.window_point((start, held.to)), self.window_point((end, held.to)));
|
||||
Bounds { origin: left, size: size(right.x - left.x, px(1.0)) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::table::Line;
|
||||
|
||||
fn table() -> Table {
|
||||
let line = |position, min, max| Line { position, min, max, hidden: false };
|
||||
Table {
|
||||
columns: Lines { left: 0, right: 9000, offset: 1000, inner: vec![line(3000, 300, 6000), line(6000, 3300, 8700)] },
|
||||
rows: Lines { left: 0, right: 800, offset: 2000, inner: vec![line(400, 100, 800)] },
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn borders_lie_where_the_kernel_puts_them() {
|
||||
let sides: Vec<_> = lines(&table()).into_iter().map(|(side, at, _, room)| (side, at, room)).collect();
|
||||
assert_eq!(
|
||||
sides,
|
||||
[
|
||||
(Side::ColumnLeft, 1000, 3000),
|
||||
(Side::Column(0), 4000, 3000),
|
||||
(Side::Column(1), 7000, 3000),
|
||||
(Side::ColumnRight, 10_000, 3000),
|
||||
(Side::Row(0), 2400, 400),
|
||||
(Side::RowBottom, 2800, 400),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_clamps_as_the_kernel_does() {
|
||||
let table = table();
|
||||
assert_eq!(clamp(&table, Side::Column(0), 500), 500, "inside its bounds");
|
||||
assert_eq!(clamp(&table, Side::Column(0), 9000), 2960, "up to max less the gap");
|
||||
assert_eq!(clamp(&table, Side::Column(1), -9000), -2700, "down to min");
|
||||
assert_eq!(clamp(&table, Side::ColumnLeft, 5000), 2960, "short of the first line");
|
||||
assert_eq!(clamp(&table, Side::ColumnRight, -8000), -2960, "past the last line");
|
||||
assert_eq!(clamp(&table, Side::RowBottom, -800), -360, "a row keeps the gap");
|
||||
assert_eq!(clamp(&table, Side::Row(0), 500), 360, "max less the gap");
|
||||
}
|
||||
}
|
||||
@@ -314,7 +314,7 @@ impl LokView {
|
||||
};
|
||||
window.focus(&self.focus, cx);
|
||||
self.right_at = (event.button == MouseButton::Right).then_some(event.position);
|
||||
if event.button == MouseButton::Left && self.grab(event.position) {
|
||||
if event.button == MouseButton::Left && (self.grab(event.position) || self.hold_border(event.position)) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -334,6 +334,10 @@ impl LokView {
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.drop_border(event.position, cx) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
if let Some(grip) = self.grip.take() {
|
||||
let to = self.doc_point(event.position);
|
||||
log::info!("lok: handle released");
|
||||
@@ -366,7 +370,7 @@ impl LokView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.dragging || self.grip.is_some() {
|
||||
if self.dragging || self.grip.is_some() || self.holds_border() {
|
||||
self.on_mouse_up(event, window, cx);
|
||||
}
|
||||
}
|
||||
@@ -383,6 +387,10 @@ impl LokView {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
if self.move_border(event.position) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
if self.dragging && event.pressed_button == Some(MouseButton::Left) {
|
||||
self.mouse(2, event.position, 1, 1, &event.modifiers);
|
||||
} else if event.pressed_button.is_none() {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use ely_gpui_component::theme::ActiveTheme;
|
||||
use gpui::{
|
||||
BorderStyle, Bounds, ContentMask, Context, Corners, CursorStyle, Edges, ElementInputHandler,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, Render, Role,
|
||||
BorderStyle, Bounds, ContentMask, Context, Corners, CursorStyle, DispatchPhase, Edges,
|
||||
ElementInputHandler, InteractiveElement, IntoElement, MouseButton, MouseMoveEvent, ParentElement, Render, Role,
|
||||
StatefulInteractiveElement, Styled, Window, canvas, div, fill, outline, px, quad, size,
|
||||
transparent_black,
|
||||
};
|
||||
@@ -59,7 +59,7 @@ impl Render for LokView {
|
||||
.aria_value(self.a11y.text.clone())
|
||||
.key_context(CONTEXT)
|
||||
.track_focus(&self.focus)
|
||||
.cursor(self.pointer)
|
||||
.cursor(self.border_pointer().unwrap_or(self.pointer))
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.bg(canvas_color)
|
||||
@@ -164,6 +164,9 @@ impl Render for LokView {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(guide) = painted.guide {
|
||||
window.paint_quad(fill(guide, frame_color));
|
||||
}
|
||||
if let Some(caret) = painted.caret.filter(|_| focus.is_focused(window))
|
||||
{
|
||||
let caret = Bounds {
|
||||
@@ -173,6 +176,19 @@ impl Render for LokView {
|
||||
window.paint_quad(fill(caret, caret_color));
|
||||
}
|
||||
});
|
||||
if painted.guide.is_some() {
|
||||
// A held border follows the pointer out of the view.
|
||||
let this = this.clone();
|
||||
window.on_mouse_event(move |event: &MouseMoveEvent, phase, _, app| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
this.update(app, |view, cx| {
|
||||
if view.move_border(event.position) {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
window.handle_input(&focus, ElementInputHandler::new(bounds, this), app);
|
||||
},
|
||||
)
|
||||
@@ -196,6 +212,8 @@ pub(super) struct Painted {
|
||||
pub(super) frame: Option<(Bounds<Pixels>, Vec<Bounds<Pixels>>)>,
|
||||
/// The references of a formula being typed.
|
||||
pub(super) marks: Vec<(Bounds<Pixels>, gpui::Hsla)>,
|
||||
/// A held table border's guide line.
|
||||
pub(super) guide: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
impl LokView {
|
||||
@@ -357,6 +375,7 @@ impl LokView {
|
||||
cell: self.cell.cursor.map(rect),
|
||||
frame,
|
||||
marks: self.marks(),
|
||||
guide: self.guide(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
mod borders;
|
||||
mod clip;
|
||||
mod commands;
|
||||
mod fit;
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
use super::*;
|
||||
use crate::client::tests::poll_until;
|
||||
use gpui::{CursorStyle, MouseButton, MouseDownEvent, MouseUpEvent, TestAppContext};
|
||||
|
||||
const TABLE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||
<office:body><office:text>
|
||||
<table:table table:name="T"><table:table-column table:number-columns-repeated="2"/>
|
||||
<table:table-row><table:table-cell><text:p>left</text:p></table:table-cell><table:table-cell><text:p>right</text:p></table:table-cell></table:table-row>
|
||||
<table:table-row><table:table-cell><text:p>down</text:p></table:table-cell><table:table-cell><text:p>more</text:p></table:table-cell></table:table-row>
|
||||
</table:table><text:p>after</text:p></office:text></office:body>
|
||||
</office:document>"#;
|
||||
|
||||
const WAIT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
/// The view takes the cursor's table the kernel reports.
|
||||
fn borders_read(view: &mut LokView) -> crate::table::Table {
|
||||
let doc = view.doc.clone();
|
||||
let mut worker = view.worker.lock().expect("the worker lock");
|
||||
worker.post_key(&doc, 0, 1027).expect("Right moves the cursor");
|
||||
let seen = poll_until(&mut worker, &doc, &Events::default(), WAIT, |seen| seen.table.is_some());
|
||||
let table = seen.table.expect("the cursor's table");
|
||||
let now = worker.now(&doc).expect("the revision");
|
||||
drop(worker);
|
||||
view.doc = now;
|
||||
view.read_borders(Some(table.clone()), view.doc.rev);
|
||||
table
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dragged_border_moves_once_where_its_guide_shows() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
let cx = TestAppContext::single();
|
||||
let (view, _) = cx.update(|app| live_view_of(app, &install, "table.fodt", TABLE));
|
||||
let (line, moved) = cx.update(|app| {
|
||||
view.update(app, |view, cx| {
|
||||
let table = borders_read(view);
|
||||
let (columns, rows) = (&table.columns, &table.rows);
|
||||
let line = columns.offset + columns.inner[0].position;
|
||||
let middle = rows.offset + (rows.left + rows.right) / 2;
|
||||
let on = view.window_point((line, middle));
|
||||
view.move_border(on);
|
||||
assert_eq!(view.border_pointer(), Some(CursorStyle::ResizeColumn), "a hovered line asks to resize");
|
||||
assert!(view.hold_border(on), "the line is held");
|
||||
assert!(view.drop_border(on, cx), "let go in place");
|
||||
assert!(view.hold_border(on));
|
||||
let end = on + gpui::point(px(40.0), px(3.0));
|
||||
assert!(view.move_border(end));
|
||||
let guide = view.guide().expect("a guide line");
|
||||
let to = view.doc_point(guide.origin).0;
|
||||
assert!(to > line, "the guide follows the pointer: {line} to {to}");
|
||||
assert!(view.drop_border(end, cx), "the drop runs");
|
||||
assert!(!view.holds_border());
|
||||
(line, to - line)
|
||||
})
|
||||
});
|
||||
for _ in 0..80 {
|
||||
cx.executor().tick();
|
||||
}
|
||||
cx.update(|app| {
|
||||
let view = view.read(app);
|
||||
let mut worker = view.worker.lock().expect("the worker lock");
|
||||
let seen = poll_until(&mut worker, &view.doc, &Events::default(), WAIT, |seen| {
|
||||
seen.table.as_ref().is_some_and(|table| table.columns.offset + table.columns.inner[0].position != line)
|
||||
});
|
||||
let table = seen.table.expect("still in the table");
|
||||
let landed = table.columns.offset + table.columns.inner[0].position;
|
||||
assert!((landed - line - moved).abs() <= 1, "the kernel lands it at the guide: {line} + {moved} vs {landed}");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_held_border_keeps_its_table_and_a_newer_read_refuses_it() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
let cx = TestAppContext::single();
|
||||
let (view, _) = cx.update(|app| live_view_of(app, &install, "table.fodt", TABLE));
|
||||
let errors = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
|
||||
let line = cx.update(|app| {
|
||||
let errors = errors.clone();
|
||||
app.subscribe(&view, move |_, error: &LokError, _| errors.borrow_mut().push(error.0.clone())).detach();
|
||||
view.update(app, |view, cx| {
|
||||
let table = borders_read(view);
|
||||
let (columns, rows) = (&table.columns, &table.rows);
|
||||
let line = columns.offset + columns.inner[0].position;
|
||||
let on = view.window_point((line, rows.offset + (rows.left + rows.right) / 2));
|
||||
assert!(view.hold_border(on), "the line is held");
|
||||
let doc = view.doc.clone();
|
||||
let mut worker = view.worker.lock().expect("the worker lock");
|
||||
worker.post_key(&doc, 'a' as u32, 512).expect("a types");
|
||||
view.doc = worker.now(&doc).expect("the revision");
|
||||
drop(worker);
|
||||
let mut gone = table.clone();
|
||||
gone.columns.inner.clear();
|
||||
view.read_borders(Some(gone), view.doc.rev);
|
||||
let end = on + gpui::point(px(40.0), px(0.0));
|
||||
assert!(view.move_border(end), "it moves by its own table");
|
||||
assert!(view.guide().is_some(), "its guide still shows");
|
||||
assert!(view.drop_border(end, cx), "the drop runs");
|
||||
line
|
||||
})
|
||||
});
|
||||
for _ in 0..60 {
|
||||
for _ in 0..20 {
|
||||
cx.executor().tick();
|
||||
}
|
||||
if !errors.borrow().is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(errors.borrow().iter().any(|error| error.contains("stale")), "{:?}", errors.borrow());
|
||||
cx.update(|app| {
|
||||
let view = view.read(app);
|
||||
let mut worker = view.worker.lock().expect("the worker lock");
|
||||
worker.post_key(&view.doc, 0, 1027).expect("Right moves the cursor");
|
||||
let seen = poll_until(&mut worker, &view.doc, &Events::default(), WAIT, |seen| seen.table.is_some());
|
||||
let table = seen.table.expect("still in the table");
|
||||
assert_eq!(table.columns.offset + table.columns.inner[0].position, line, "the old drag never lands");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_in_a_short_row_reaches_the_document() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
let mut cx = TestAppContext::single();
|
||||
let (view, _) = cx.update(|app| live_view_of(app, &install, "table.fodt", TABLE));
|
||||
let window = cx.add_window(|_, _| super::input::Blank);
|
||||
cx.update_window(window.into(), |_, window, app| {
|
||||
view.update(app, |view, cx| {
|
||||
view.set_zoom(0.5, cx);
|
||||
let table = borders_read(view);
|
||||
let (columns, rows) = (&table.columns, &table.rows);
|
||||
let x = columns.offset + (columns.left + columns.inner[0].position) / 2;
|
||||
let middle = view.window_point((x, rows.offset + (rows.inner[0].position + rows.right) / 2));
|
||||
let line = view.window_point((x, rows.offset + rows.inner[0].position));
|
||||
assert!(f32::from(line.y - middle.y).abs() < 6.0, "the row is shorter than two reaches");
|
||||
let down = |position| MouseDownEvent {
|
||||
button: MouseButton::Left,
|
||||
position,
|
||||
modifiers: Default::default(),
|
||||
click_count: 1,
|
||||
first_mouse: false,
|
||||
};
|
||||
let up = MouseUpEvent { button: MouseButton::Left, position: middle, modifiers: Default::default(), click_count: 1 };
|
||||
view.on_mouse_down(&down(middle), window, cx);
|
||||
assert!(!view.holds_border(), "the row's middle goes to the document");
|
||||
view.on_mouse_up(&up, window, cx);
|
||||
view.on_mouse_down(&down(line), window, cx);
|
||||
assert!(view.holds_border(), "the line itself is held");
|
||||
assert!(view.drop_border(line, cx));
|
||||
})
|
||||
})
|
||||
.expect("the window lives");
|
||||
}
|
||||
|
||||
/// The batch whose table has `count` inner columns.
|
||||
fn table_batch(view: &LokView, count: usize) -> Events {
|
||||
let mut worker = view.worker.lock().expect("the worker lock");
|
||||
let seen = poll_until(&mut worker, &view.doc, &Events::default(), WAIT, |seen| {
|
||||
seen.table.as_ref().is_some_and(|table| table.columns.inner.len() == count)
|
||||
});
|
||||
assert_eq!(seen.table.as_ref().map(|table| table.columns.inner.len()), Some(count));
|
||||
seen
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_late_batch_keeps_its_own_revision() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
let cx = TestAppContext::single();
|
||||
let (view, _) = cx.update(|app| live_view_of(app, &install, "table.fodt", TABLE));
|
||||
let errors = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
|
||||
let before = cx.update(|app| {
|
||||
let errors = errors.clone();
|
||||
app.subscribe(&view, move |_, error: &LokError, _| errors.borrow_mut().push(error.0.clone())).detach();
|
||||
view.update(app, |view, cx| {
|
||||
borders_read(view);
|
||||
let old = table_batch(view, 1);
|
||||
let table = old.table.clone().expect("the old table");
|
||||
let now = {
|
||||
let mut worker = view.worker.lock().expect("the worker lock");
|
||||
worker.run(&view.doc, "table.column_right", &[]).expect("a column goes in");
|
||||
worker.now(&view.doc).expect("the revision")
|
||||
};
|
||||
let before = table_batch(view, 2).table.expect("the new table").columns.inner[0].position;
|
||||
view.seen_revision(now.rev);
|
||||
view.apply_batch(old, None, cx);
|
||||
let on = view.window_point((table.columns.offset + table.columns.inner[0].position, table.rows.offset + 100));
|
||||
assert!(view.hold_border(on), "the old table's line is held");
|
||||
let end = on + gpui::point(px(20.0), px(0.0));
|
||||
assert!(view.drop_border(end, cx), "the drop runs");
|
||||
before
|
||||
})
|
||||
});
|
||||
for _ in 0..60 {
|
||||
for _ in 0..20 {
|
||||
cx.executor().tick();
|
||||
}
|
||||
if !errors.borrow().is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(errors.borrow().iter().any(|error| error.contains("stale")), "{:?}", errors.borrow());
|
||||
let after = cx.update(|app| view.update(app, |view, _| table_batch(view, 2).table.expect("the table").columns.inner[0].position));
|
||||
assert_eq!(after, before, "the old read never moves the new table");
|
||||
}
|
||||
|
||||
/// The real view, narrower than its window.
|
||||
struct Narrow(gpui::Entity<LokView>);
|
||||
|
||||
impl gpui::Render for Narrow {
|
||||
fn render(&mut self, _: &mut gpui::Window, _: &mut gpui::Context<Self>) -> impl gpui::IntoElement {
|
||||
use gpui::{ParentElement, Styled};
|
||||
gpui::div().w(px(450.0)).h(px(300.0)).child(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_border_let_go_outside_the_view_lands_there() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(ely_gpui_component::init_for_tests);
|
||||
let (view, _) = cx.update(|app| live_view_of(app, &install, "table.fodt", TABLE));
|
||||
let (_, vcx) = cx.add_window_view(|_, _| Narrow(view.clone()));
|
||||
vcx.run_until_parked();
|
||||
let (line, on) = vcx.update(|_, app| {
|
||||
view.update(app, |view, cx| {
|
||||
let table = borders_read(view);
|
||||
cx.notify();
|
||||
let line = table.columns.offset + table.columns.inner[0].position;
|
||||
(line, view.window_point((line, table.rows.offset + 100)))
|
||||
})
|
||||
});
|
||||
vcx.run_until_parked();
|
||||
assert!(on.x > px(380.0) && on.x < px(440.0), "the line sits near the view's edge: {on:?}");
|
||||
vcx.simulate_mouse_down(on, MouseButton::Left, Default::default());
|
||||
assert!(vcx.update(|_, app| view.read(app).holds_border()), "the line is held");
|
||||
let inside = gpui::point(px(445.0), on.y);
|
||||
vcx.simulate_mouse_move(inside, MouseButton::Left, Default::default());
|
||||
let at = |vcx: &mut gpui::VisualTestContext| vcx.update(|_, app| view.read(app).guide().expect("a guide").origin.x);
|
||||
let held = at(vcx);
|
||||
let outside = gpui::point(px(490.0), on.y);
|
||||
vcx.simulate_mouse_move(outside, MouseButton::Left, Default::default());
|
||||
let out = at(vcx);
|
||||
assert!(out > held, "the guide follows out of the view: {held:?} to {out:?}");
|
||||
let wanted = vcx.update(|_, app| view.read(app).doc_point(gpui::point(out, on.y)).0);
|
||||
vcx.simulate_mouse_up(outside, MouseButton::Left, Default::default());
|
||||
assert!(!vcx.update(|_, app| view.read(app).holds_border()), "let go");
|
||||
vcx.run_until_parked();
|
||||
let landed = vcx.update(|_, app| {
|
||||
let view = view.read(app);
|
||||
let mut worker = view.worker.lock().expect("the worker lock");
|
||||
let seen = poll_until(&mut worker, &view.doc, &Events::default(), WAIT, |seen| {
|
||||
seen.table.as_ref().is_some_and(|table| table.columns.offset + table.columns.inner[0].position != line)
|
||||
});
|
||||
let table = seen.table.expect("still in the table");
|
||||
table.columns.offset + table.columns.inner[0].position
|
||||
});
|
||||
assert!((landed - wanted).abs() <= 1, "it lands at the release: {landed} vs {wanted}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_still_click_beside_a_border_sends_nothing() {
|
||||
let Some(install) = crate::client::tests::install() else {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
};
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(ely_gpui_component::init_for_tests);
|
||||
let (view, _) = cx.update(|app| live_view_of(app, &install, "table.fodt", TABLE));
|
||||
let (_, vcx) = cx.add_window_view(|_, _| Narrow(view.clone()));
|
||||
vcx.run_until_parked();
|
||||
let on = vcx.update(|_, app| {
|
||||
view.update(app, |view, cx| {
|
||||
let table = borders_read(view);
|
||||
cx.notify();
|
||||
let line = table.columns.offset + table.columns.inner[0].position;
|
||||
view.window_point((line, table.rows.offset + 100)) + gpui::point(px(3.0), px(0.0))
|
||||
})
|
||||
});
|
||||
vcx.run_until_parked();
|
||||
let sent = vcx.update(|_, app| view.read(app).sent());
|
||||
vcx.simulate_mouse_down(on, MouseButton::Left, Default::default());
|
||||
assert!(vcx.update(|_, app| view.read(app).holds_border()), "the line is held off its centre");
|
||||
vcx.simulate_mouse_up(on, MouseButton::Left, Default::default());
|
||||
assert!(!vcx.update(|_, app| view.read(app).holds_border()), "let go");
|
||||
assert_eq!(vcx.update(|_, app| view.read(app).sent()), sent, "a still click sends no command");
|
||||
}
|
||||
@@ -60,7 +60,7 @@ fn typing_and_composing_reach_the_document() {
|
||||
assert_eq!(saved(&view, &mut cx), ["ab你", "Hello 世界"]);
|
||||
}
|
||||
|
||||
struct Blank;
|
||||
pub(super) struct Blank;
|
||||
|
||||
impl gpui::Render for Blank {
|
||||
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl gpui::IntoElement {
|
||||
|
||||
@@ -82,5 +82,5 @@ fn pack_events(batch: Events) -> Vec<u8> {
|
||||
.i64(signed(a11y.caret))
|
||||
.i64(signed(start))
|
||||
.i64(signed(end));
|
||||
batch.cell.pack(pack).bytes()
|
||||
crate::table::Table::pack(batch.table.as_ref(), batch.cell.pack(pack)).bytes()
|
||||
}
|
||||
+1
-1
@@ -248,7 +248,7 @@
|
||||
|
||||
- [x] R5.1 属性栏:光标在表格里时表格组排在最前(同 Word 的表格布局页签),否则次序不变(DOC-07) — R3 PASS(组件 9ff403d:属性组按标题记折叠)
|
||||
- [x] R5.2 右键菜单(DOC-02、07、08、11、12):原先只在内核菜单带拼写建议时才显示,其余右键无反应;改为照 Word 按目标出 Ely 的菜单,开在所点之处:剪切、复制、粘贴为图标行;正文为链接…、新建批注;表格单元格另有插入 ▸(左侧列、右侧列、上方行、下方行)、删除 ▸(行、列、表格)、合并单元格、拆分单元格 ▸(两列、两行)、平均分布各行与各列;图片为文字环绕 ▸、置于顶层 ▸、置于底层 ▸、插入题注…、新建批注;拼写错词时建议与“全部忽略”在最上。目标与各项可否取自内核这次给的菜单,命令只经 `COMMANDS`,文案进中英目录 — R2 PASS(链接…随内核的 `.uno:InsertHyperlink`:须选中可编辑的文字,未选中时灰显)
|
||||
- [ ] R5.3 拖动表格边线(DOC-07):读内核的 `LOK_CALLBACK_TABLE_SELECTED`(光标所在表格的列与行边线),指针在边线上变为调整指针,拖动时画参考线,松开以 `.uno:TableChangeCurrentBorderPosition` 改列宽或行高(左、内、右列边线;内、下行边线),一步撤销,按读到边线时的修订执行;内核在 LOK 下有意不处理边线的指针与拖动(`SwEditWin::changeMousePointer`、`RulerColumnDrag` 依赖 LOK 没有的标尺);未进入的表格先单击一次
|
||||
- [x] R5.3 拖动表格边线(DOC-07):读内核的 `LOK_CALLBACK_TABLE_SELECTED`(光标所在表格的列与行边线),指针在边线上变为调整指针,拖动时画参考线,松开以 `.uno:TableChangeCurrentBorderPosition` 改列宽或行高(左、内、右列边线;内、下行边线),一步撤销,按读到边线时的修订执行;内核在 LOK 下有意不处理边线的指针与拖动(`SwEditWin::changeMousePointer`、`RulerColumnDrag` 依赖 LOK 没有的标尺);未进入的表格先单击一次 — 三轮用完,末轮修复已测(按住处与边线的偏移保留,原地单击不发命令;松开取所在处,出视图照跟;表格带其批次的修订)
|
||||
- [ ] R5.4 验收:Windows 真窗口逐项核对(右键四种目标、加行加列、拖动列宽与行高、撤销、保存后重开);`agents.md` 随之;逐条核对表
|
||||
- [ ] R5.5 ElyTable 与 ElyShow 核对同类问题(用户 2026-10-14 要求):右键是否有菜单、表格能否加删行列与调尺寸;查出的缺口照 R5.2、R5.3 的做法补齐
|
||||
- [ ] R5.6 Gitea #1:ElyShow 左侧缩略图条滚轮不滚动(同窗口右栏正常,Linux Wayland 报);查明根因再修,加滚轮回归测试
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
FAIL
|
||||
|
||||
- [P1] `crates/ely-lok/src/view/borders.rs:97` — New event batches replace the table and revision used by an active drag, while `Held` retains the old border index. Reproduce: insert a third column, hold the second inner border, undo the insertion while holding, then move the mouse. `clamp` at line 87 indexes entry 1 of a one-entry array and panics. When the index still exists, dropping instead applies the old drag to the changed table: the kernel probe moved the new border from 3323 to 3623 twips without a stale error. This breaks R5.3's read-revision requirement. Keep the drag's geometry and exact supplying revision together; never reinterpret its index against a newer table, and reject or cancel invalidated drags. Add regressions for undo and topology changes during a drag. Evidence: `.work/R5.3-r1-probes.log`.
|
||||
|
||||
- [P2] `crates/ely-lok/src/view/borders.rs:103` — The fixed six-pixel reach on both sides of every border consumes the interior of short rows. A normal 276-twip row is only 9.2 pixels high at 50% zoom, so the adjacent row-border hit regions overlap across its entire height. After entering the table, clicking the middle of the second row starts a border drag instead of positioning the caret; the input-handler probe reports `held border=true, sent delta=0`. Limit border hit regions according to the displayed neighboring geometry so ordinary cell clicks remain available, and test those clicks through `on_mouse_down` at reduced zoom. Evidence: `.work/R5.3-r1-hit.log`.
|
||||
|
||||
- [P3] `crates/ely-lok/src/view/borders.rs:129` — This new comment, the comment at line 145, and `crates/ely-lok/src/view/tests/borders.rs:16` each contain eleven words. They fail the project's explicit one-line, ten-word comment rule. Shorten all three to at most ten words.
|
||||
@@ -0,0 +1,5 @@
|
||||
FAIL
|
||||
|
||||
- [P1] `crates/ely-lok/src/view/borders.rs:115` — The table snapshot still receives the latest view revision rather than its supplying batch's revision. `apply_batch` first calls `seen_revision`, which keeps the maximum because input completions and event batches can arrive out of order. Reproduce: receive a two-column table at revision 1, insert a column and process its revision-2 input completion, then apply the delayed revision-1 batch and drag its inner border. The kernel probe moved the new table's border from 3324 to 3624 twips without a stale error. Holding geometry through subsequent updates fixes round one's active-drag case, but this ordering still violates R5.3's read-revision requirement. Store the exact supplying `batch.revision` with the geometry, independently of the latest view revision, and add an out-of-order batch/completion regression. Evidence: `.work/R5.3-r2/probes.log`.
|
||||
|
||||
- [P2] `crates/ely-lok/src/view/input.rs:337` — Releasing a border ignores `event.position` and commits the last position received by `on_mouse_move`. GPUI only dispatches that move handler while the pointer is over the canvas, so dragging outside freezes the guide and commits the wrong width. Reproduce with a 450-pixel canvas: hold the inner column border at x=426.87, move to x=440, continue outside to x=480, then release. Dispatching through the same GPUI registrations and actual LokView handlers leaves the guide at x=440 and commits 5182 twips instead of the release position's 5782. Track pointer movement throughout the held drag and apply the release coordinates before issuing the single border command; cover release outside the canvas with an event-dispatch regression. Evidence: `.work/R5.3-r2/probes.log`.
|
||||
@@ -0,0 +1,5 @@
|
||||
FAIL
|
||||
|
||||
- [P2] `crates/ely-lok/src/view/borders.rs:163` — The drag uses the pointer's absolute position without retaining its initial offset from the border. Because `drop_border` calls `move_border` even without a mouse-move event, a stationary click within the border's hit area changes the document. Reproduce at 100% zoom: press and release 3 pixels right of a two-column table's inner border without moving. The painted-LokView GPUI dispatch probe sent one command and moved the kernel border from 6403 to 6448 twips. Preserve the press position or grab offset, derive movement from that origin, and issue no command for a stationary click. Add an off-center stationary-click regression while keeping release-coordinate handling. Evidence: `.work/R5.3-r3/probes.log` and `.work/R5.3-r3/probe.source.log`.
|
||||
|
||||
- [P3] `crates/ely-lok/src/view/borders.rs:172` — The comment “Lets go at `position`: one command, at the held table's revision.” contains eleven words. Running `tests/scripts/comments.py` fails on this line under the project's ten-word limit. Shorten it to at most ten words. Evidence: `.work/R5.3-r3/comments.log`.
|
||||
Reference in new issue
Block a user