Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

refactor(ui): better RibbonTab + widget polishing

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (May 7, 2026, 10:55 PM +0300) commit af10a92f parent 5f462098 change-id wlwkwktt
+326 -239
+72 -52
crates/bone-ui/src/gallery.rs
··· 17 17 use crate::theme::Theme; 18 18 use crate::widget_id::{WidgetId, WidgetKey}; 19 19 use crate::widgets::{ 20 - AlwaysValid, AngleEditor, BoolEditor, Button, ButtonState, ButtonVariant, Checkbox, CheckboxState, 21 - ConfirmationDialog, ContextMenu, Dialog, DialogButton, Dropdown, DropdownItem, DropdownState, 22 - FilePickerDialog, FilePickerEntry, FilePickerMode, FilePickerState, HotkeyCapture, 23 - HotkeyCaptureState, LengthEditor, ListItem, ListView, ListViewState, MemoryClipboard, Menu, 24 - MenuBar, MenuBarEntry, MenuBarState, MenuItem, MenuState, Modal, NumericInput, Panel, 25 - PanelState, PanelTitlebar, PropertyGrid, PropertyOption, PropertyRow, RadioGroup, RadioOption, 26 - Ribbon, RibbonGroup, RibbonIconSize, RibbonState, RibbonTab, SelectionEditor, Slider, 27 - SliderRange, SliderStep, StatusAlign, StatusBar, StatusItem, Tab, Table, TableColumn, TableRow, 28 - TableState, Tabs, TabsOrientation, TextEditor, TextInput, TextInputState, Toast, ToastKind, 29 - ToastState, ToggleButton, Toolbar, ToolbarItem, Tooltip, TooltipPlacement, TooltipState, 30 - TreeNode, TreeView, TreeViewState, WidgetPaint, show_button, show_checkbox, show_confirmation, 31 - show_context_menu, show_dialog, show_dropdown, show_file_picker, show_hotkey_capture, 32 - show_list_view, show_menu, show_menu_bar, show_modal, show_panel, show_parsed_input, 33 - show_property_grid, show_radio_group, show_ribbon, show_slider, show_status_bar, show_table, 34 - show_tabs, show_text_input, show_toast, show_toggle_button, show_toolbar, show_tooltip, 35 - show_tree_view, 20 + AlwaysValid, AngleEditor, BoolEditor, Button, ButtonState, ButtonVariant, Checkbox, 21 + CheckboxState, ConfirmationDialog, ContextMenu, Dialog, DialogButton, Dropdown, DropdownItem, 22 + DropdownState, FilePickerDialog, FilePickerEntry, FilePickerMode, FilePickerState, 23 + HotkeyCapture, HotkeyCaptureState, LengthEditor, ListItem, ListView, ListViewState, 24 + MemoryClipboard, Menu, MenuBar, MenuBarEntry, MenuBarState, MenuItem, MenuState, Modal, 25 + NumericInput, Panel, PanelState, PanelTitlebar, PropertyGrid, PropertyOption, PropertyRow, 26 + RadioGroup, RadioOption, Ribbon, RibbonGroup, RibbonIconSize, RibbonState, RibbonTab, 27 + SelectionEditor, Slider, SliderRange, SliderStep, StatusAlign, StatusBar, StatusItem, Tab, 28 + Table, TableColumn, TableRow, TableState, Tabs, TabsOrientation, TextEditor, TextInput, 29 + TextInputState, Toast, ToastKind, ToastState, ToggleButton, Toolbar, ToolbarItem, Tooltip, 30 + TooltipPlacement, TooltipState, TreeNode, TreeView, TreeViewState, WidgetPaint, show_button, 31 + show_checkbox, show_confirmation, show_context_menu, show_dialog, show_dropdown, 32 + show_file_picker, show_hotkey_capture, show_list_view, show_menu, show_menu_bar, show_modal, 33 + show_panel, show_parsed_input, show_property_grid, show_radio_group, show_ribbon, show_slider, 34 + show_status_bar, show_table, show_tabs, show_text_input, show_toast, show_toggle_button, 35 + show_toolbar, show_tooltip, show_tree_view, 36 36 }; 37 37 38 38 pub const GALLERY_LABEL: StringKey = StringKey::new("gallery.label"); ··· 60 60 } 61 61 } 62 62 63 - const fn root_with( 64 - key: &'static str, 65 - kind: StoryKind, 66 - extras: &'static [StoryPath], 67 - ) -> Self { 63 + const fn root_with(key: &'static str, kind: StoryKind, extras: &'static [StoryPath]) -> Self { 68 64 Self { 69 65 key, 70 66 parent: None, ··· 100 96 Story::root("toggle_off", StoryKind::InputPrimitive), 101 97 Story::root_with("radio_a", StoryKind::InputPrimitive, &[&["radio_b"]]), 102 98 Story::root("slider", StoryKind::InputPrimitive), 103 - Story::root_with( 104 - "tabs", 105 - StoryKind::InputPrimitive, 106 - &[&["tab_a"], &["tab_b"]], 107 - ), 99 + Story::root_with("tabs", StoryKind::InputPrimitive, &[&["tab_a"], &["tab_b"]]), 108 100 Story::root_with("status_bar", StoryKind::Composite, &[&["status_item"]]), 109 101 Story::root_with("panel", StoryKind::Composite, &[&["panel", "titlebar"]]), 110 102 Story::root_with( ··· 429 421 paint.extend(response.paint); 430 422 let response = show_toggle_button( 431 423 ctx, 432 - ToggleButton::new(id("toggle"), rect(0.0, 52.0, 80.0, 20.0), GALLERY_LABEL, true), 424 + ToggleButton::new( 425 + id("toggle"), 426 + rect(0.0, 52.0, 80.0, 20.0), 427 + GALLERY_LABEL, 428 + true, 429 + ), 433 430 ); 434 431 paint.extend(response.paint); 435 432 let response = show_radio_group( ··· 486 483 ), 487 484 ); 488 485 paint.extend(response.paint); 489 - let status_items = [ 490 - StatusItem::new( 491 - id("status_item"), 492 - GALLERY_LABEL, 493 - StatusAlign::Start, 494 - LayoutPx::new(80.0), 495 - ) 496 - .interactive(true), 497 - ]; 486 + let status_items = [StatusItem::new( 487 + id("status_item"), 488 + GALLERY_LABEL, 489 + StatusAlign::Start, 490 + LayoutPx::new(80.0), 491 + ) 492 + .interactive(true)]; 498 493 let response = show_status_bar( 499 494 ctx, 500 495 StatusBar::new( ··· 507 502 paint.extend(response.paint); 508 503 let response = show_panel( 509 504 ctx, 510 - Panel::new(id("panel"), rect(0.0, 192.0, 200.0, 100.0), &mut state.panel).titlebar( 511 - PanelTitlebar { 512 - label: GALLERY_LABEL, 513 - height: LayoutPx::new(22.0), 514 - collapsible: true, 515 - }, 516 - ), 505 + Panel::new( 506 + id("panel"), 507 + rect(0.0, 192.0, 200.0, 100.0), 508 + &mut state.panel, 509 + ) 510 + .titlebar(PanelTitlebar { 511 + label: GALLERY_LABEL, 512 + height: LayoutPx::new(22.0), 513 + collapsible: true, 514 + }), 517 515 ); 518 516 paint.extend(response.paint); 519 517 let response = show_dropdown( ··· 720 718 ToolbarItem::new(id("ribbon_tool_a"), GALLERY_LABEL), 721 719 ToolbarItem::new(id("ribbon_tool_b"), GALLERY_LABEL), 722 720 ]; 723 - let ribbon_tabs = [RibbonTab { 724 - tab: Tab::new(id("ribbon_tab"), rect(0.0, 0.0, 80.0, 24.0), GALLERY_LABEL), 725 - groups: vec![RibbonGroup { 721 + let ribbon_tabs = [RibbonTab::new( 722 + id("ribbon_tab"), 723 + GALLERY_LABEL, 724 + vec![RibbonGroup { 726 725 id: id("ribbon_group"), 727 726 label: GALLERY_LABEL, 728 727 items: ribbon_toolbar_items.to_vec(), 729 728 icon_size: RibbonIconSize::Large, 730 729 width: LayoutPx::new(140.0), 731 730 }], 732 - }]; 731 + )]; 733 732 let response = show_ribbon( 734 733 ctx, 735 734 Ribbon::new( ··· 813 812 } 814 813 815 814 const BUTTON_VARIANTS: [(&str, ButtonVariant, ButtonState); 6] = [ 816 - ("button_secondary", ButtonVariant::Secondary, ButtonState::Idle), 817 - ("button_destructive", ButtonVariant::Destructive, ButtonState::Idle), 815 + ( 816 + "button_secondary", 817 + ButtonVariant::Secondary, 818 + ButtonState::Idle, 819 + ), 820 + ( 821 + "button_destructive", 822 + ButtonVariant::Destructive, 823 + ButtonState::Idle, 824 + ), 818 825 ("button_ghost", ButtonVariant::Ghost, ButtonState::Idle), 819 826 ("button_icon", ButtonVariant::IconOnly, ButtonState::Idle), 820 - ("button_disabled", ButtonVariant::Primary, ButtonState::Disabled), 821 - ("button_loading", ButtonVariant::Primary, ButtonState::Loading), 827 + ( 828 + "button_disabled", 829 + ButtonVariant::Primary, 830 + ButtonState::Disabled, 831 + ), 832 + ( 833 + "button_loading", 834 + ButtonVariant::Primary, 835 + ButtonState::Loading, 836 + ), 822 837 ]; 823 838 824 839 const CHECKBOX_VARIANTS: [(&str, CheckboxState); 2] = [ ··· 878 893 }); 879 894 let response = show_toggle_button( 880 895 ctx, 881 - ToggleButton::new(id("toggle_off"), rect(0.0, 468.0, 80.0, 20.0), GALLERY_LABEL, false), 896 + ToggleButton::new( 897 + id("toggle_off"), 898 + rect(0.0, 468.0, 80.0, 20.0), 899 + GALLERY_LABEL, 900 + false, 901 + ), 882 902 ); 883 903 paint.extend(response.paint); 884 904 let response = show_panel(
+5 -2
crates/bone-ui/src/input/script.rs
··· 2 2 3 3 use crate::layout::{LayoutOffset, LayoutPos, LayoutPx}; 4 4 5 + use super::InputSnapshot; 5 6 use super::key::{KeyChar, KeyCode, KeyEvent, ModifierMask, NamedKey}; 6 7 use super::pointer::{FrameInstant, PointerButton, PointerButtonMask, PointerSample}; 7 - use super::InputSnapshot; 8 8 9 9 const DEFAULT_TICK: Duration = Duration::from_millis(16); 10 10 const DRAG_SAMPLES: usize = 8; ··· 241 241 fn named_key_step_records_named_code() { 242 242 let frames = Script::new().named(NamedKey::Escape).frames(); 243 243 assert_eq!(frames.len(), 1); 244 - assert_eq!(frames[0].keys_pressed[0].code, KeyCode::Named(NamedKey::Escape)); 244 + assert_eq!( 245 + frames[0].keys_pressed[0].code, 246 + KeyCode::Named(NamedKey::Escape) 247 + ); 245 248 } 246 249 247 250 #[test]
+1 -1
crates/bone-ui/src/layout/engine.rs
··· 240 240 state, 241 241 panels, 242 242 tab_strip_height, 243 - } => self.dock_host(*id, state, panels, *tab_strip_height, retained), 243 + } => self.dock_host(*id, state.as_ref(), panels, *tab_strip_height, retained), 244 244 Layout::Leaf { id } => { 245 245 self.leaf_node(NodeKind::Leaf(*id), fill_along_main(Style::DEFAULT)) 246 246 }
+4 -2
crates/bone-ui/src/layout/primitives.rs
··· 1 + use std::sync::Arc; 2 + 1 3 use super::axis::{Axis, CrossAxisAlign, MainAxisJustify}; 2 4 use super::dock::{DockState, PanelId, SplitFraction}; 3 5 use super::geometry::EdgeInsets; ··· 66 68 }, 67 69 DockHost { 68 70 id: WidgetId, 69 - state: DockState, 71 + state: Arc<DockState>, 70 72 panels: Vec<DockPanel>, 71 73 tab_strip_height: Spacing, 72 74 }, ··· 128 130 #[must_use] 129 131 pub fn dock_host( 130 132 id: WidgetId, 131 - state: DockState, 133 + state: Arc<DockState>, 132 134 panels: Vec<DockPanel>, 133 135 tab_strip_height: Spacing, 134 136 ) -> Self {
+9 -8
crates/bone-ui/src/layout/tests.rs
··· 1 1 use core::num::{NonZeroU16, NonZeroU32, NonZeroU64}; 2 + use std::sync::Arc; 2 3 3 4 use super::axis::{Axis, CrossAxisAlign, LayoutDirection, MainAxisJustify}; 4 5 use super::dock::{ ··· 396 397 ]; 397 398 let layout = Layout::DockHost { 398 399 id: wid(50), 399 - state: dock, 400 + state: Arc::new(dock), 400 401 panels, 401 402 tab_strip_height: sp(24.0), 402 403 }; ··· 695 696 ]; 696 697 let layout = Layout::DockHost { 697 698 id: wid(99), 698 - state: dock, 699 + state: Arc::new(dock), 699 700 panels, 700 701 tab_strip_height: sp(24.0), 701 702 }; ··· 713 714 } 714 715 let layout = Layout::DockHost { 715 716 id: wid(50), 716 - state: DockState::new(tabs), 717 + state: Arc::new(DockState::new(tabs)), 717 718 panels: vec![ 718 719 DockPanel { 719 720 id: a, ··· 748 749 } 749 750 let layout = Layout::DockHost { 750 751 id: wid(80), 751 - state: DockState::new(tabs), 752 + state: Arc::new(DockState::new(tabs)), 752 753 panels: vec![ 753 754 DockPanel { 754 755 id: a, ··· 788 789 } 789 790 let layout = Layout::DockHost { 790 791 id: wid(81), 791 - state: DockState::new(tabs), 792 + state: Arc::new(DockState::new(tabs)), 792 793 panels: vec![ 793 794 DockPanel { 794 795 id: a, ··· 1003 1004 fn dock_host_with_empty_tabs_returns_empty_dock_tabs_error() { 1004 1005 let layout = Layout::DockHost { 1005 1006 id: wid(60), 1006 - state: DockState::new(DockNode::tabs(Vec::new())), 1007 + state: Arc::new(DockState::new(DockNode::tabs(Vec::new()))), 1007 1008 panels: Vec::new(), 1008 1009 tab_strip_height: sp(24.0), 1009 1010 }; ··· 1028 1029 )); 1029 1030 let layout = Layout::DockHost { 1030 1031 id: wid(70), 1031 - state: dock, 1032 + state: Arc::new(dock), 1032 1033 panels: vec![ 1033 1034 DockPanel { 1034 1035 id: a, ··· 1390 1391 }; 1391 1392 let layout = Layout::DockHost { 1392 1393 id: wid(60), 1393 - state, 1394 + state: Arc::new(state), 1394 1395 panels: vec![DockPanel { 1395 1396 id: pid(1), 1396 1397 child: Layout::leaf(wid(1)),
+10 -16
crates/bone-ui/src/widgets/menu.rs
··· 673 673 item_width, 674 674 item_padding, 675 675 } = bar; 676 - ctx.a11y.push( 677 - id, 678 - rect, 679 - AccessNode::new(Role::MenuBar).with_label(label), 680 - ); 676 + ctx.a11y 677 + .push(id, rect, AccessNode::new(Role::MenuBar).with_label(label)); 681 678 let mut paint = vec![WidgetPaint::Surface { 682 679 rect, 683 680 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L0), ··· 1129 1126 focus.request_focus(entries[0].id); 1130 1127 let prev = HitState::new(); 1131 1128 1132 - [ 1133 - InputSnapshot::idle(FrameInstant::ZERO), 1134 - { 1135 - let mut s = InputSnapshot::idle(FrameInstant::ZERO); 1136 - s.keys_pressed.push(KeyEvent::new( 1137 - KeyCode::Named(NamedKey::ArrowDown), 1138 - ModifierMask::NONE, 1139 - )); 1140 - s 1141 - }, 1142 - ] 1129 + [InputSnapshot::idle(FrameInstant::ZERO), { 1130 + let mut s = InputSnapshot::idle(FrameInstant::ZERO); 1131 + s.keys_pressed.push(KeyEvent::new( 1132 + KeyCode::Named(NamedKey::ArrowDown), 1133 + ModifierMask::NONE, 1134 + )); 1135 + s 1136 + }] 1143 1137 .into_iter() 1144 1138 .for_each(|mut snap| { 1145 1139 let mut hits = HitFrame::new();
+29 -9
crates/bone-ui/src/widgets/panel.rs
··· 380 380 let prev = HitState::new(); 381 381 focus.request_focus(toggle_id); 382 382 let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 383 - let _ = render(&mut state, &mut focus, &mut warm, &prev, Some(titlebar(true))); 383 + let _ = render( 384 + &mut state, 385 + &mut focus, 386 + &mut warm, 387 + &prev, 388 + Some(titlebar(true)), 389 + ); 384 390 assert_eq!(focus.focused(), Some(toggle_id)); 385 391 386 392 let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 387 - enter 388 - .keys_pressed 389 - .push(KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE)); 390 - let _ = render(&mut state, &mut focus, &mut enter, &prev, Some(titlebar(true))); 393 + enter.keys_pressed.push(KeyEvent::new( 394 + KeyCode::Named(NamedKey::Enter), 395 + ModifierMask::NONE, 396 + )); 397 + let _ = render( 398 + &mut state, 399 + &mut focus, 400 + &mut enter, 401 + &prev, 402 + Some(titlebar(true)), 403 + ); 391 404 assert!(state.collapsed, "Enter on focused titlebar must collapse"); 392 405 393 406 let mut space = InputSnapshot::idle(FrameInstant::ZERO); 394 - space 395 - .keys_pressed 396 - .push(KeyEvent::new(KeyCode::Named(NamedKey::Space), ModifierMask::NONE)); 397 - let _ = render(&mut state, &mut focus, &mut space, &prev, Some(titlebar(true))); 407 + space.keys_pressed.push(KeyEvent::new( 408 + KeyCode::Named(NamedKey::Space), 409 + ModifierMask::NONE, 410 + )); 411 + let _ = render( 412 + &mut state, 413 + &mut focus, 414 + &mut space, 415 + &prev, 416 + Some(titlebar(true)), 417 + ); 398 418 assert!(!state.collapsed, "Space on focused titlebar must expand"); 399 419 } 400 420
+24 -4
crates/bone-ui/src/widgets/property_grid.rs
··· 526 526 }]; 527 527 show_property_grid( 528 528 &mut ctx, 529 - PropertyGrid::new(grid_id(), rect(), StringKey::new("test.grid"), &mut rows), 529 + PropertyGrid::new( 530 + grid_id(), 531 + rect(), 532 + StringKey::new("test.grid"), 533 + &mut rows, 534 + ), 530 535 &mut clipboard, 531 536 ) 532 537 }; ··· 656 661 }; 657 662 658 663 focus.request(FocusRequest::Advance); 659 - render(&mut focus, &mut bool_a, &mut bool_b, InputSnapshot::idle(FrameInstant::ZERO)); 664 + render( 665 + &mut focus, 666 + &mut bool_a, 667 + &mut bool_b, 668 + InputSnapshot::idle(FrameInstant::ZERO), 669 + ); 660 670 assert_eq!(focus.focused(), Some(alpha_id)); 661 671 662 672 focus.request(FocusRequest::Advance); 663 - render(&mut focus, &mut bool_a, &mut bool_b, InputSnapshot::idle(FrameInstant::ZERO)); 673 + render( 674 + &mut focus, 675 + &mut bool_a, 676 + &mut bool_b, 677 + InputSnapshot::idle(FrameInstant::ZERO), 678 + ); 664 679 assert_eq!(focus.focused(), Some(beta_id)); 665 680 666 681 focus.request(FocusRequest::Retreat); 667 - render(&mut focus, &mut bool_a, &mut bool_b, InputSnapshot::idle(FrameInstant::ZERO)); 682 + render( 683 + &mut focus, 684 + &mut bool_a, 685 + &mut bool_b, 686 + InputSnapshot::idle(FrameInstant::ZERO), 687 + ); 668 688 assert_eq!(focus.focused(), Some(alpha_id)); 669 689 } 670 690
+66 -71
crates/bone-ui/src/widgets/ribbon.rs
··· 38 38 39 39 #[derive(Clone, Debug, PartialEq)] 40 40 pub struct RibbonTab { 41 - pub tab: Tab, 41 + pub id: WidgetId, 42 + pub label: StringKey, 43 + pub disabled: bool, 44 + pub closable: bool, 42 45 pub groups: Vec<RibbonGroup>, 46 + } 47 + 48 + impl RibbonTab { 49 + #[must_use] 50 + pub const fn new(id: WidgetId, label: StringKey, groups: Vec<RibbonGroup>) -> Self { 51 + Self { 52 + id, 53 + label, 54 + disabled: false, 55 + closable: false, 56 + groups, 57 + } 58 + } 59 + 60 + #[must_use] 61 + pub const fn closable(mut self, closable: bool) -> Self { 62 + self.closable = closable; 63 + self 64 + } 65 + 66 + #[must_use] 67 + pub const fn disabled(mut self, disabled: bool) -> Self { 68 + self.disabled = disabled; 69 + self 70 + } 43 71 } 44 72 45 73 #[derive(Clone, Debug, Default, PartialEq, Eq)] ··· 123 151 ), 124 152 ); 125 153 let tab_views: Vec<Tab> = build_tab_strip(tabs, strip_rect); 126 - ctx.a11y.push( 127 - id, 128 - rect, 129 - AccessNode::new(Role::TabPanel).with_label(label), 130 - ); 154 + ctx.a11y 155 + .push(id, rect, AccessNode::new(Role::TabPanel).with_label(label)); 131 156 let mut paint = vec![WidgetPaint::Surface { 132 157 rect, 133 158 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L1), ··· 150 175 ); 151 176 paint.extend(tabs_response.paint); 152 177 let mut activated_tool: Option<WidgetId> = None; 153 - if let Some(active_tab) = tabs.iter().find(|t| t.tab.id == active) { 178 + if let Some(active_tab) = tabs.iter().find(|t| t.id == active) { 154 179 let groups_paint = render_groups( 155 180 ctx, 156 181 GroupsArgs { ··· 204 229 ), 205 230 LayoutSize::new(LayoutPx::new(stride), strip_rect.size.height), 206 231 ); 207 - Tab { rect, ..t.tab } 232 + Tab::new(t.id, rect, t.label) 233 + .closable(t.closable) 234 + .disabled(t.disabled) 208 235 }) 209 236 .collect() 210 237 } ··· 339 366 use crate::strings::{StringKey, StringTable}; 340 367 use crate::theme::Theme; 341 368 use crate::widget_id::{WidgetId, WidgetKey}; 342 - use crate::widgets::{Tab, ToolbarItem}; 369 + use crate::widgets::ToolbarItem; 343 370 344 371 fn ribbon_id() -> WidgetId { 345 372 WidgetId::ROOT.child(WidgetKey::new("ribbon")) ··· 348 375 fn make_ribbon_tab(name: &'static str) -> RibbonTab { 349 376 let tab_id = ribbon_id().child(WidgetKey::new(name)); 350 377 let tool_id = tab_id.child(WidgetKey::new("tool")); 351 - RibbonTab { 352 - tab: Tab::new( 353 - tab_id, 354 - LayoutRect::new( 355 - LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), 356 - LayoutSize::new(LayoutPx::new(80.0), LayoutPx::new(28.0)), 357 - ), 358 - StringKey::new("ribbon.tab"), 359 - ), 360 - groups: vec![RibbonGroup { 378 + RibbonTab::new( 379 + tab_id, 380 + StringKey::new("ribbon.tab"), 381 + vec![RibbonGroup { 361 382 id: tab_id.child(WidgetKey::new("group")), 362 383 label: StringKey::new("ribbon.group"), 363 384 items: vec![ToolbarItem::new(tool_id, StringKey::new("ribbon.tool"))], 364 385 icon_size: RibbonIconSize::Large, 365 386 width: LayoutPx::new(120.0), 366 387 }], 367 - } 388 + ) 368 389 } 369 390 370 391 fn render( ··· 421 442 [press(click_pos), release(click_pos), idle(click_pos)] 422 443 .into_iter() 423 444 .for_each(|mut snap| { 424 - let (response, next) = render( 425 - &tabs, 426 - tabs[0].tab.id, 427 - &mut state, 428 - &mut focus, 429 - &mut snap, 430 - &prev, 431 - ); 445 + let (response, next) = 446 + render(&tabs, tabs[0].id, &mut state, &mut focus, &mut snap, &prev); 432 447 last = Some(response); 433 448 prev = next; 434 449 }); 435 450 let Some(response) = last else { 436 451 panic!("response missing") 437 452 }; 438 - assert_eq!(response.activated_tab, Some(tabs[1].tab.id)); 453 + assert_eq!(response.activated_tab, Some(tabs[1].id)); 439 454 } 440 455 441 456 #[test] ··· 449 464 [press(click_pos), release(click_pos), idle(click_pos)] 450 465 .into_iter() 451 466 .for_each(|mut snap| { 452 - let (response, next) = render( 453 - &tabs, 454 - tabs[0].tab.id, 455 - &mut state, 456 - &mut focus, 457 - &mut snap, 458 - &prev, 459 - ); 467 + let (response, next) = 468 + render(&tabs, tabs[0].id, &mut state, &mut focus, &mut snap, &prev); 460 469 last = Some(response); 461 470 prev = next; 462 471 }); ··· 468 477 469 478 #[test] 470 479 fn closing_a_closable_ribbon_tab_propagates_closed_tab() { 471 - let make_closable = |name: &'static str| -> RibbonTab { 472 - let mut t = make_ribbon_tab(name); 473 - t.tab = t.tab.closable(true); 474 - t 475 - }; 480 + let make_closable = 481 + |name: &'static str| -> RibbonTab { make_ribbon_tab(name).closable(true) }; 476 482 let tabs = vec![make_closable("home"), make_closable("sketch")]; 477 483 let mut state = RibbonState::default(); 478 484 let mut focus = FocusManager::new(); ··· 485 491 [press(close_pos), release(close_pos), idle(close_pos)] 486 492 .into_iter() 487 493 .for_each(|mut snap| { 488 - let (response, next) = render( 489 - &tabs, 490 - tabs[0].tab.id, 491 - &mut state, 492 - &mut focus, 493 - &mut snap, 494 - &prev, 495 - ); 494 + let (response, next) = 495 + render(&tabs, tabs[0].id, &mut state, &mut focus, &mut snap, &prev); 496 496 last = Some(response); 497 497 prev = next; 498 498 }); 499 499 let Some(response) = last else { 500 500 panic!("response missing") 501 501 }; 502 - assert_eq!(response.closed_tab, Some(tabs[1].tab.id)); 502 + assert_eq!(response.closed_tab, Some(tabs[1].id)); 503 503 assert!(response.activated_tab.is_none()); 504 504 } 505 505 ··· 511 511 let mut state = RibbonState::default(); 512 512 let mut focus = FocusManager::new(); 513 513 let prev = HitState::new(); 514 - focus.request_focus(tabs[0].tab.id); 514 + focus.request_focus(tabs[0].id); 515 515 let mut warm = InputSnapshot::idle(FrameInstant::ZERO); 516 - let _ = render(&tabs, tabs[0].tab.id, &mut state, &mut focus, &mut warm, &prev); 517 - assert_eq!(focus.focused(), Some(tabs[0].tab.id)); 516 + let _ = render(&tabs, tabs[0].id, &mut state, &mut focus, &mut warm, &prev); 517 + assert_eq!(focus.focused(), Some(tabs[0].id)); 518 518 519 519 let mut arrow = InputSnapshot::idle(FrameInstant::ZERO); 520 - arrow 521 - .keys_pressed 522 - .push(KeyEvent::new(KeyCode::Named(NamedKey::ArrowRight), ModifierMask::NONE)); 523 - let _ = render(&tabs, tabs[0].tab.id, &mut state, &mut focus, &mut arrow, &prev); 524 - assert_eq!(focus.focused(), Some(tabs[1].tab.id)); 520 + arrow.keys_pressed.push(KeyEvent::new( 521 + KeyCode::Named(NamedKey::ArrowRight), 522 + ModifierMask::NONE, 523 + )); 524 + let _ = render(&tabs, tabs[0].id, &mut state, &mut focus, &mut arrow, &prev); 525 + assert_eq!(focus.focused(), Some(tabs[1].id)); 525 526 526 527 let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 527 - enter 528 - .keys_pressed 529 - .push(KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE)); 530 - let (response, _) = render( 531 - &tabs, 532 - tabs[0].tab.id, 533 - &mut state, 534 - &mut focus, 535 - &mut enter, 536 - &prev, 537 - ); 538 - assert_eq!(response.activated_tab, Some(tabs[1].tab.id)); 528 + enter.keys_pressed.push(KeyEvent::new( 529 + KeyCode::Named(NamedKey::Enter), 530 + ModifierMask::NONE, 531 + )); 532 + let (response, _) = render(&tabs, tabs[0].id, &mut state, &mut focus, &mut enter, &prev); 533 + assert_eq!(response.activated_tab, Some(tabs[1].id)); 539 534 } 540 535 541 536 fn press(pos: LayoutPos) -> InputSnapshot {
+4 -3
crates/bone-ui/src/widgets/status_bar.rs
··· 401 401 assert_eq!(focus.focused(), Some(interactive_id)); 402 402 403 403 let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 404 - enter 405 - .keys_pressed 406 - .push(KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE)); 404 + enter.keys_pressed.push(KeyEvent::new( 405 + KeyCode::Named(NamedKey::Enter), 406 + ModifierMask::NONE, 407 + )); 407 408 let (response, _) = render(&items, &mut focus, &mut enter, &prev); 408 409 assert_eq!(response.activated, Some(interactive_id)); 409 410 }
+2 -5
crates/bone-ui/src/widgets/table.rs
··· 107 107 mode, 108 108 row_height, 109 109 } = view; 110 - ctx.a11y.push( 111 - id, 112 - rect, 113 - AccessNode::new(Role::ListBox).with_label(label), 114 - ); 110 + ctx.a11y 111 + .push(id, rect, AccessNode::new(Role::ListBox).with_label(label)); 115 112 let mut paint = vec![WidgetPaint::Surface { 116 113 rect, 117 114 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L0),
+53 -39
crates/bone-ui/src/widgets/toast.rs
··· 151 151 color: ctx.theme().colors.text_primary(), 152 152 role: ctx.theme().typography.body, 153 153 }); 154 - let mut dismissed_now = false; 155 - if dismissible { 156 - let close_id = id.child(WidgetKey::new("close")); 157 - let close_rect = close_button_rect(rect); 158 - let interaction = ctx.interact( 159 - InteractDeclaration::new(close_id, close_rect, Sense::INTERACTIVE) 160 - .focusable(true) 161 - .a11y(AccessNode::new(Role::Button).with_label(StringKey::new("toast.close"))), 162 - ); 163 - let live_focused = ctx.is_focused(close_id); 164 - let key_activated = live_focused 165 - && take_key( 166 - ctx.input, 167 - &[TakeKey::named(NamedKey::Enter), TakeKey::named(NamedKey::Space)], 168 - ) 169 - .is_some(); 170 - if interaction.click() || key_activated { 171 - state.dismissed = true; 172 - dismissed_now = true; 173 - } 174 - paint.push(WidgetPaint::Surface { 175 - rect: close_rect, 176 - fill: if interaction.hover() { 177 - ctx.theme().colors.neutral.step(Step12::HOVER_BG) 178 - } else { 179 - Color::TRANSPARENT 180 - }, 181 - border: None, 182 - radius: ctx.theme().radius.sm, 183 - elevation: None, 184 - }); 185 - paint.push(WidgetPaint::Mark { 186 - rect: close_rect, 187 - kind: GlyphMark::Close, 188 - color: ctx.theme().colors.text_secondary(), 189 - }); 190 - push_focus_ring(ctx, &mut paint, close_rect, ctx.theme().radius.sm, live_focused); 191 - } 154 + let dismissed_now = dismissible && draw_close_button(ctx, id, rect, state, &mut paint); 192 155 ToastResponse { 193 156 visible: true, 194 157 dismissed_now, 195 158 paint, 196 159 } 160 + } 161 + 162 + fn draw_close_button( 163 + ctx: &mut FrameCtx<'_>, 164 + id: WidgetId, 165 + rect: LayoutRect, 166 + state: &mut ToastState, 167 + paint: &mut Vec<WidgetPaint>, 168 + ) -> bool { 169 + let close_id = id.child(WidgetKey::new("close")); 170 + let close_rect = close_button_rect(rect); 171 + let interaction = ctx.interact( 172 + InteractDeclaration::new(close_id, close_rect, Sense::INTERACTIVE) 173 + .focusable(true) 174 + .a11y(AccessNode::new(Role::Button).with_label(StringKey::new("toast.close"))), 175 + ); 176 + let live_focused = ctx.is_focused(close_id); 177 + let key_activated = live_focused 178 + && take_key( 179 + ctx.input, 180 + &[ 181 + TakeKey::named(NamedKey::Enter), 182 + TakeKey::named(NamedKey::Space), 183 + ], 184 + ) 185 + .is_some(); 186 + let dismissed_now = interaction.click() || key_activated; 187 + if dismissed_now { 188 + state.dismissed = true; 189 + } 190 + paint.push(WidgetPaint::Surface { 191 + rect: close_rect, 192 + fill: if interaction.hover() { 193 + ctx.theme().colors.neutral.step(Step12::HOVER_BG) 194 + } else { 195 + Color::TRANSPARENT 196 + }, 197 + border: None, 198 + radius: ctx.theme().radius.sm, 199 + elevation: None, 200 + }); 201 + paint.push(WidgetPaint::Mark { 202 + rect: close_rect, 203 + kind: GlyphMark::Close, 204 + color: ctx.theme().colors.text_secondary(), 205 + }); 206 + push_focus_ring(ctx, paint, close_rect, ctx.theme().radius.sm, live_focused); 207 + dismissed_now 197 208 } 198 209 199 210 fn surface_fill(ctx: &FrameCtx<'_>, kind: ToastKind) -> Color { ··· 390 401 ModifierMask::NONE, 391 402 )); 392 403 let _ = render(&mut state, &mut focus, &mut enter, &prev, 4000); 393 - assert!(state.dismissed, "Enter on focused close button must dismiss"); 404 + assert!( 405 + state.dismissed, 406 + "Enter on focused close button must dismiss" 407 + ); 394 408 } 395 409 396 410 #[test]
+10 -11
crates/bone-ui/src/widgets/toolbar.rs
··· 125 125 } = toolbar; 126 126 let visible_count = compute_visible_count(rect, items.len(), item_size, item_gap, orientation); 127 127 let needs_overflow = visible_count < items.len(); 128 - ctx.a11y.push( 129 - id, 130 - rect, 131 - AccessNode::new(Role::Toolbar).with_label(label), 132 - ); 128 + ctx.a11y 129 + .push(id, rect, AccessNode::new(Role::Toolbar).with_label(label)); 133 130 let mut paint = Vec::new(); 134 131 let mut activated: Option<WidgetId> = None; 135 132 items ··· 586 583 assert_eq!(focus.focused(), Some(items[1].id)); 587 584 588 585 let mut snap_space = InputSnapshot::idle(FrameInstant::ZERO); 589 - snap_space 590 - .keys_pressed 591 - .push(KeyEvent::new(KeyCode::Named(NamedKey::Space), ModifierMask::NONE)); 586 + snap_space.keys_pressed.push(KeyEvent::new( 587 + KeyCode::Named(NamedKey::Space), 588 + ModifierMask::NONE, 589 + )); 592 590 let (response, _) = render( 593 591 &items, 594 592 rect, ··· 624 622 ); 625 623 626 624 let mut enter = InputSnapshot::idle(FrameInstant::ZERO); 627 - enter 628 - .keys_pressed 629 - .push(KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE)); 625 + enter.keys_pressed.push(KeyEvent::new( 626 + KeyCode::Named(NamedKey::Enter), 627 + ModifierMask::NONE, 628 + )); 630 629 let (response, _) = render( 631 630 &items, 632 631 rect,
+32 -15
crates/bone-ui/src/widgets/tree_view.rs
··· 17 17 #[derive(Clone, Debug, PartialEq)] 18 18 pub struct TreeNode { 19 19 pub id: WidgetId, 20 - pub label: StringKey, 20 + pub label: LabelText, 21 21 pub children: Vec<TreeNode>, 22 22 } 23 23 24 24 impl TreeNode { 25 25 #[must_use] 26 - pub const fn leaf(id: WidgetId, label: StringKey) -> Self { 27 - Self { 28 - id, 29 - label, 30 - children: Vec::new(), 31 - } 26 + pub fn leaf(id: WidgetId, label: StringKey) -> Self { 27 + Self::with_label(id, LabelText::Key(label), Vec::new()) 28 + } 29 + 30 + #[must_use] 31 + pub fn leaf_owned(id: WidgetId, label: String) -> Self { 32 + Self::with_label(id, LabelText::Owned(label), Vec::new()) 33 + } 34 + 35 + #[must_use] 36 + pub fn parent(id: WidgetId, label: StringKey, children: Vec<TreeNode>) -> Self { 37 + Self::with_label(id, LabelText::Key(label), children) 38 + } 39 + 40 + #[must_use] 41 + pub fn parent_owned(id: WidgetId, label: String, children: Vec<TreeNode>) -> Self { 42 + Self::with_label(id, LabelText::Owned(label), children) 32 43 } 33 44 34 45 #[must_use] 35 - pub const fn parent(id: WidgetId, label: StringKey, children: Vec<TreeNode>) -> Self { 46 + fn with_label(id: WidgetId, label: LabelText, children: Vec<TreeNode>) -> Self { 36 47 Self { 37 48 id, 38 49 label, ··· 223 234 #[derive(Clone, Debug, PartialEq)] 224 235 struct VisibleRow { 225 236 id: WidgetId, 226 - label: StringKey, 237 + label: LabelText, 227 238 depth: usize, 228 239 has_children: bool, 229 240 } ··· 234 245 .flat_map(|node| { 235 246 let row = VisibleRow { 236 247 id: node.id, 237 - label: node.label, 248 + label: node.label.clone(), 238 249 depth, 239 250 has_children: node.has_children(), 240 251 }; ··· 297 308 .active(selected) 298 309 .a11y({ 299 310 let node = AccessNode::new(Role::TreeItem) 300 - .with_label(row.label) 311 + .with_label_text(row.label.clone()) 301 312 .with_selected(selected); 302 313 if row.has_children { 303 314 node.with_expanded(expanded) ··· 332 343 } 333 344 if state.renaming == Some(row.id) { 334 345 paint.extend(draw_rename_editor( 335 - ctx, row.id, row.label, label_rect, state, 346 + ctx, row.id, &row.label, label_rect, state, 336 347 )); 337 348 } else { 338 349 paint.push(label_paint(ctx, row, label_rect)); ··· 423 434 let disclosure_interaction = ctx.interact( 424 435 InteractDeclaration::new(disclosure_id, disclosure_rect, Sense::INTERACTIVE).a11y( 425 436 AccessNode::new(Role::DisclosureTriangle) 426 - .with_label(row.label) 437 + .with_label_text(row.label.clone()) 427 438 .with_expanded(state.expanded.contains(&row.id)), 428 439 ), 429 440 ); ··· 444 455 fn label_paint(ctx: &FrameCtx<'_>, row: &VisibleRow, label_rect: LayoutRect) -> WidgetPaint { 445 456 WidgetPaint::Label { 446 457 rect: label_rect, 447 - text: LabelText::Key(row.label), 458 + text: row.label.clone(), 448 459 color: ctx.theme().colors.text_primary(), 449 460 role: ctx.theme().typography.body, 450 461 } 451 462 } 452 463 464 + const RENAME_FALLBACK_PLACEHOLDER: StringKey = StringKey::new("tree.rename.placeholder"); 465 + 453 466 fn draw_rename_editor( 454 467 ctx: &mut FrameCtx<'_>, 455 468 row_id: WidgetId, 456 - placeholder: StringKey, 469 + label: &LabelText, 457 470 label_rect: LayoutRect, 458 471 state: &mut TreeViewState, 459 472 ) -> Vec<WidgetPaint> { 473 + let placeholder = match label { 474 + LabelText::Key(k) => *k, 475 + LabelText::Owned(_) => RENAME_FALLBACK_PLACEHOLDER, 476 + }; 460 477 let rename_id = row_id.child(WidgetKey::new("rename")); 461 478 let response = show_text_input( 462 479 ctx,
+5 -1
crates/bone-ui/tests/input_script_drive.rs
··· 51 51 (response, next) 52 52 } 53 53 54 - fn drive_button(focus: &mut FocusManager, prev: &HitState, snap: &mut InputSnapshot) -> (bool, HitState) { 54 + fn drive_button( 55 + focus: &mut FocusManager, 56 + prev: &HitState, 57 + snap: &mut InputSnapshot, 58 + ) -> (bool, HitState) { 55 59 let id = child("button"); 56 60 let (response, next) = run(focus, prev, snap, |ctx| { 57 61 show_button(