Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions crates/ui-egui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,51 @@ fn shown(p: &Path) -> String {
notecraft_store::tree::display_path(p)
}

/// The Insert Space tool's own layer over the visible page (issue #164): a vertical-resize
/// pointer, a guide at the pointer's height, and while it is dragged, the band of room being
/// added (below the start) or taken away (above it). Returns where the drag began and how far it
/// went, in screen points, on the frame it is let go.
fn insert_space_overlay(ctx: &Context, visible: egui::Rect, t: &Tokens) -> Option<(f32, f32)> {
let id = Id::new("insert-space-tool");
let from_id = id.with("from");
let mut done = None;
egui::Area::new(id).order(egui::Order::Foreground).fixed_pos(visible.min).constrain(false).movable(false).show(ctx, |ui| {
let (rect, resp) = ui.allocate_exact_size(visible.size(), Sense::drag());
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, crate::tl_id!("ui.insert_space_how")));
if resp.hovered() || resp.dragged() {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
}
if resp.drag_started()
&& let Some(p) = resp.interact_pointer_pos()
{
ui.data_mut(|d| d.insert_temp(from_id, p.y));
}
let from = ui.data(|d| d.get_temp::<f32>(from_id));
let now = ui.ctx().pointer_latest_pos().filter(|p| rect.contains(*p) || resp.dragged()).map(|p| p.y.clamp(rect.min.y, rect.max.y));
let painter = ui.painter();
let line = egui::Stroke::new(1.5, t.accent);
match (from, now) {
(Some(a), Some(b)) => {
let band = egui::Rect::from_x_y_ranges(rect.x_range(), egui::Rangef::new(a.min(b), a.max(b)));
painter.rect_filled(band, 0.0, t.accent.gamma_multiply(0.12));
painter.hline(rect.x_range(), a, line);
painter.hline(rect.x_range(), b, egui::Stroke::new(1.0, t.accent.gamma_multiply(0.7)));
}
(None, Some(y)) => {
painter.hline(rect.x_range(), y, line);
}
_ => {}
}
if resp.drag_stopped() {
if let (Some(a), Some(p)) = (from, resp.interact_pointer_pos()) {
done = Some((a, p.y.clamp(rect.min.y, rect.max.y) - a));
}
ui.data_mut(|d| d.remove::<f32>(from_id));
}
});
done
}

