authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 20:59:10-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 07:05:30-07:00
log5e10e17cd5f73270850258fba1ea45a4b203de7d
tree024366bcd779a0cdc94645ec55a9c5fbad0ecc5b
parent7a0b2662303af3d08687c1a26ff0cf6555c56723
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: Rename on a section tab's menu edits the tab in place

As a double click on the tab does, rather than opening the sidebar on the section's row. A menu counts as the tab's when it opened over the tab row. Assisted-by: claude-opus-5.5

2 files changed, 37 insertions(+), 6 deletions(-)

crates/snowbound/src/menus.rs+29-1
...@@ -376,12 +376,40 @@ impl State {...@@ -376,12 +376,40 @@ impl State {
376 }376 }
377 (Some(action), _) => {377 (Some(action), _) => {
378 self.menu = None;378 self.menu = None;
379 self.act_on(target, action);379 match target {
380 // From its tab, as a double click there renames it.
381 Target::Section { library, path }
382 if action == Action::Rename && self.on_tab(&library, &path, point) =>
383 {
384 self.rename(crate::rename::Target::Entry {
385 library,
386 path,
387 in_tab: true,
388 });
389 }
390 target => self.act_on(target, action),
391 }
380 }392 }
381 (None, _) => {}393 (None, _) => {}
382 }394 }
383 }395 }
384396
397 /// Whether `point`, where a context menu opened, is on the tab of section `path`.
398 fn on_tab(&self, library: &Arc<Library>, path: &str, point: [f32; 2]) -> bool {
399 let shown = self.session.as_ref().is_some_and(|session| {
400 Arc::ptr_eq(&session.library, library)
401 && session.tabs.iter().any(|tab| tab.path == path)
402 });
403 let [x, y] = point;
404 shown
405 && self
406 .ui
407 .rect(crate::sections())
408 .is_some_and(|[left, top, right, bottom]| {
409 x >= left && x < right && y >= top && y < bottom
410 })
411 }
412
385 /// What can be done to `target`, in its context menu's order, as each shows there.413 /// What can be done to `target`, in its context menu's order, as each shows there.
386 pub(crate) fn actions(&self, target: &Target) -> Vec<(Action, Item<'static>)> {414 pub(crate) fn actions(&self, target: &Target) -> Vec<(Action, Item<'static>)> {
387 // An action, its label, whether it is disabled, and whether a rule starts its group.415 // An action, its label, whether it is disabled, and whether a rule starts its group.
crates/snowbound/tests/replay.rs+8-5
...@@ -564,23 +564,26 @@ fn a_version_copied_into_its_section_is_listed_and_found() {...@@ -564,23 +564,26 @@ fn a_version_copied_into_its_section_is_listed_and_found() {
564 assert!(!search_results(&found).is_empty(), "{found}");564 assert!(!search_results(&found).is_empty(), "{found}");
565}565}
566566
567/// Rename on a section tab's menu opens the shut sidebar on a field that takes what is567/// Rename on a section tab's menu opens the field on the tab, as a double click there does,
568/// typed over the old name at once.568/// which takes what is typed over the old name at once; the shut sidebar stays shut.
569#[test]569#[test]
570fn renaming_a_section_from_its_tab_types_into_the_sidebar_at_once() {570fn renaming_a_section_from_its_tab_types_into_the_tab_at_once() {
571 let scratch = Scratch::new("rename-shut-sidebar");571 let scratch = Scratch::new("rename-tab");
572 let notebook =572 let notebook =
573 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");573 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");
574 let mut steps = vec!["move 110 53", "press right", "release right", "settle"];574 let mut steps = vec!["move 110 53", "press right", "release right", "settle"];
575 steps.extend(["key Down", "key Enter", "settle"]);575 steps.extend(["key Down", "key Enter", "settle"]);
576 steps.extend(["type Renamed", "accessibility typed"]);576 steps.extend(["type Renamed", "accessibility typed"]);
577 let [typed] = replay(&scratch, Some(&notebook), &steps)577 steps.extend(["key Enter", "settle", "accessibility renamed"]);
578 let [typed, renamed] = replay(&scratch, Some(&notebook), &steps)
578 .try_into()579 .try_into()
579 .unwrap();580 .unwrap();
580 assert!(581 assert!(
581 typed.contains(r#"TextInput "Name" = "Renamed" [focused]"#),582 typed.contains(r#"TextInput "Name" = "Renamed" [focused]"#),
582 "{typed}"583 "{typed}"
583 );584 );
585 assert!(notebooks(&typed).is_empty(), "the sidebar opened:\n{typed}");
586 assert!(renamed.contains(r#"Tab "Renamed""#), "{renamed}");
584}587}
585588
586/// Finding on the page leaves the query room beside the match count: a press just past589/// Finding on the page leaves the query room beside the match count: a press just past