Another project
0

Configure Feed

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

at main 6.2 kB View raw
1use bone_ui::frame::FrameCtx; 2use bone_ui::hotkey::{ActionId, HotkeyScope}; 3use bone_ui::input::{KeyCode, ModifierMask, NamedKey, PointerButton}; 4use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 5use bone_ui::strings::StringKey; 6use bone_ui::theme::{Border, Step12, StrokeWidth}; 7use bone_ui::widgets::{ 8 Button, ButtonState, ButtonVariant, LabelText, TakeKey, WidgetPaint, show_button, take_key, 9}; 10use bone_ui::{WidgetId, WidgetKey}; 11 12use crate::hotkeys::{ 13 COMMANDS, DELETE_SELECTION_ACTION, ENTER_SKETCH_ACTION, EXTEND_ACTION, MIRROR_ACTION, 14 SELECT_ALL_ACTION, SMART_DIMENSION_ACTION, TOGGLE_CONSTRUCTION_ACTION, TRIM_ACTION, 15 ZOOM_FIT_ACTION, 16}; 17use crate::strings as s; 18 19const ITEM_HEIGHT_PX: f32 = 28.0; 20const ITEM_GAP_PX: f32 = 2.0; 21const PANEL_WIDTH_PX: f32 = 220.0; 22const PANEL_PAD_PX: f32 = 6.0; 23const TITLE_HEIGHT_PX: f32 = 24.0; 24 25#[derive(Copy, Clone, Debug, PartialEq)] 26pub struct ShortcutBarState { 27 pub anchor: LayoutPos, 28} 29 30#[derive(Debug, Default)] 31pub struct ShortcutBarOutcome { 32 pub paints: Vec<WidgetPaint>, 33 pub activated: Option<ActionId>, 34 pub dismissed: bool, 35} 36 37const ITEMS: &[(ActionId, StringKey)] = &[ 38 (ENTER_SKETCH_ACTION, s::HOTKEY_LABEL_SKETCH), 39 (SMART_DIMENSION_ACTION, s::HOTKEY_LABEL_SMART_DIMENSION), 40 (TRIM_ACTION, s::HOTKEY_LABEL_TRIM), 41 (EXTEND_ACTION, s::HOTKEY_LABEL_EXTEND), 42 (MIRROR_ACTION, s::HOTKEY_LABEL_MIRROR), 43 ( 44 TOGGLE_CONSTRUCTION_ACTION, 45 s::HOTKEY_LABEL_CONSTRUCTION_TOGGLE, 46 ), 47 (ZOOM_FIT_ACTION, s::HOTKEY_LABEL_ZOOM_FIT), 48 (SELECT_ALL_ACTION, s::HOTKEY_LABEL_SELECT_ALL), 49 (DELETE_SELECTION_ACTION, s::HOTKEY_LABEL_DELETE_SELECTION), 50]; 51 52#[must_use] 53#[allow( 54 clippy::cast_precision_loss, 55 reason = "ITEMS.len() and item indices fit in u8; f32 mantissa is fine" 56)] 57pub fn panel_rect(anchor: LayoutPos, viewport: LayoutSize) -> LayoutRect { 58 let height = 59 TITLE_HEIGHT_PX + PANEL_PAD_PX * 2.0 + ITEMS.len() as f32 * (ITEM_HEIGHT_PX + ITEM_GAP_PX) 60 - ITEM_GAP_PX; 61 let width = PANEL_WIDTH_PX; 62 let max_x = (viewport.width.value() - width).max(0.0); 63 let max_y = (viewport.height.value() - height).max(0.0); 64 let x = anchor.x.value().min(max_x).max(0.0); 65 let y = anchor.y.value().min(max_y).max(0.0); 66 LayoutRect::new( 67 LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)), 68 LayoutSize::new(LayoutPx::new(width), LayoutPx::new(height)), 69 ) 70} 71 72#[must_use] 73#[allow( 74 clippy::cast_precision_loss, 75 reason = "ITEMS.len() and item indices fit in u8; f32 mantissa is fine" 76)] 77pub fn render( 78 ctx: &mut FrameCtx<'_>, 79 state: ShortcutBarState, 80 viewport: LayoutSize, 81 is_sketch: bool, 82) -> ShortcutBarOutcome { 83 let rect = panel_rect(state.anchor, viewport); 84 let theme = ctx.theme(); 85 let radius = theme.radius.sm; 86 let surface_fill = theme.colors.neutral.step(Step12::APP_BG); 87 let border = Border { 88 width: StrokeWidth::HAIRLINE, 89 color: theme.colors.neutral.step(Step12::BORDER), 90 }; 91 let elevation = theme.elevation.level3; 92 let mut paints = vec![ 93 WidgetPaint::Surface { 94 rect, 95 fill: surface_fill, 96 border: Some(border), 97 radius, 98 elevation: Some(elevation), 99 }, 100 WidgetPaint::Label { 101 rect: LayoutRect::new( 102 LayoutPos::new( 103 LayoutPx::new(rect.origin.x.value() + PANEL_PAD_PX), 104 LayoutPx::new(rect.origin.y.value() + PANEL_PAD_PX), 105 ), 106 LayoutSize::new( 107 LayoutPx::new(rect.size.width.value() - PANEL_PAD_PX * 2.0), 108 LayoutPx::new(TITLE_HEIGHT_PX), 109 ), 110 ), 111 text: LabelText::Key(s::SHORTCUT_BAR_TITLE), 112 color: theme.colors.text_primary(), 113 role: theme.typography.label, 114 }, 115 ]; 116 let root_id = WidgetId::ROOT.child(WidgetKey::new("shortcut_bar")); 117 let row_origin_y = rect.origin.y.value() + PANEL_PAD_PX + TITLE_HEIGHT_PX; 118 let activated = ITEMS 119 .iter() 120 .enumerate() 121 .fold(None, |found, (i, (action, label))| { 122 let y = row_origin_y + i as f32 * (ITEM_HEIGHT_PX + ITEM_GAP_PX); 123 let row_rect = LayoutRect::new( 124 LayoutPos::new( 125 LayoutPx::new(rect.origin.x.value() + PANEL_PAD_PX), 126 LayoutPx::new(y), 127 ), 128 LayoutSize::new( 129 LayoutPx::new(rect.size.width.value() - PANEL_PAD_PX * 2.0), 130 LayoutPx::new(ITEM_HEIGHT_PX), 131 ), 132 ); 133 let id = root_id.child_indexed(WidgetKey::new("item"), u64::from(action.get().get())); 134 let item_state = if item_enabled(*action, is_sketch) { 135 ButtonState::Idle 136 } else { 137 ButtonState::Disabled 138 }; 139 let response = show_button( 140 ctx, 141 Button::new(id, row_rect, *label, ButtonVariant::Secondary).with_state(item_state), 142 ); 143 paints.extend(response.paint); 144 found.or(response.activated.then_some(*action)) 145 }); 146 let esc = TakeKey::new(KeyCode::Named(NamedKey::Escape), ModifierMask::NONE); 147 let esc_dismiss = take_key(ctx.input, &[esc]).is_some(); 148 let outside_click = ctx.input.buttons_pressed.contains(PointerButton::Primary) 149 && ctx 150 .input 151 .pointer 152 .is_some_and(|sample| !rect.contains(sample.position)); 153 if outside_click { 154 ctx.input.buttons_pressed = ctx.input.buttons_pressed.without(PointerButton::Primary); 155 } 156 ShortcutBarOutcome { 157 paints, 158 activated, 159 dismissed: esc_dismiss || outside_click, 160 } 161} 162 163fn item_enabled(action: ActionId, is_sketch: bool) -> bool { 164 if action == ENTER_SKETCH_ACTION { 165 return !is_sketch; 166 } 167 let Some(scope) = COMMANDS 168 .iter() 169 .find(|c| c.action == action) 170 .map(|c| c.scope) 171 else { 172 return true; 173 }; 174 !matches!(scope, HotkeyScope::Sketch) || is_sketch 175}