From e50f0e32d8284b47ee4862b73b1c3e3607483a97 Mon Sep 17 00:00:00 2001 From: DWAA1660 Date: Fri, 13 Nov 2026 16:52:24 -0700 Subject: [PATCH] feat(ui-egui): Alt+1..9 pick up a pen, Alt+0 goes back to typing (#44) Tablet express keys and keyboards can now change pens without reaching for the ribbon. Alt+1 to Alt+9 pick up the pen in that place on the Draw tab's rack, from anywhere in the window; Alt+0 puts the pen down and goes back to typing. A number past the end of the rack does nothing. On a Mac, the character Option+digit types is dropped so it does not land in the note. The rack's tooltips name each pen's key. Part of #44 (the shortcut request; the stroke smoothing in the same issue is not touched here). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y4im4nvZvQHZdShop1ff8w --- crates/ui-egui/src/app.rs | 18 ++++++++++++++ crates/ui-egui/src/ribbon.rs | 2 ++ crates/ui-egui/tests/ui.rs | 47 ++++++++++++++++++++++++++++++++++++ docs/gaps.md | 4 ++- i18n/ar-XB.tsv | 2 ++ i18n/en-XA.tsv | 2 ++ i18n/en.tsv | 2 ++ 7 files changed, 76 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 055446f..4d72fe5 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -1433,6 +1433,23 @@ impl NoteCraftApp { } } + /// Keys for the pens, so a tablet's express keys or a keyboard can change tools without + /// reaching for the ribbon (issue #44): Alt+1 to Alt+9 pick up that pen on the Draw tab's + /// rack, and Alt+0 puts the pens down and goes back to typing. On a Mac, where Option and a + /// digit also type a character, the character is dropped so it does not land in the note. + fn pen_keys(&mut self, ctx: &Context) { + const DIGITS: [Key; 10] = [Key::Num0, Key::Num1, Key::Num2, Key::Num3, Key::Num4, Key::Num5, Key::Num6, Key::Num7, Key::Num8, Key::Num9]; + let Some(n) = ctx.input_mut(|i| DIGITS.iter().position(|key| i.consume_key(Modifiers::ALT, *key))) else { return }; + ctx.input_mut(|i| i.events.retain(|e| !matches!(e, egui::Event::Text(_)))); + if n == 0 { + self.ribbon_act(ribbon::Act::InkPointer(ribbon::Pointer::Write)); + self.say(crate::tl_id!("ui.pen_keys_typing")); + } else if n <= self.rack.len() { + self.ribbon_act(ribbon::Act::InkPen(n - 1)); + self.say(crate::i18n::format_message_id("ui.pen_keys_picked", &[("number", &n.to_string())])); + } + } + /// The keys and buttons that move through the pages that were opened. Undo and Redo keys /// are the page history's own, taken below. fn nav_keys(&mut self, ctx: &Context) { @@ -8483,6 +8500,7 @@ impl NoteCraftApp { self.focus_search = true; } self.nav_keys(ctx); + self.pen_keys(ctx); self.direction_keys(ctx); self.hierarchy_keys(ctx); self.restore_last_page(); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 5054c05..2f2bc01 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -2123,6 +2123,8 @@ fn pen_on_rack(ui: &mut Ui, i: usize, nib: Nib, chosen: bool, rack_len: usize, t open = false; } ui.data_mut(|d| d.insert_temp(open_id, open)); + // The first nine pens answer Alt and their number (issue #44). + let name = if i < 9 { format!("{name} (Alt+{})", i + 1) } else { name }; resp.on_hover_text(name); picked_up } diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index fe41b0d..b7df76a 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -9377,3 +9377,50 @@ fn content_wider_than_the_view_gets_a_horizontal_scroll_bar() { assert_eq!(state.pointer("/ink/strokes").and_then(Value::as_u64), Some(0), "the bar took the press, not the pen: {state}"); } } + +/// 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. +#[test] +fn alt_and_a_number_pick_up_a_pen_and_alt_zero_goes_back_to_typing() { + let (_t, mut app, s) = sample_app("ui-pen-keys"); + app.open_path(&s.md_page.to_string_lossy()); + let mut h = harness(app, LIGHT); + settle(&mut h); + let ink = |h: &mut Harness<'_, NoteCraftApp>| { + let v = ask(h, "ui.inspect", json!({})).expect("inspect"); + (v.pointer("/ink/pointer").and_then(Value::as_str).unwrap_or_default().to_string(), v.pointer("/ink/nib").and_then(Value::as_u64)) + }; + assert_eq!(ink(&mut h).0, "write"); + ask(&mut h, "ui.key", json!({"key": "2", "alt": true})).expect("Alt+2"); + settle(&mut h); + assert_eq!(ink(&mut h), ("draw".to_string(), Some(1)), "the second pen on the rack is in hand"); + ask(&mut h, "ui.key", json!({"key": "5", "alt": true})).expect("Alt+5"); + settle(&mut h); + assert_eq!(ink(&mut h), ("draw".to_string(), Some(4))); + ask(&mut h, "ui.key", json!({"key": "0", "alt": true})).expect("Alt+0"); + settle(&mut h); + assert_eq!(ink(&mut h).0, "write", "Alt+0 puts the pen down"); + // A number past the end of the rack picks up nothing. + ask(&mut h, "ui.key", json!({"key": "9", "alt": true})).expect("Alt+9"); + settle(&mut h); + assert_eq!(ink(&mut h).0, "write", "there is no ninth pen on the starting rack"); + // A plain digit is still typing, not a pen. + ask(&mut h, "ui.key", json!({"key": "3"})).expect("3"); + settle(&mut h); + assert_eq!(ink(&mut h).0, "write"); + + // With the caret in the note, the character a Mac types for Option+1 does not reach it. + ask(&mut h, "ui.caret", json!({"block": 1})).expect("caret"); + pump(&mut h); + let before = h.state().body().unwrap_or_default().to_string(); + let alt = egui::Modifiers { alt: true, ..Default::default() }; + h.input_mut().events.push(egui::Event::Key { key: egui::Key::Num1, physical_key: None, pressed: true, repeat: false, modifiers: alt }); + h.input_mut().events.push(egui::Event::Text("\u{a1}".to_string())); + h.step(); + settle(&mut h); + assert_eq!(ink(&mut h), ("draw".to_string(), Some(0)), "Alt+1 picks up the first pen"); + let after = h.state().body().unwrap_or_default().to_string(); + assert!(!after.contains('\u{a1}'), "the Option character was not typed"); + assert_eq!(before, after, "the note is unchanged"); +} diff --git a/docs/gaps.md b/docs/gaps.md index b47cc36..16f7b14 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -387,7 +387,9 @@ Playground hosts with the Files API continue using their existing persistence pa ellipse, circle, diamond, triangle and star - drawn by dragging out the two corners they sit between. A drawn shape is ink like any other stroke, so the eraser, Select and Undo Stroke all work on it; it is not kept as a shape that can be turned back into a perfect - circle later, and the file does not say which shape it was. + circle later, and the file does not say which shape it was. Alt+1 to Alt+9 pick up the + pen in that place on the rack, from anywhere, so a tablet's express keys can be bound to + pens; Alt+0 puts the pen down and goes back to typing. OneNote has no such keys. **Not built:** automatic shapes; the ruler and other stencils; ink replay; ink to text and ink to math; laser pointer; More Colours and the eyedropper in the tool editor. Pressure is real and measured on Windows (hardest 0.930 over 1223 diff --git a/i18n/ar-XB.tsv b/i18n/ar-XB.tsv index f702892..aa4650c 100644 --- a/i18n/ar-XB.tsv +++ b/i18n/ar-XB.tsv @@ -874,3 +874,5 @@ @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.pen_keys_typing [[RLI]]$$$Typing (Alt+0)@@@[[PDI]] +@id ui.pen_keys_picked [[RLI]]$$$Pen {number} (Alt+{number})@@@[[PDI]] diff --git a/i18n/en-XA.tsv b/i18n/en-XA.tsv index 1c3d7ca..31d7071 100644 --- a/i18n/en-XA.tsv +++ b/i18n/en-XA.tsv @@ -874,3 +874,5 @@ @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.pen_keys_typing $$$Typing (Alt+0)@@@ +@id ui.pen_keys_picked $$$Pen {number} (Alt+{number})@@@ diff --git a/i18n/en.tsv b/i18n/en.tsv index ce5591a..429ffd3 100644 --- a/i18n/en.tsv +++ b/i18n/en.tsv @@ -874,3 +874,5 @@ @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.pen_keys_typing Typing (Alt+0) +@id ui.pen_keys_picked Pen {number} (Alt+{number})