fn notebook_for(path: &Path) -> Option<PathBuf> {
let mut current = path;
loop {
Expand Down Expand Up @@ -876,6 +921,8 @@ pub struct NoteCraftApp {
canvas_drag: Option<egui::Pos2>,
/// The page's sideways scroll bar and its thumb, when one was drawn last frame (issue #188).
page_hbar: Option<(egui::Rect, egui::Rect)>,
/// True while Insert Space is in hand: the next drag on the page adds or takes away room.
space_tool: bool,
/// What the pointer does on the page (the Draw tab).
pointer: ribbon::Pointer,
/// The tools on the rack, as the owner has arranged them, and which is in hand. Both
Expand Down Expand Up @@ -1038,6 +1085,7 @@ impl NoteCraftApp {
page_rect: None,
canvas_pan: egui::Vec2::ZERO,
page_hbar: None,
space_tool: false,
canvas_drag: None,
pointer: ribbon::Pointer::Write,
rack,
Expand Down Expand Up @@ -3430,6 +3478,7 @@ impl NoteCraftApp {
rack: &self.rack,
nib: self.nib,
in_table: self.caret_in_table(),
space_tool: self.space_tool,
table_borders: self.caret_table_has_borders(),
table_align: self.caret_cells_align(),
table_shade: self.caret_cells_shade(),
Expand Down Expand Up @@ -3852,6 +3901,17 @@ impl NoteCraftApp {
}
ribbon::Act::PageWidth => self.fit_page_width = true,
ribbon::Act::FormatPainter(sticky) => self.pick_up_formatting(sticky),
ribbon::Act::InsertSpaceTool => {
if self.editor.is_none() {
self.say(crate::tl_id!("ui.open_a_page_first"));
return;
}
self.space_tool = !self.space_tool;
if self.space_tool {
self.pointer = ribbon::Pointer::Write;
self.say(crate::tl_id!("ui.insert_space_how"));
}
}
ribbon::Act::FindTags => {
// OneNote looks in the section first; with no page open there is no section.
let scope = self.tags_pane.as_ref().map_or(if self.editor.is_some() { TagScope::Section } else { TagScope::All }, |p| p.scope);
Expand Down Expand Up @@ -6502,6 +6562,9 @@ impl NoteCraftApp {
let mut hbar = crate::hscroll::Shown::default();
// What the page's right-click menu asked for, run once the page's borrow is over.
let mut page_acts: Vec<ribbon::Act> = Vec::new();
// Insert Space: in hand this frame, and whether it was used or put down.
let space_on = self.space_tool && self.pointer == ribbon::Pointer::Write;
let mut space_over = false;
{
let Some(ed) = self.editor.as_mut() else { return };
let missing_fonts = crate::fonts::ensure_page_fonts(ui.ctx(), &crate::fonts::page_families(&ed.blocks));
Expand Down Expand Up @@ -6685,6 +6748,35 @@ impl NoteCraftApp {
}
page_span = Some(span);
}
// Insert Space (issue #164): a drag down the page pushes everything that
// starts below where it began, boxes and ink together, down by as far as
// it went; a drag up pulls it back up. One drag, then the tool is put down.
if space_on {
let visible = ui.clip_rect().intersect(viewport.with_max_y(viewport.max.y.max(ui.clip_rect().max.y)));
if let Some((from, by)) = insert_space_overlay(ui.ctx(), visible, &tokens) {
let z = zoom_scale.max(0.01);
let (at, by) = ((from - moved.origin.y) / z, by / z);
let below: Vec<usize> = (0..ed.boxes.len()).filter(|i| ed.boxes.get(*i).is_some_and(|b| b.y >= at)).collect();
// Room taken away never pulls anything above the top of the page.
let highest = below.iter().filter_map(|i| ed.boxes.get(*i)).map(|b| b.y).fold(f32::INFINITY, f32::min);
let by = if by < 0.0 && highest.is_finite() { by.max(-highest) } else { by };
for i in &below {
if let Some(b) = ed.boxes.get_mut(*i) {
*b = notecraft_model::container::Container::new(b.x, b.y + by, b.width, b.blocks);
}
}
if !below.is_empty() && by != 0.0 {
moved.boxes = true;
}
if let Some(pad) = pad.as_mut() {
ink_changed |= pad.shift_below(at, by);
}
space_over = true;
}
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
space_over = true;
}
}
// The selection box takes ink as well as boxes, as in OneNote, and what
// it took moves together: boxes dragged take the ink with them, ink
// dragged takes the boxes.
Expand Down Expand Up @@ -6944,6 +7036,9 @@ impl NoteCraftApp {
self.canvas_pan.x = (self.canvas_pan.x + hbar.pan).clamp(-1.0e6, 1.0e6);
ui.ctx().request_repaint();
}
if space_over {
self.space_tool = false;
}
for act in page_acts {
self.ribbon_act(act);
}
Expand Down Expand Up @@ -8402,6 +8497,8 @@ impl NoteCraftApp {
// [left, top, right, bottom] in screen points, or null when everything fits.
"horizontalBar": self.page_hbar.map(|(track, _)| [track.min.x, track.min.y, track.max.x, track.max.y]),
"horizontalThumb": self.page_hbar.map(|(_, thumb)| [thumb.min.x, thumb.min.y, thumb.max.x, thumb.max.y]),
// True while Insert Space is in hand (issue #164).
"insertSpace": self.space_tool,
},
// How the side panel is arranged: one tree, or the organisers and the pages in
// columns of their own, with the first one hidden or not.
Expand Down
17 changes: 17 additions & 0 deletions crates/ui-egui/src/inkpad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,23 @@ impl Pad {
(shift != (0.0, 0.0)).then_some(shift)
}

/// Insert Space (issue #164): every stroke whose top is at or below `y`, in page points,
/// moves down by `dy`, or up when `dy` is negative. True when any moved.
pub fn shift_below(&mut self, y: f32, dy: f32) -> bool {
if dy == 0.0 || !dy.is_finite() {
return false;
}
let below: Vec<usize> =
(0..self.drawing.strokes.len()).filter(|i| pick::bounds(&self.drawing, &[*i]).is_some_and(|(_, top, _, _)| top >= y)).collect();
if below.is_empty() {
return false;
}
pick::move_by(&mut self.drawing, &below, 0.0, dy);
self.dirty = true;
self.touched = true;
true
}

/// The box round all the ink on the page, in page points.
pub fn bounds(&self) -> Option<[f32; 4]> {
let all: Vec<usize> = (0..self.drawing.strokes.len()).collect();
Expand Down
9 changes: 7 additions & 2 deletions crates/ui-egui/src/ribbon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ pub enum Act {
ClearFormatting,
/// Put a tag on the line the caret is in, or take it off again.
Tag(Tag),
/// Take up the Insert Space tool, or put it down (issue #164).
InsertSpaceTool,
/// Open the Tags Summary: the tagged notes, and a way to each of them (issue #166).
FindTags,
/// Something OneNote has that NoteCraft does not: say so plainly.
Expand Down Expand Up @@ -426,6 +428,8 @@ pub struct View<'a> {
/// The tools on the rack, and which of them is in hand.
pub rack: &'a [Nib],
pub nib: usize,
/// True while the Insert Space tool is in hand.
pub space_tool: bool,
/// True while the caret is in a table, which is when the Table tab is offered.
pub in_table: bool,
/// Whether the table the caret is in is drawn with lines round its cells.
Expand Down Expand Up @@ -1597,8 +1601,9 @@ fn mark_button(ui: &mut Ui, mark: Mark, label: &str, tip: &str, on: bool, t: &To
/// work is rather than a short list of what happens to exist.
fn insert_tab(ui: &mut Ui, view: &View<'_>, t: &Tokens, acts: &mut Vec<Act>) {
group(ui, crate::tl_id!("ui.insert"), t, |ui| {
if big(ui, Icon::InsertSpace, crate::tl_id!("ui.insert_space_2"), crate::tl_id!("ui.add_a_blank_paragraph_at_the_caret"), t).clicked() {
acts.push(Act::Insert("space"));
// OneNote's tool: a drag on the page adds room there, or takes it away (issue #164).
if big_on(ui, Icon::InsertSpace, crate::tl_id!("ui.insert_space_2"), crate::tl_id!("ui.insert_space_tip"), view.space_tool, t).clicked() {
acts.push(Act::InsertSpaceTool);
}
});
group(ui, crate::tl_id!("ui.tables"), t, |ui| {
Expand Down
81 changes: 81 additions & 0 deletions crates/ui-egui/tests/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9552,6 +9552,87 @@ fn content_wider_than_the_view_gets_a_horizontal_scroll_bar() {
}
}

/// Issue #164: Insert > Insert Space is OneNote's tool. A drag down the page pushes the boxes
/// and the ink below where it began down by as far as it went; a drag up takes room away; Esc
/// puts the tool down; Undo puts the boxes back.
#[test]
fn insert_space_drags_room_into_the_page_and_out_again() {
let (_t, mut app, s) = sample_app("ui-insert-space");
let page = s.md_page.to_string_lossy().to_string();
app.open_path(&page);
let mut h = sized_harness(app, LIGHT, egui::vec2(1400.0, 1000.0));
settle(&mut h);
let blocks = ask(&mut h, "ui.inspect", json!({})).expect("inspect").pointer("/page/blocks").and_then(Value::as_u64).unwrap_or(0);
assert!(blocks > 3, "the sample page has blocks to split: {blocks}");
ask(
&mut h,
"engine.execute",
json!({"command": "page.containers", "params": {"path": page.clone(), "boxes": format!("40,20,320,3;40,500,320,{}", blocks - 3)}}),
)
.expect("two boxes");
ask(&mut h, "ui.open", json!({"path": page.clone()})).expect("open");
settle(&mut h);
let boxes = |h: &mut Harness<'_, NoteCraftApp>| {
let v = ask(h, "ui.inspect", json!({})).expect("inspect");
let y = |i: usize| v.pointer(&format!("/page/containers/{i}/y")).and_then(Value::as_f64).unwrap_or(-1.0);
(y(0), y(1))
};
let tool =
|h: &mut Harness<'_, NoteCraftApp>| ask(h, "ui.inspect", json!({})).expect("inspect").pointer("/canvas/insertSpace").and_then(Value::as_bool);
assert_eq!(boxes(&mut h), (20.0, 500.0));
let origin = page_origin(&mut h);

// Ink under the line moves with the boxes; the pen is put down again before the tool.
h.get_by_label("Draw").click();
settle(&mut h);
h.get_by_label("Pen red").click();
settle(&mut h);
drag_by(&mut h, origin + egui::vec2(500.0, 420.0), egui::vec2(80.0, 0.0));
let ink_top = |h: &mut Harness<'_, NoteCraftApp>| {
ask(h, "ui.inspect", json!({})).expect("inspect").pointer("/ink/bounds/1").and_then(Value::as_f64).expect("ink bounds")
};
let ink_before = ink_top(&mut h);
h.get_by_label("Insert").click();
settle(&mut h);
assert_eq!(tool(&mut h), Some(false));
h.get_by_label("Insert Space").click();
settle(&mut h);
assert_eq!(tool(&mut h), Some(true), "the tool is in hand");
assert_eq!(ask(&mut h, "ui.inspect", json!({})).expect("inspect").pointer("/ink/pointer").and_then(Value::as_str), Some("write"));

// Down 100 points from between the boxes: the lower box and the ink move, the upper box stays.
drag_by(&mut h, origin + egui::vec2(600.0, 300.0), egui::vec2(0.0, 100.0));
assert_eq!(boxes(&mut h), (20.0, 600.0), "room was added above the lower box");
assert!((ink_top(&mut h) - (ink_before + 100.0)).abs() < 1.0, "the ink below moved with it");
assert_eq!(tool(&mut h), Some(false), "one drag, then the tool is put down");
review(&mut h, "insert_space_added");
let kept = ask(&mut h, "engine.execute", json!({"command": "page.containers", "params": {"path": page.clone()}})).expect("layout");
assert!(kept.as_str().unwrap_or_default().contains("40,600,320"), "the page keeps the new place: {kept}");

// Undo puts the box back where it was.
ask(&mut h, "ui.undo", json!({})).expect("undo");
settle(&mut h);
assert_eq!(boxes(&mut h).1, 500.0, "undo takes the room back out");
ask(&mut h, "ui.redo", json!({})).expect("redo");
settle(&mut h);
assert_eq!(boxes(&mut h).1, 600.0);

// Up 150 points from just above the lower box: the room is taken away again.
h.get_by_label("Insert Space").click();
settle(&mut h);
drag_by(&mut h, origin + egui::vec2(600.0, 590.0), egui::vec2(0.0, -150.0));
assert_eq!(boxes(&mut h), (20.0, 450.0), "room was taken away");

// Esc puts the tool down without changing anything.
h.get_by_label("Insert Space").click();
settle(&mut h);
assert_eq!(tool(&mut h), Some(true));
ask(&mut h, "ui.key", json!({"key": "Escape"})).expect("Escape");
settle(&mut h);
assert_eq!(tool(&mut h), Some(false), "Esc puts the tool down");
assert_eq!(boxes(&mut h), (20.0, 450.0));
}

/// Issue #44: Alt and a number pick up that pen on the rack, from anywhere, so a tablet's express
/// keys can be bound to pens; Alt+0 goes back to typing. A Mac's Option+digit character is not
/// typed into the note.
Expand Down
7 changes: 6 additions & 1 deletion docs/gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,12 @@ Playground hosts with the Files API continue using their existing persistence pa
picture, a link, the three time stamps, an equation, a symbol picker, insert space, and
NoteCraft's own Mermaid diagram. File printout, screen clipping, online video,
transcription, recording, page templates and stickers remain unbuilt. Spreadsheet is
available through the existing GridCraft integration when installed.
available through the existing GridCraft integration when installed. Insert Space is
OneNote's tool: with it in hand, a drag down the page moves every box and every stroke of
ink that starts below where the drag began down by as far as it went, and a drag up moves
them up, never past the top of the page; a guide shows the line and the band of room, Esc
puts the tool down, and one drag is one step of Undo. It moves boxes, as OneNote moves its
containers: it does not open a gap between two lines inside one box.
- **Unread and author history** are not built. Page version
history stores snapshots of the page's main file; changes to attached files and drawings
are not captured by those snapshots. (Tags are: to-do boxes and the other familiar marks,
Expand Down
2 changes: 2 additions & 0 deletions i18n/ar-XB.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -882,5 +882,7 @@
@id ui.no_tagged_notes_here [[RLI]]$$$No tagged notes here.@@@[[PDI]]
@id ui.tags_empty_line [[RLI]]$$$(an empty line)@@@[[PDI]]
@id ui.scroll_the_page_sideways [[RLI]]$$$Scroll the page sideways@@@[[PDI]]
@id ui.insert_space_tip [[RLI]]$$$Drag down the page to add room, or up to take it away@@@[[PDI]]
@id ui.insert_space_how [[RLI]]$$$Drag down where you want more room, or up to take room away. Esc puts the tool down.@@@[[PDI]]
@id ui.pen_keys_typing [[RLI]]$$$Typing (Alt+0)@@@[[PDI]]
@id ui.pen_keys_picked [[RLI]]$$$Pen {number} (Alt+{number})@@@[[PDI]]
2 changes: 2 additions & 0 deletions i18n/en-XA.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -882,5 +882,7 @@
@id ui.no_tagged_notes_here $$$No tagged notes here.@@@
@id ui.tags_empty_line $$$(an empty line)@@@
@id ui.scroll_the_page_sideways $$$Scroll the page sideways@@@
@id ui.insert_space_tip $$$Drag down the page to add room, or up to take it away@@@
@id ui.insert_space_how $$$Drag down where you want more room, or up to take room away. Esc puts the tool down.@@@
@id ui.pen_keys_typing $$$Typing (Alt+0)@@@
@id ui.pen_keys_picked $$$Pen {number} (Alt+{number})@@@
2 changes: 2 additions & 0 deletions i18n/en.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -882,5 +882,7 @@
@id ui.no_tagged_notes_here No tagged notes here.
@id ui.tags_empty_line (an empty line)
@id ui.scroll_the_page_sideways Scroll the page sideways
@id ui.insert_space_tip Drag down the page to add room, or up to take it away
@id ui.insert_space_how Drag down where you want more room, or up to take room away. Esc puts the tool down.
@id ui.pen_keys_typing Typing (Alt+0)
@id ui.pen_keys_picked Pen {number} (Alt+{number})
Loading