mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-02 06:57:04 -06:00
Merge the landing branch into land-r8
# Conflicts: # contributors/people.toml # crates/engine/src/cmd/file.rs # crates/pptx/src/opc.rs # crates/pptx/tests/roundtrip.rs # crates/ui-egui/src/backstage.rs # crates/ui-egui/src/lib.rs
This commit is contained in:
+684
-1
@@ -293,7 +293,8 @@ fn writable_format_for_path_rejects_formats_deckcraft_does_not_write() {
|
||||
assert_eq!(f("a.pdf"), Some("pdf"));
|
||||
assert_eq!(f("a.jpg"), Some("jpeg"));
|
||||
assert_eq!(f("a.txt"), Some("outline"));
|
||||
for path in ["a.odp", "a.gif", "a.svg", "a.outline", "a", "dir.v2/a"] {
|
||||
assert_eq!(f("a.gif"), Some("gif"));
|
||||
for path in ["a.odp", "a.bmp", "a.svg", "a.outline", "a", "dir.v2/a"] {
|
||||
assert_eq!(f(path), None, "{path}");
|
||||
}
|
||||
}
|
||||
@@ -567,11 +568,307 @@ fn grown_table_frame_covers_its_drawn_rows() {
|
||||
assert!(matches!(s.hit_test(below_stored, 1.0), Some(tools::Hit::Shape { .. })));
|
||||
}
|
||||
|
||||
/// A table at (100, 100): columns `cols` wide, rows 40 pt tall, cells holding `data`.
|
||||
fn table_at(s: &mut Session, cols: &[f64], data: serde_json::Value) -> deckcraft_model::ShapeId {
|
||||
let rows = data.as_array().map_or(1, Vec::len);
|
||||
let w: f64 = cols.iter().sum();
|
||||
let r =
|
||||
s.execute("insert.table", &json!({"rows": rows, "cols": cols.len(), "rect": [100.0, 100.0, w, 40.0 * rows as f64], "data": data})).unwrap();
|
||||
let id = deckcraft_model::ShapeId(r["id"].as_u64().unwrap() as u32);
|
||||
for (c, cw) in cols.iter().enumerate() {
|
||||
s.execute("table.columnWidth", &json!({"id": id, "col": c, "width": cw})).unwrap();
|
||||
}
|
||||
s.execute("edit.deselect", &json!({})).unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
fn table_of(s: &Session, id: deckcraft_model::ShapeId) -> deckcraft_model::Table {
|
||||
match &s.doc().unwrap().shape(id).unwrap().kind {
|
||||
deckcraft_model::ShapeKind::Table(t) => t.clone(),
|
||||
k => panic!("not a table: {k:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn tev(kind: PointerKind, x: f64, y: f64) -> PointerEvent {
|
||||
PointerEvent { kind, x, y, mods: Mods::default(), tol: 3.0, pressure: None }
|
||||
}
|
||||
|
||||
fn text_sel(s: &Session) -> Option<TextSel> {
|
||||
s.doc().unwrap().selection.text.clone()
|
||||
}
|
||||
|
||||
/// Slide point of the caret before character `ch` of paragraph `para` in the text being edited.
|
||||
fn caret_point(s: &Session, para: usize, ch: usize) -> deckcraft_geom::Point {
|
||||
let st = s.doc().unwrap();
|
||||
text_point(st, st.selection.text.as_ref().unwrap(), para, ch)
|
||||
}
|
||||
|
||||
/// Slide point of the caret before character `ch` of paragraph `para` of text `t`.
|
||||
fn text_point(st: &DocState, t: &TextSel, para: usize, ch: usize) -> deckcraft_geom::Point {
|
||||
let (l, m) = cmd::text::text_frame(st, t).unwrap();
|
||||
let (x, top, bot) = l.caret(deckcraft_text::Pos { para, ch }).unwrap();
|
||||
m * deckcraft_geom::Point::new(x, (top + bot) / 2.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_text_is_laid_out_where_the_cell_draws_it() {
|
||||
// The caret and hit testing of a cell's text use the cell's rect, not the table's.
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[200.0, 200.0], json!([["Hello world", "b"], ["c", ""]]));
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [0, 1], "at": [0, 0]})).unwrap();
|
||||
let c = caret_point(&s, 0, 0);
|
||||
assert!(c.x > 300.0 && c.x < 320.0 && c.y > 100.0 && c.y < 140.0, "caret of cell (0, 1) at {c:?}");
|
||||
// An empty cell still has a caret.
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [1, 1]})).unwrap();
|
||||
let c = caret_point(&s, 0, 0);
|
||||
assert!(c.x > 300.0 && c.y > 140.0 && c.y < 180.0, "caret of empty cell (1, 1) at {c:?}");
|
||||
// Hit testing: the start and the end of "Hello world".
|
||||
let st = s.doc().unwrap();
|
||||
let t = TextSel { shape: id, cell: Some((0, 0)), ..Default::default() };
|
||||
assert_eq!(cmd::text::text_pos_at(st, &t, deckcraft_geom::Point::new(101.0, 115.0)), (0, 0));
|
||||
assert_eq!(cmd::text::text_pos_at(st, &t, deckcraft_geom::Point::new(295.0, 115.0)), (0, 11));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_click_in_a_cell_selects_the_word_clicked() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[200.0, 200.0], json!([["Hello world", "b"], ["c", "d"]]));
|
||||
// Where "world" is drawn.
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [0, 0]})).unwrap();
|
||||
let p = caret_point(&s, 0, 8);
|
||||
s.execute("text.exit", &json!({})).unwrap();
|
||||
s.execute("edit.deselect", &json!({})).unwrap();
|
||||
// As the UI sends it: press, release, press, double-click (on release), then the release.
|
||||
s.pointer(tev(PointerKind::Down, p.x, p.y)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, p.x, p.y)).unwrap();
|
||||
assert!(text_sel(&s).is_none(), "a click on an unselected table selects it");
|
||||
s.pointer(tev(PointerKind::Down, p.x, p.y)).unwrap();
|
||||
s.pointer(tev(PointerKind::DoubleClick, p.x, p.y)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, p.x, p.y)).unwrap();
|
||||
let t = text_sel(&s).unwrap();
|
||||
assert_eq!((t.cell, t.ordered()), (Some((0, 0)), ((0, 6), (0, 11))), "the word under the double-click");
|
||||
// The control channel sends the double-click alone.
|
||||
s.execute("text.exit", &json!({})).unwrap();
|
||||
s.execute("edit.deselect", &json!({})).unwrap();
|
||||
s.pointer(tev(PointerKind::DoubleClick, p.x, p.y)).unwrap();
|
||||
assert_eq!(text_sel(&s).map(|t| t.ordered()), Some(((0, 6), (0, 11))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicks_while_editing_a_cell_move_the_caret_between_cells() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[200.0, 200.0], json!([["Hello world", "Second cell"], ["c", "d"]]));
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [0, 1], "at": [0, 0]})).unwrap();
|
||||
let p6 = caret_point(&s, 0, 6);
|
||||
let p11 = caret_point(&s, 0, 11);
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [1, 0]})).unwrap();
|
||||
// A click in another cell of the table moves the caret there, at the click.
|
||||
s.pointer(tev(PointerKind::Down, p6.x, p6.y)).unwrap();
|
||||
let t = text_sel(&s).unwrap();
|
||||
assert_eq!((t.shape, t.cell, t.caret, t.anchor), (id, Some((0, 1)), (0, 6), (0, 6)));
|
||||
// Dragging selects within that cell, even past its edge.
|
||||
s.pointer(tev(PointerKind::Drag, p11.x, p11.y)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 600.0, p11.y)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 600.0, p11.y)).unwrap();
|
||||
let t = text_sel(&s).unwrap();
|
||||
assert_eq!((t.cell, t.ordered()), (Some((0, 1)), ((0, 6), (0, 11))));
|
||||
// Shift+click in another cell starts a new caret there instead of extending across cells.
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
s.pointer(PointerEvent { mods: shift, ..tev(PointerKind::Down, 150.0, 165.0) }).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 150.0, 165.0)).unwrap();
|
||||
let t = text_sel(&s).unwrap();
|
||||
assert_eq!((t.cell, t.is_range()), (Some((1, 0)), false));
|
||||
// Typing goes into the clicked cell.
|
||||
s.execute("text.insert", &json!({"text": "X"})).unwrap();
|
||||
assert_eq!(table_of(&s, id).cell(1, 0).unwrap().text.text(), "cX");
|
||||
// A click outside the table stops editing.
|
||||
s.pointer(tev(PointerKind::Down, 50.0, 400.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 50.0, 400.0)).unwrap();
|
||||
assert!(text_sel(&s).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_in_a_selected_table_enters_the_clicked_cell() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[200.0, 200.0], json!([["a", "b"], ["c", "Hello world"]]));
|
||||
s.pointer(tev(PointerKind::Down, 350.0, 160.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 350.0, 160.0)).unwrap();
|
||||
assert_eq!((s.doc().unwrap().selection.shapes.clone(), text_sel(&s)), (vec![id], None), "the first click selects the table");
|
||||
let st = s.doc().unwrap();
|
||||
let p = text_point(st, &TextSel { shape: id, cell: Some((1, 1)), ..Default::default() }, 0, 6);
|
||||
s.pointer(tev(PointerKind::Down, p.x, p.y)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, p.x, p.y)).unwrap();
|
||||
let t = text_sel(&s).unwrap();
|
||||
assert_eq!((t.cell, t.caret), (Some((1, 1)), (0, 6)), "caret at the click");
|
||||
// Dragging a selected table still moves it.
|
||||
s.execute("text.exit", &json!({})).unwrap();
|
||||
s.pointer(tev(PointerKind::Down, 150.0, 120.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 170.0, 130.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 170.0, 130.0)).unwrap();
|
||||
assert!(text_sel(&s).is_none());
|
||||
assert_ne!(s.doc().unwrap().shape(id).unwrap().xfrm.unwrap().x, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caret_moves_by_line_in_a_wrapped_cell() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[90.0, 200.0], json!([["one two three four five six seven", "b"]]));
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [0, 0], "at": [0, 0]})).unwrap();
|
||||
s.execute("text.move", &json!({"to": "down"})).unwrap();
|
||||
let down = text_sel(&s).unwrap().caret;
|
||||
assert!(down.0 == 0 && down.1 > 0, "down goes to the next line of the paragraph: {down:?}");
|
||||
s.execute("text.move", &json!({"to": "lineEnd"})).unwrap();
|
||||
let end = text_sel(&s).unwrap().caret;
|
||||
assert!(end.1 > down.1 && end.1 < 33, "end of the second line: {end:?}");
|
||||
s.execute("text.move", &json!({"to": "up"})).unwrap();
|
||||
assert!(text_sel(&s).unwrap().caret.1 < down.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_borders_are_hit_where_they_are_drawn() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[100.0, 100.0, 100.0], json!([["a", "b", "c"], ["d", "e", "f"]]));
|
||||
let hit = |s: &Session, x: f64, y: f64| s.hit_test(deckcraft_geom::Point::new(x, y), 3.0);
|
||||
assert_eq!(hit(&s, 201.0, 125.0), Some(tools::Hit::TableColumn { id, col: 0 }));
|
||||
assert_eq!(hit(&s, 400.0, 125.0), Some(tools::Hit::TableColumn { id, col: 2 }), "the right edge");
|
||||
assert_eq!(hit(&s, 150.0, 139.0), Some(tools::Hit::TableRow { id, row: 0 }));
|
||||
assert_eq!(hit(&s, 150.0, 181.0), Some(tools::Hit::TableRow { id, row: 1 }), "the bottom edge");
|
||||
assert!(matches!(hit(&s, 100.0, 125.0), Some(tools::Hit::Shape { .. })), "the left edge moves the table");
|
||||
assert!(matches!(hit(&s, 150.0, 125.0), Some(tools::Hit::Shape { .. })));
|
||||
// Hovering a border shows it (the UI picks the resize cursor from the hover).
|
||||
s.pointer(tev(PointerKind::Move, 300.0, 170.0)).unwrap();
|
||||
assert_eq!(s.tool.hover, Some(tools::Hit::TableColumn { id, col: 1 }));
|
||||
// A border inside a merged cell isn't there.
|
||||
s.execute("table.merge", &json!({"id": id, "from": [0, 0], "to": [0, 1]})).unwrap();
|
||||
assert!(matches!(hit(&s, 200.0, 120.0), Some(tools::Hit::Shape { .. })));
|
||||
assert_eq!(hit(&s, 200.0, 160.0), Some(tools::Hit::TableColumn { id, col: 0 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_column_borders_resizes_columns() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[100.0, 100.0, 100.0], json!([["a", "b", "c"], ["d", "e", "f"]]));
|
||||
let undo_depth = s.doc().unwrap().history.undo.len();
|
||||
// An inner border: the two columns share the change, the table keeps its width.
|
||||
s.pointer(tev(PointerKind::Down, 200.0, 125.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 215.0, 125.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 230.0, 128.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 230.0, 128.0)).unwrap();
|
||||
assert_eq!(table_of(&s, id).cols, vec![130.0, 70.0, 100.0]);
|
||||
assert_eq!(s.doc().unwrap().shape(id).unwrap().xfrm.unwrap().w, 300.0);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo_depth + 1, "one undo step per drag");
|
||||
// Columns don't collapse.
|
||||
s.pointer(tev(PointerKind::Down, 230.0, 125.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 500.0, 125.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 500.0, 125.0)).unwrap();
|
||||
assert_eq!(table_of(&s, id).cols, vec![200.0 - cmd::table::MIN_COLUMN_WIDTH, cmd::table::MIN_COLUMN_WIDTH, 100.0]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(table_of(&s, id).cols, vec![100.0, 100.0, 100.0]);
|
||||
// The right edge: the last column and the table get wider.
|
||||
s.pointer(tev(PointerKind::Down, 400.0, 120.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 450.0, 120.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 450.0, 120.0)).unwrap();
|
||||
assert_eq!(table_of(&s, id).cols, vec![100.0, 100.0, 150.0]);
|
||||
assert_eq!(s.doc().unwrap().shape(id).unwrap().xfrm.unwrap().w, 350.0);
|
||||
// A click on a border without dragging changes nothing.
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
s.pointer(tev(PointerKind::Down, 200.0, 125.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 200.0, 125.0)).unwrap();
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_row_border_sets_the_row_height() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[200.0, 200.0], json!([["a", "b"], ["c", "d"]]));
|
||||
let drawn = |s: &Session| {
|
||||
let st = s.doc().unwrap();
|
||||
tools::table_rows(st, &table_of(s, id))
|
||||
};
|
||||
s.pointer(tev(PointerKind::Down, 150.0, 140.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 150.0, 170.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 150.0, 170.0)).unwrap();
|
||||
assert_eq!(drawn(&s), vec![70.0, 40.0]);
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(cmd::xfrm_of(&st.doc, &st.selection, st.shape(id).unwrap()).h, 110.0, "the table grows");
|
||||
// Rows still fit their text: dragging the border up stops at the text.
|
||||
s.pointer(tev(PointerKind::Down, 150.0, 170.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 150.0, 60.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 150.0, 60.0)).unwrap();
|
||||
let h = drawn(&s)[0];
|
||||
assert!(h > 10.0 && h < 40.0, "row 0 shrinks to its text: {h}");
|
||||
// The bottom edge sizes the last row.
|
||||
let bottom = 100.0 + drawn(&s).iter().sum::<f64>();
|
||||
s.pointer(tev(PointerKind::Down, 150.0, bottom)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 150.0, bottom + 20.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 150.0, bottom + 20.0)).unwrap();
|
||||
assert_eq!(drawn(&s)[1], 60.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_border_while_editing_a_cell_keeps_editing() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[100.0, 100.0], json!([["a", "b"]]));
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [0, 1]})).unwrap();
|
||||
s.pointer(tev(PointerKind::Down, 200.0, 120.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 180.0, 120.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 180.0, 120.0)).unwrap();
|
||||
assert_eq!(table_of(&s, id).cols, vec![80.0, 120.0]);
|
||||
assert_eq!(text_sel(&s).and_then(|t| t.cell), Some((0, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_width_can_keep_the_table_width() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[100.0, 100.0, 100.0], json!([["a", "b", "c"]]));
|
||||
s.execute("table.columnWidth", &json!({"id": id, "col": 1, "width": 150.0, "keepTotal": true})).unwrap();
|
||||
assert_eq!(table_of(&s, id).cols, vec![100.0, 150.0, 50.0]);
|
||||
s.execute("table.columnWidth", &json!({"id": id, "col": 1, "width": 1000.0, "keepTotal": true})).unwrap();
|
||||
assert_eq!(table_of(&s, id).cols, vec![100.0, 200.0 - cmd::table::MIN_COLUMN_WIDTH, cmd::table::MIN_COLUMN_WIDTH]);
|
||||
// The last column has no neighbour: the table changes width.
|
||||
s.execute("table.columnWidth", &json!({"id": id, "col": 2, "width": 80.0, "keepTotal": true})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().shape(id).unwrap().xfrm.unwrap().w, 100.0 + (200.0 - cmd::table::MIN_COLUMN_WIDTH) + 80.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotated_tables_hit_the_cell_under_the_pointer() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[100.0, 100.0], json!([["a", "b"], ["c", "d"]]));
|
||||
s.execute("shape.rotate", &json!({"ids": [id], "deg": 90})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let f = cmd::table::table_frame(st, st.shape(id).unwrap()).unwrap();
|
||||
let p = f.affine * deckcraft_geom::Point::new(150.0, 60.0);
|
||||
s.pointer(tev(PointerKind::DoubleClick, p.x, p.y)).unwrap();
|
||||
assert_eq!(text_sel(&s).and_then(|t| t.cell), Some((1, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_moves_between_cells_skipping_merged_ones() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[100.0, 100.0, 100.0], json!([["a", "b", "c"], ["d", "e", "f"]]));
|
||||
s.execute("table.merge", &json!({"id": id, "from": [0, 0], "to": [0, 1]})).unwrap();
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [0, 0]})).unwrap();
|
||||
let cell = |s: &Session| text_sel(s).and_then(|t| t.cell);
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
s.key("Tab", Mods::default()).unwrap();
|
||||
assert_eq!(cell(&s), Some((0, 2)), "the cell under the merge is skipped");
|
||||
s.key("Tab", shift).unwrap();
|
||||
assert_eq!(cell(&s), Some((0, 0)));
|
||||
s.key("Tab", shift).unwrap();
|
||||
assert_eq!(cell(&s), Some((0, 0)), "Shift+Tab in the first cell stays");
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [1, 2]})).unwrap();
|
||||
s.key("Tab", Mods::default()).unwrap();
|
||||
assert_eq!((cell(&s), table_of(&s, id).n_rows()), (Some((2, 0)), 3), "Tab in the last cell adds a row");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_to_left_paragraphs() {
|
||||
let mut s = session();
|
||||
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
|
||||
s.execute("text.edit", &json!({"id": title})).unwrap();
|
||||
// Explicitly left-to-right first, so typing Arabic doesn't already turn it right-to-left (#169).
|
||||
s.execute("format.rtl", &json!({"on": false})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "سنة 2024 (Retrait) فقط"})).unwrap();
|
||||
s.execute("format.rtl", &json!({})).unwrap();
|
||||
let para = |s: &Session| s.doc().unwrap().current_slide().unwrap().shapes[0].text.as_ref().unwrap().paragraphs[0].props.clone();
|
||||
@@ -592,6 +889,61 @@ fn right_to_left_paragraphs() {
|
||||
assert_eq!(para(&s).rtl, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_kurdish_or_arabic_auto_detects_rtl_and_advances_caret_smoothly() {
|
||||
let mut s = session();
|
||||
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
|
||||
s.execute("text.edit", &json!({"id": title})).unwrap();
|
||||
|
||||
let mut first_x = None;
|
||||
let mut last_x = 0.0;
|
||||
|
||||
// Type Kurdish sentence character-by-character: "من ناوم هێڤاره"
|
||||
for ch in ["م", "ن", " ", "ن", "ا", "و", "م", " ", "ه", "ێ", "ڤ", "ا", "ر", "ه"] {
|
||||
s.execute("text.insert", &json!({"text": ch})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let t = st.selection.text.as_ref().unwrap();
|
||||
let l = super::cmd::text::layout_for(st, t).unwrap();
|
||||
let (cx, ..) = l.caret(deckcraft_text::Pos { para: t.caret.0, ch: t.caret.1 }).unwrap();
|
||||
|
||||
if first_x.is_none() {
|
||||
first_x = Some(cx);
|
||||
}
|
||||
last_x = cx;
|
||||
|
||||
// The title is centred: the caret must NOT jump back to the left margin (~7.2 pt).
|
||||
assert!(cx > 100.0, "caret should not jump to left margin: {cx}");
|
||||
}
|
||||
|
||||
// Caret advances leftwards overall as text is appended
|
||||
assert!(last_x < first_x.unwrap(), "caret should advance leftwards: last={last_x} first={:?}", first_x);
|
||||
let para = s.doc().unwrap().current_slide().unwrap().shapes[0].text.as_ref().unwrap().paragraphs[0].props.clone();
|
||||
assert_eq!(para.rtl, Some(true), "auto-detected as RTL");
|
||||
assert_eq!(para.align, None, "the title keeps the alignment it inherits");
|
||||
|
||||
// A text box inherits the deck's default (left) alignment, which it keeps too.
|
||||
s.execute("text.exit", &json!({})).ok();
|
||||
let id = s.execute("insert.textBox", &json!({"rect": [10, 10, 300, 100], "text": ""})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.edit", &json!({"id": id})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "مرحبا"})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let sh = st.shape(deckcraft_model::ShapeId(id as u32)).unwrap();
|
||||
let para = sh.text.as_ref().unwrap().paragraphs[0].props.clone();
|
||||
assert_eq!(para.rtl, Some(true));
|
||||
assert_eq!(para.align, None, "the text box keeps the alignment it inherits");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_ltr_override_is_preserved_when_typing_rtl() {
|
||||
let mut s = session();
|
||||
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
|
||||
s.execute("text.edit", &json!({"id": title})).unwrap();
|
||||
s.execute("format.rtl", &json!({"on": false})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "مرحبا"})).unwrap();
|
||||
let para = s.doc().unwrap().current_slide().unwrap().shapes[0].text.as_ref().unwrap().paragraphs[0].props.clone();
|
||||
assert_eq!(para.rtl, Some(false), "user explicit LTR is preserved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_footer_fixed_date_is_the_slides_own_text() {
|
||||
use deckcraft_model::text::RunKind;
|
||||
@@ -2362,6 +2714,306 @@ fn png_size(bytes: &[u8]) -> Option<(u32, u32)> {
|
||||
Some((u32::from_be_bytes(w), u32::from_be_bytes(h)))
|
||||
}
|
||||
|
||||
/// A blank slide holding one text box being edited, with smart guides and grid snapping off so
|
||||
/// drags move by exactly the pointer delta. Returns the session, the box id and its box.
|
||||
fn editing_text_box(rot: f64) -> (Session, u64, deckcraft_geom::Xfrm) {
|
||||
let mut s = session();
|
||||
s.prefs.snap_to_grid = false;
|
||||
s.prefs.smart_guides = false;
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let id = s.execute("insert.textBox", &json!({"rect": [200.0, 150.0, 240.0, 60.0], "text": "Hello world"})).unwrap()["id"].as_u64().unwrap();
|
||||
if rot != 0.0 {
|
||||
s.execute("shape.rotate", &json!({"ids": [id], "deg": rot})).unwrap();
|
||||
}
|
||||
s.execute("text.edit", &json!({"id": id, "at": [0, 0]})).unwrap();
|
||||
let x = text_box_xfrm(&s, id);
|
||||
(s, id, x)
|
||||
}
|
||||
|
||||
/// Slide point for a point in the box's own (rotated) frame.
|
||||
fn on_box(x: &deckcraft_geom::Xfrm, lx: f64, ly: f64) -> (f64, f64) {
|
||||
let p = x.affine() * deckcraft_geom::Point::new(lx, ly);
|
||||
(p.x, p.y)
|
||||
}
|
||||
|
||||
fn drag_gesture(s: &mut Session, from: (f64, f64), to: (f64, f64)) -> serde_json::Value {
|
||||
let ev = |kind, (x, y): (f64, f64)| PointerEvent { kind, x, y, mods: Mods::default(), tol: 3.0, pressure: None };
|
||||
let down = s.pointer(ev(PointerKind::Down, from)).unwrap();
|
||||
let mid = ((from.0 + to.0) / 2.0, (from.1 + to.1) / 2.0);
|
||||
s.pointer(ev(PointerKind::Drag, mid)).unwrap();
|
||||
s.pointer(ev(PointerKind::Drag, to)).unwrap();
|
||||
s.pointer(ev(PointerKind::Up, to)).unwrap();
|
||||
down
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_the_frame_of_edited_text_moves_the_box() {
|
||||
let (mut s, id, x) = editing_text_box(0.0);
|
||||
assert!(x.h > 12.0, "{x:?}");
|
||||
let sid = deckcraft_model::ShapeId(id as u32);
|
||||
// Hovering the band on either side of the edge says "move"; the inside says "text".
|
||||
for ly in [-2.0, 2.0, x.h + 2.0] {
|
||||
let (px, py) = on_box(&x, x.w / 4.0, ly);
|
||||
s.pointer(PointerEvent { kind: PointerKind::Move, x: px, y: py, mods: Mods::default(), tol: 3.0, pressure: None }).unwrap();
|
||||
assert_eq!(s.tool.hover, Some(tools::Hit::Frame { id: sid }), "ly {ly}");
|
||||
}
|
||||
let (px, py) = on_box(&x, x.w / 4.0, x.h / 2.0);
|
||||
s.pointer(PointerEvent { kind: PointerKind::Move, x: px, y: py, mods: Mods::default(), tol: 3.0, pressure: None }).unwrap();
|
||||
assert_eq!(s.tool.hover, Some(tools::Hit::Shape { id: sid, text: true }));
|
||||
|
||||
// Press on the top edge, just outside, and drag: the box moves, text editing ends.
|
||||
let undo_before = s.doc().unwrap().history.undo.len();
|
||||
let from = on_box(&x, x.w / 4.0, -1.0);
|
||||
let down = drag_gesture(&mut s, from, (from.0 + 30.0, from.1 + 20.0));
|
||||
assert_eq!(down["hit"], json!({"kind": "frame", "id": id}));
|
||||
let moved = text_box_xfrm(&s, id);
|
||||
assert!((moved.x - (x.x + 30.0)).abs() < 1e-9 && (moved.y - (x.y + 20.0)).abs() < 1e-9, "{moved:?}");
|
||||
assert_eq!((moved.w, moved.h), (x.w, x.h));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.selection.text.is_none());
|
||||
assert_eq!(st.selection.shapes, vec![sid]);
|
||||
// One undo step, which puts the box back.
|
||||
assert_eq!(st.history.undo.len(), undo_before + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(text_box_xfrm(&s, id), x);
|
||||
|
||||
// The inner side of the band works too; a click there without a drag selects the box whole
|
||||
// and changes nothing.
|
||||
s.execute("text.edit", &json!({"id": id, "at": [0, 0]})).unwrap();
|
||||
let undo_before = s.doc().unwrap().history.undo.len();
|
||||
let (px, py) = on_box(&x, x.w * 0.75, x.h - 1.0);
|
||||
let ev = |kind| PointerEvent { kind, x: px, y: py, mods: Mods::default(), tol: 3.0, pressure: None };
|
||||
s.pointer(ev(PointerKind::Down)).unwrap();
|
||||
s.pointer(ev(PointerKind::Up)).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.selection.text.is_none());
|
||||
assert_eq!(st.selection.shapes, vec![sid]);
|
||||
assert_eq!(st.history.undo.len(), undo_before);
|
||||
assert_eq!(text_box_xfrm(&s, id), x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_inside_edited_text_selects_text() {
|
||||
let (mut s, id, x) = editing_text_box(0.0);
|
||||
let undo_before = s.doc().unwrap().history.undo.len();
|
||||
let down = drag_gesture(&mut s, on_box(&x, 8.0, x.h / 2.0), on_box(&x, x.w - 8.0, x.h / 2.0));
|
||||
assert!(down.get("caret").is_some(), "{down}");
|
||||
assert_eq!(text_box_xfrm(&s, id), x);
|
||||
let st = s.doc().unwrap();
|
||||
let t = st.selection.text.as_ref().expect("still editing");
|
||||
assert!(t.is_range(), "{t:?}");
|
||||
assert_eq!(t.ordered(), ((0, 0), (0, 11)));
|
||||
assert_eq!(st.history.undo.len(), undo_before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_of_rotated_edited_text_follows_the_rotation() {
|
||||
let (mut s, id, x) = editing_text_box(30.0);
|
||||
assert_eq!(x.rot, 30.0);
|
||||
let sid = deckcraft_model::ShapeId(id as u32);
|
||||
// The rotated bottom edge is the frame; dragging it moves the box and keeps its rotation.
|
||||
let from = on_box(&x, x.w / 4.0, x.h + 1.0);
|
||||
assert_eq!(s.hit_test(deckcraft_geom::Point::new(from.0, from.1), 3.0), Some(tools::Hit::Frame { id: sid }));
|
||||
let down = drag_gesture(&mut s, from, (from.0 + 25.0, from.1 - 10.0));
|
||||
assert_eq!(down["hit"]["kind"], "frame");
|
||||
let moved = text_box_xfrm(&s, id);
|
||||
assert!((moved.x - (x.x + 25.0)).abs() < 1e-9 && (moved.y - (x.y - 10.0)).abs() < 1e-9, "{moved:?}");
|
||||
assert_eq!(moved.rot, 30.0);
|
||||
// Inside the rotated box, pressing still places the caret.
|
||||
s.execute("text.edit", &json!({"id": id, "at": [0, 0]})).unwrap();
|
||||
let mx = text_box_xfrm(&s, id);
|
||||
let (px, py) = on_box(&mx, mx.w / 2.0, mx.h / 2.0);
|
||||
let r = s.pointer(PointerEvent { kind: PointerKind::Down, x: px, y: py, mods: Mods::default(), tol: 3.0, pressure: None }).unwrap();
|
||||
s.pointer(PointerEvent { kind: PointerKind::Up, x: px, y: py, mods: Mods::default(), tol: 3.0, pressure: None }).unwrap();
|
||||
assert!(r.get("caret").is_some(), "{r}");
|
||||
assert_eq!(text_box_xfrm(&s, id), mx);
|
||||
// Inside the axis-aligned bounds but off the rotated box: neither text nor frame, so a press
|
||||
// there leaves editing instead of moving the caret.
|
||||
let off = on_box(&mx, mx.w / 2.0, -mx.h);
|
||||
let off_pt = deckcraft_geom::Point::new(off.0, off.1);
|
||||
assert!(mx.bounds().contains(off_pt));
|
||||
assert_ne!(s.hit_test(off_pt, 3.0), Some(tools::Hit::Frame { id: sid }));
|
||||
let r = s.pointer(PointerEvent { kind: PointerKind::Down, x: off.0, y: off.1, mods: Mods::default(), tol: 3.0, pressure: None }).unwrap();
|
||||
s.pointer(PointerEvent { kind: PointerKind::Up, x: off.0, y: off.1, mods: Mods::default(), tol: 3.0, pressure: None }).unwrap();
|
||||
assert!(r.get("caret").is_none(), "{r}");
|
||||
assert!(s.doc().unwrap().selection.text.is_none());
|
||||
assert_eq!(text_box_xfrm(&s, id), mx);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_handles_beat_the_frame_of_edited_text() {
|
||||
let (mut s, id, x) = editing_text_box(0.0);
|
||||
// The SE corner lies in the frame band, but its handle wins.
|
||||
let from = on_box(&x, x.w, x.h);
|
||||
let down = drag_gesture(&mut s, from, (from.0 + 40.0, from.1));
|
||||
assert_eq!(down["hit"]["kind"], "resize");
|
||||
let sized = text_box_xfrm(&s, id);
|
||||
assert!((sized.w - (x.w + 40.0)).abs() < 1e-9, "{sized:?}");
|
||||
assert_eq!((sized.x, sized.y), (x.x, x.y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_language_marks_the_selected_text() {
|
||||
let mut s = session();
|
||||
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
|
||||
s.execute("text.edit", &json!({"id": title})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "Hola mundo"})).unwrap();
|
||||
s.execute("text.move", &json!({"to": "wordLeft", "extend": true})).unwrap();
|
||||
s.execute("format.language", &json!({"lang": "es-ES"})).unwrap();
|
||||
assert_eq!(s.execute("format.state", &json!({})).unwrap()["lang"], "es-ES");
|
||||
let body = s.doc().unwrap().current_slide().unwrap().shapes[0].text.clone().unwrap();
|
||||
let langs: Vec<_> = body.paragraphs[0].runs.iter().map(|r| (r.text.as_str(), r.props.lang.as_deref())).collect();
|
||||
assert_eq!(langs, vec![("Hola ", None), ("mundo", Some("es-ES"))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_params_doc_names_its_keys() {
|
||||
for c in command_specs() {
|
||||
assert!(cmd::param_keys(c.params).is_ok(), "{}: {:?}", c.id, cmd::param_keys(c.params));
|
||||
}
|
||||
assert_eq!(cmd::param_keys("{index} | {id}").unwrap(), ["index", "id"]);
|
||||
assert_eq!(cmd::param_keys("{color? | none?: true, kind?: a|b (x, y), ids?} → {id}").unwrap(), ["color", "none", "kind", "ids"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_params_doc_spells_out_each_parameter() {
|
||||
use cmd::Param;
|
||||
let p = |name, optional, about, or, object| Param { name, optional, about, or, object };
|
||||
assert_eq!(
|
||||
cmd::parse_params("{preset: rect|roundRect, rect?: [x,y,w,h] pt, index? (else selected shapes)} → {id}").unwrap(),
|
||||
[
|
||||
p("preset", false, "rect|roundRect", false, 0),
|
||||
p("rect", true, "[x,y,w,h] pt", false, 0),
|
||||
p("index", true, "(else selected shapes)", false, 0)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
cmd::parse_params("{color? | none?: true, gradient?: {stops, kind?: a|b}, ids?}").unwrap(),
|
||||
[
|
||||
p("color", true, "", false, 0),
|
||||
p("none", true, "true", true, 0),
|
||||
p("gradient", true, "{stops, kind?: a|b}", false, 0),
|
||||
p("ids", true, "", false, 0)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
cmd::parse_params("{name} | {major, minor}").unwrap(),
|
||||
[p("name", false, "", false, 0), p("major", false, "", false, 1), p("minor", false, "", false, 1)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_a_command_acts_on_names_only_keys_it_takes() {
|
||||
for c in command_specs() {
|
||||
let keys = cmd::param_keys(c.params).unwrap();
|
||||
let said = c.acts_on().unwrap_or_default();
|
||||
assert!(!said.contains("`ids`") || keys.contains(&"ids"), "{}: {said}", c.id);
|
||||
assert!(!said.contains("`id`") || keys.contains(&"id"), "{}: {said}", c.id);
|
||||
}
|
||||
let acts = |id: &str| find_command(id).and_then(|c| c.acts_on());
|
||||
assert_eq!(acts("shape.resize").as_deref(), Some("Acts on the shapes in `ids`, else on the selected shapes."));
|
||||
assert_eq!(acts("table.style").as_deref(), Some("Acts on the shape in `id`, else on the selected shapes."));
|
||||
assert_eq!(acts("shape.setDefault").as_deref(), Some("Acts on the selected shapes."));
|
||||
assert_eq!(acts("format.size").as_deref(), Some("Acts on the selected text or shapes."));
|
||||
assert_eq!(acts("slide.new"), None);
|
||||
}
|
||||
|
||||
/// Tables whose style the file defines (#292, #294: its look and background drawn) edit like the
|
||||
/// others: the caret is laid out with the file style's lettering, clicks and double-clicks land
|
||||
/// in the cell where it is drawn, and borders still resize.
|
||||
#[test]
|
||||
fn tables_with_a_file_defined_style_edit_and_resize() {
|
||||
use deckcraft_model::style::{ColorRef, Fill};
|
||||
use deckcraft_model::table::{TablePart, TablePartStyle, TableStyleDef};
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[200.0, 200.0], json!([["Hello world", "b"], ["c", "d"]]));
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [0, 0]})).unwrap();
|
||||
let built_in = caret_point(&s, 0, 8);
|
||||
s.execute("text.exit", &json!({})).unwrap();
|
||||
// The table's style, defined by the file: a plain (not bold) header, a filled body, a
|
||||
// background.
|
||||
{
|
||||
let style = table_of(&s, id).style;
|
||||
let d = s.doc_mut().unwrap();
|
||||
let doc = std::sync::Arc::make_mut(&mut d.doc);
|
||||
let solid = |r, g, b| Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(r, g, b))));
|
||||
doc.table_styles.push(TableStyleDef {
|
||||
id: style,
|
||||
name: "File style".into(),
|
||||
parts: vec![
|
||||
(TablePart::WholeTbl, TablePartStyle { fill: solid(230, 240, 250), ..Default::default() }),
|
||||
(TablePart::FirstRow, TablePartStyle { fill: solid(40, 60, 90), bold: Some(false), ..Default::default() }),
|
||||
],
|
||||
background: Some(TablePartStyle { fill: solid(200, 200, 200), ..Default::default() }),
|
||||
raw: String::new(),
|
||||
});
|
||||
}
|
||||
s.execute("edit.deselect", &json!({})).unwrap();
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [0, 0]})).unwrap();
|
||||
let p = caret_point(&s, 0, 8);
|
||||
assert!((p.x - built_in.x).abs() > 0.5, "the header is laid out in the file style's (regular) weight: {p:?} vs {built_in:?}");
|
||||
s.execute("text.exit", &json!({})).unwrap();
|
||||
s.execute("edit.deselect", &json!({})).unwrap();
|
||||
// A double-click selects the word drawn there.
|
||||
s.pointer(tev(PointerKind::DoubleClick, p.x, p.y)).unwrap();
|
||||
let t = text_sel(&s).unwrap();
|
||||
assert_eq!((t.cell, t.ordered()), (Some((0, 0)), ((0, 6), (0, 11))));
|
||||
// A click in another cell moves the caret there.
|
||||
s.pointer(tev(PointerKind::Down, 400.0, 160.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 400.0, 160.0)).unwrap();
|
||||
assert_eq!(text_sel(&s).unwrap().cell, Some((1, 1)));
|
||||
// Border drag, while editing: the columns resize, editing goes on.
|
||||
s.pointer(tev(PointerKind::Down, 300.0, 120.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 320.0, 120.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Drag, 330.0, 121.0)).unwrap();
|
||||
s.pointer(tev(PointerKind::Up, 330.0, 121.0)).unwrap();
|
||||
assert_eq!(table_of(&s, id).cols, vec![230.0, 170.0]);
|
||||
assert_eq!(text_sel(&s).unwrap().cell, Some((1, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_notes_keeps_their_formatting_through_pptx() {
|
||||
use deckcraft_model::text::{Bullet, ParaProps, Paragraph, Run, RunProps, TextBody};
|
||||
// A deck whose notes have a bold heading over indented bullet points, as saved by PowerPoint.
|
||||
let mut p = deckcraft_model::defaults::new_presentation(None);
|
||||
let bullet = ParaProps { margin_left: Some(22.5), indent: Some(-22.5), bullet: Some(Bullet::Char { char: "•".into() }), ..Default::default() };
|
||||
let point = |t: &str| Paragraph { level: 1, props: bullet.clone(), runs: vec![Run::new(t)], ..Default::default() };
|
||||
std::sync::Arc::make_mut(&mut p.slides[0]).notes = TextBody {
|
||||
paragraphs: vec![
|
||||
Paragraph { runs: vec![Run::with("Overview", RunProps { bold: Some(true), ..Default::default() })], ..Default::default() },
|
||||
point("First point"),
|
||||
point("Second point"),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let bytes = deckcraft_pptx::export(&p).unwrap();
|
||||
let mut s = session();
|
||||
s.execute("file.openBytes", &json!({"name": "notes.pptx", "data": crate::cmd::base64_encode(&bytes)})).unwrap();
|
||||
// Type a word into the second bullet, as the notes pane does.
|
||||
s.execute("slide.notes", &json!({"index": 0, "text": "Overview\nFirst point\nSecond main point"})).unwrap();
|
||||
let check = |notes: &TextBody| {
|
||||
assert_eq!(notes.text(), "Overview\nFirst point\nSecond main point");
|
||||
assert_eq!(notes.paragraphs[0].runs[0].props.bold, Some(true));
|
||||
for para in ¬es.paragraphs[1..] {
|
||||
assert_eq!(para.level, 1);
|
||||
assert_eq!(para.props.margin_left, Some(22.5));
|
||||
assert_eq!(para.props.indent, Some(-22.5));
|
||||
assert_eq!(para.props.bullet, Some(Bullet::Char { char: "•".into() }));
|
||||
}
|
||||
};
|
||||
check(&s.doc().unwrap().doc.slides[0].notes);
|
||||
let saved = s.execute("file.saveBytes", &json!({"format": "pptx"})).unwrap()["data"].as_str().unwrap().to_string();
|
||||
let reopened = deckcraft_pptx::import(&crate::cmd::base64_decode(&saved).unwrap()).unwrap();
|
||||
check(&reopened.slides[0].notes);
|
||||
// slide.inspect hands out the formatted notes, and slide.notes {body} takes them back as they are.
|
||||
let body = s.execute("slide.inspect", &json!({"index": 0})).unwrap()["notesBody"].clone();
|
||||
s.execute("slide.notes", &json!({"index": 0, "text": "Plain"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.slides[0].notes, TextBody::from_text("Plain"), "new text shares nothing: plain");
|
||||
s.execute("slide.notes", &json!({"index": 0, "body": body})).unwrap();
|
||||
check(&s.doc().unwrap().doc.slides[0].notes);
|
||||
assert!(s.execute("slide.notes", &json!({"index": 0, "body": 7})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autocorrect_options_as_you_type() {
|
||||
let mut s = session();
|
||||
@@ -2522,6 +3174,37 @@ fn language_marks_text_and_no_proof_and_repeats_are_reported() {
|
||||
assert!(s.execute("format.language", &json!({"lang": "en US!"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_caret_takes_the_colour_of_the_text() {
|
||||
let caret = |theme: &str| {
|
||||
let mut s = session();
|
||||
s.execute("design.theme", &json!({"name": theme})).unwrap();
|
||||
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
|
||||
s.execute("text.edit", &json!({"id": title})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let empty = cmd::text::caret_color(st, &st.selection.text.clone().unwrap()).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "Typed"})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(cmd::text::caret_color(st, &st.selection.text.clone().unwrap()), Some(empty));
|
||||
empty
|
||||
};
|
||||
// Dark text on the light theme, light text on the dark one: never black on a dark slide.
|
||||
let (light, dark) = (caret("DeckCraft"), caret("Nocturne"));
|
||||
assert!(light.r < 0x60 && light.g < 0x60 && light.b < 0x60, "{light:?}");
|
||||
assert!(dark.r > 0xC0 && dark.g > 0xC0 && dark.b > 0xC0, "{dark:?}");
|
||||
// Text set to red gets a red caret.
|
||||
let mut s = session();
|
||||
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
|
||||
s.execute("text.edit", &json!({"id": title})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "Red"})).unwrap();
|
||||
s.execute("text.selectAll", &json!({})).unwrap();
|
||||
s.execute("format.color", &json!({"color": "#FF0000"})).unwrap();
|
||||
s.execute("text.move", &json!({"to": "end"})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let red = cmd::text::caret_color(st, &st.selection.text.clone().unwrap()).unwrap();
|
||||
assert_eq!((red.r, red.g, red.b), (0xFF, 0, 0));
|
||||
}
|
||||
|
||||
/// Smooth Corners (#286): a rounded rectangle becomes a squircle outline that survives PPTX.
|
||||
#[test]
|
||||
fn smooth_corners_makes_a_squircle_outline() {
|
||||
|
||||
Reference in New Issue
Block a user