Another project
0

Configure Feed

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

feat(app): sketch_mode pending+drag+plane, text spans, menu&property strings

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

author
Lewis
date (May 9, 2026, 9:30 AM +0300) commit 3b8aae2e parent e5d646bb change-id uonkwyso
+619 -14
+2
crates/bone-app/Cargo.toml
··· 9 9 bone-types = { workspace = true } 10 10 bone-document = { workspace = true } 11 11 bone-render = { workspace = true } 12 + bone-text = { workspace = true } 12 13 bone-ui = { workspace = true } 14 + swash = { workspace = true } 13 15 14 16 pollster = { workspace = true } 15 17 thiserror = { workspace = true }
+264 -2
crates/bone-app/src/chrome.rs
··· 1 - use bone_render::ChromeInstance; 1 + use bone_render::{ChromeInstance, SdfGlyphInstance}; 2 + use bone_text::{FontFace, FontWeight, ShapeRequest, ShapedLine, Shaper}; 2 3 use bone_ui::layout::LayoutRect; 3 - use bone_ui::theme::{Color, Theme}; 4 + use bone_ui::strings::StringTable; 5 + use bone_ui::text::{GlyphAtlasKey, SdfAtlas}; 6 + use bone_ui::theme::{Color, FontSize, Theme}; 4 7 use bone_ui::widgets::{PaintPrim, WidgetPaint, lower_paint}; 8 + use swash::FontRef; 9 + 10 + #[derive(Clone, Debug)] 11 + pub struct ChromeTextSpan<'a> { 12 + pub rect_px: [f32; 4], 13 + pub text: &'a str, 14 + pub color_premul_rgba: [f32; 4], 15 + pub font_size_px: f32, 16 + pub face: FontFace, 17 + pub weight: FontWeight, 18 + } 5 19 6 20 #[must_use] 7 21 pub fn paint_to_instances(theme: &Theme, paints: &[WidgetPaint]) -> Vec<ChromeInstance> { 8 22 paints 9 23 .iter() 24 + .filter(|p| !matches!(p, WidgetPaint::Label { .. })) 10 25 .map(|p| prim_to_instance(&lower_paint(theme, p))) 11 26 .collect() 12 27 } 13 28 29 + #[must_use] 30 + pub fn paint_to_text_spans<'a>( 31 + paints: &'a [WidgetPaint], 32 + strings: &'a StringTable, 33 + ) -> Vec<ChromeTextSpan<'a>> { 34 + paints 35 + .iter() 36 + .filter_map(|p| match p { 37 + WidgetPaint::Label { 38 + rect, 39 + text, 40 + color, 41 + role, 42 + } => Some(ChromeTextSpan { 43 + rect_px: rect_to_xywh(*rect), 44 + text: text.resolve(strings), 45 + color_premul_rgba: color.linear_rgba_premul(), 46 + font_size_px: role.size.as_px_f32(), 47 + face: role.face, 48 + weight: role.weight, 49 + }), 50 + _ => None, 51 + }) 52 + .collect() 53 + } 54 + 55 + struct ChromeFonts<'a> { 56 + sans_font: &'a FontRef<'static>, 57 + mono_font: &'a FontRef<'static>, 58 + } 59 + 60 + impl<'a> ChromeFonts<'a> { 61 + fn for_face(&self, face: FontFace) -> &'a FontRef<'static> { 62 + match face { 63 + FontFace::Sans => self.sans_font, 64 + FontFace::Mono => self.mono_font, 65 + } 66 + } 67 + } 68 + 69 + #[must_use] 70 + pub fn build_glyph_instances( 71 + spans: &[ChromeTextSpan<'_>], 72 + atlas: &mut SdfAtlas, 73 + shaper: &mut Shaper, 74 + sans_font: &FontRef<'static>, 75 + mono_font: &FontRef<'static>, 76 + ) -> Vec<SdfGlyphInstance> { 77 + let fonts = ChromeFonts { 78 + sans_font, 79 + mono_font, 80 + }; 81 + spans.iter().fold(Vec::new(), |mut acc, item| { 82 + push_span_instances(&mut acc, item, atlas, shaper, &fonts); 83 + acc 84 + }) 85 + } 86 + 87 + fn push_span_instances( 88 + out: &mut Vec<SdfGlyphInstance>, 89 + item: &ChromeTextSpan<'_>, 90 + atlas: &mut SdfAtlas, 91 + shaper: &mut Shaper, 92 + fonts: &ChromeFonts<'_>, 93 + ) { 94 + let layout = shaper.shape( 95 + item.text, 96 + ShapeRequest { 97 + face: item.face, 98 + size_px: item.font_size_px, 99 + weight: item.weight, 100 + line_height_px: 0.0, 101 + letter_spacing_px: 0.0, 102 + max_width: None, 103 + }, 104 + ); 105 + let metrics = fonts 106 + .for_face(item.face) 107 + .metrics(&[]) 108 + .scale(item.font_size_px); 109 + let visible_advance = layout 110 + .lines 111 + .first() 112 + .map_or(0.0, ShapedLine::visible_advance_px); 113 + let [rx, ry, rw, rh] = item.rect_px; 114 + let line = LineLayout { 115 + face: item.face, 116 + target: FontSize::from_px(f64::from(item.font_size_px)), 117 + color: item.color_premul_rgba, 118 + start_x: rx + ((rw - visible_advance) * 0.5).max(0.0), 119 + baseline_y: ry + (rh + metrics.cap_height) * 0.5, 120 + }; 121 + layout 122 + .lines 123 + .iter() 124 + .flat_map(|l| l.runs.iter()) 125 + .for_each(|run| { 126 + let run_origin = run.origin_x_px; 127 + run.glyphs.iter().for_each(|g| { 128 + if let Some(instance) = build_instance(atlas, &line, g.id, run_origin + g.x_px) { 129 + out.push(instance); 130 + } 131 + }); 132 + }); 133 + } 134 + 135 + #[derive(Copy, Clone)] 136 + struct LineLayout { 137 + face: FontFace, 138 + target: FontSize, 139 + color: [f32; 4], 140 + start_x: f32, 141 + baseline_y: f32, 142 + } 143 + 144 + fn build_instance( 145 + atlas: &mut SdfAtlas, 146 + line: &LineLayout, 147 + glyph_id: bone_text::GlyphId, 148 + advance_x: f32, 149 + ) -> Option<SdfGlyphInstance> { 150 + let key = GlyphAtlasKey::new(line.face, glyph_id); 151 + let entry = match atlas.ensure(key) { 152 + Ok(e) => e, 153 + Err(e) => { 154 + tracing::warn!(error = %e, face = ?line.face, "glyph atlas ensure failed"); 155 + return None; 156 + } 157 + }; 158 + let extent = entry.display_extent(line.target); 159 + let bearing = entry.display_bearing(line.target); 160 + Some(SdfGlyphInstance { 161 + rect_xywh_px: [ 162 + line.start_x + advance_x + bearing.dx.value(), 163 + line.baseline_y - bearing.dy.value(), 164 + extent.width.value(), 165 + extent.height.value(), 166 + ], 167 + uv_min: entry.uv_min, 168 + uv_max: entry.uv_max, 169 + color_premul_rgba: line.color, 170 + }) 171 + } 172 + 14 173 fn prim_to_instance(prim: &PaintPrim) -> ChromeInstance { 15 174 let (border_color, thickness_px) = prim 16 175 .border ··· 31 190 rect.size.height.value(), 32 191 ] 33 192 } 193 + 194 + #[cfg(test)] 195 + mod tests { 196 + use super::*; 197 + use bone_ui::layout::{LayoutPos, LayoutPx, LayoutSize}; 198 + use bone_ui::strings::StringKey; 199 + use bone_ui::widgets::LabelText; 200 + 201 + fn rect(x: f32, y: f32, w: f32, h: f32) -> LayoutRect { 202 + LayoutRect::new( 203 + LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)), 204 + LayoutSize::new(LayoutPx::new(w), LayoutPx::new(h)), 205 + ) 206 + } 207 + 208 + fn label(text: LabelText, theme: &Theme) -> WidgetPaint { 209 + WidgetPaint::Label { 210 + rect: rect(10.0, 20.0, 80.0, 14.0), 211 + text, 212 + color: theme.colors.text_secondary(), 213 + role: theme.typography.caption, 214 + } 215 + } 216 + 217 + fn surface(theme: &Theme) -> WidgetPaint { 218 + WidgetPaint::Surface { 219 + rect: rect(0.0, 0.0, 100.0, 100.0), 220 + fill: theme.colors.surface(theme.elevation.level1.surface), 221 + border: None, 222 + radius: theme.radius.none, 223 + elevation: None, 224 + } 225 + } 226 + 227 + #[test] 228 + fn rect_path_drops_label_paints() { 229 + let theme = Theme::light(); 230 + let paints = vec![ 231 + surface(&theme), 232 + label(LabelText::Owned("hello".to_owned()), &theme), 233 + ]; 234 + let instances = paint_to_instances(&theme, &paints); 235 + assert_eq!(instances.len(), 1); 236 + } 237 + 238 + #[test] 239 + fn text_span_path_skips_non_label_paints() { 240 + let theme = Theme::light(); 241 + let paints = vec![ 242 + surface(&theme), 243 + label(LabelText::Owned("hello".to_owned()), &theme), 244 + ]; 245 + let spans = paint_to_text_spans(&paints, StringTable::empty()); 246 + assert_eq!(spans.len(), 1); 247 + assert_eq!(spans[0].text, "hello"); 248 + } 249 + 250 + #[test] 251 + fn text_span_resolves_string_keys_through_table() { 252 + let theme = Theme::light(); 253 + let key = StringKey::new("test.label"); 254 + let paints = vec![label(LabelText::Key(key), &theme)]; 255 + let table = StringTable::from_entries([(key, "Resolved".to_owned())]); 256 + let spans = paint_to_text_spans(&paints, &table); 257 + assert_eq!(spans.len(), 1); 258 + assert_eq!(spans[0].text, "Resolved"); 259 + } 260 + 261 + #[test] 262 + fn text_span_carries_face_and_weight_from_role() { 263 + let theme = Theme::light(); 264 + let paints = vec![label(LabelText::Owned("x".to_owned()), &theme)]; 265 + let spans = paint_to_text_spans(&paints, StringTable::empty()); 266 + assert_eq!(spans.len(), 1); 267 + assert_eq!(spans[0].face, theme.typography.caption.face); 268 + assert_eq!(spans[0].weight, theme.typography.caption.weight); 269 + assert!((spans[0].font_size_px - theme.typography.caption.size.as_px_f32()).abs() < 1e-6); 270 + } 271 + 272 + #[test] 273 + fn build_glyph_instances_emits_one_quad_per_visible_glyph() { 274 + let theme = Theme::light(); 275 + let paints = vec![label(LabelText::Owned("Hi".to_owned()), &theme)]; 276 + let spans = paint_to_text_spans(&paints, StringTable::empty()); 277 + let mut atlas = SdfAtlas::new(bone_ui::SdfAtlasParams::STANDARD); 278 + let mut shaper = Shaper::new(); 279 + let sans_font = bone_text::load_font(FontFace::Sans); 280 + let mono_font = bone_text::load_font(FontFace::Mono); 281 + let instances = 282 + build_glyph_instances(&spans, &mut atlas, &mut shaper, &sans_font, &mono_font); 283 + assert_eq!(instances.len(), 2, "Hi has two glyphs"); 284 + } 285 + 286 + #[test] 287 + fn build_glyph_instances_is_empty_for_no_spans() { 288 + let mut atlas = SdfAtlas::new(bone_ui::SdfAtlasParams::STANDARD); 289 + let mut shaper = Shaper::new(); 290 + let sans_font = bone_text::load_font(FontFace::Sans); 291 + let mono_font = bone_text::load_font(FontFace::Mono); 292 + let instances = build_glyph_instances(&[], &mut atlas, &mut shaper, &sans_font, &mono_font); 293 + assert!(instances.is_empty()); 294 + } 295 + }
+234 -12
crates/bone-app/src/sketch_mode.rs
··· 1 - use bone_types::SketchId; 1 + use core::num::NonZeroU32; 2 + 3 + use bone_types::{Point2, Point3, SketchEntityId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3}; 4 + use bone_ui::hotkey::ActionId; 2 5 3 6 #[derive(Copy, Clone, Debug, PartialEq, Eq)] 4 7 pub enum SketchTool { ··· 34 37 ]; 35 38 } 36 39 37 - #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 40 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 41 + pub enum Plane { 42 + Xy, 43 + Yz, 44 + Zx, 45 + } 46 + 47 + impl Plane { 48 + #[must_use] 49 + pub fn basis(self) -> SketchPlaneBasis { 50 + let (x, y) = match self { 51 + Self::Xy => (UnitVec3::x_axis(), UnitVec3::y_axis()), 52 + Self::Yz => (UnitVec3::y_axis(), UnitVec3::z_axis()), 53 + Self::Zx => (UnitVec3::z_axis(), UnitVec3::x_axis()), 54 + }; 55 + let Ok(basis) = SketchPlaneBasis::new(Point3::origin(), x, y, Tolerance::new(1e-9)) else { 56 + unreachable!("canonical principal-plane axes are orthonormal"); 57 + }; 58 + basis 59 + } 60 + } 61 + 62 + const fn action_id(value: u32) -> ActionId { 63 + let Some(nz) = NonZeroU32::new(value) else { 64 + panic!("ActionId must be non-zero"); 65 + }; 66 + ActionId::new(nz) 67 + } 68 + 69 + pub const EXIT_SKETCH_ACTION: ActionId = action_id(1); 70 + pub const UNDO_ACTION: ActionId = action_id(2); 71 + pub const REDO_ACTION: ActionId = action_id(3); 72 + 73 + #[derive(Copy, Clone, Debug, PartialEq)] 74 + pub enum Pending { 75 + Position(Point2), 76 + Endpoint(SketchEntityId), 77 + } 78 + 79 + #[derive(Copy, Clone, Debug, Default, PartialEq)] 38 80 pub struct SketchSession { 39 81 pub tool: Option<SketchTool>, 82 + pub pending: Option<Pending>, 83 + pub drag: Option<SketchEntityId>, 40 84 } 41 85 42 - #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 86 + #[derive(Copy, Clone, Debug, Default, PartialEq)] 43 87 pub enum Mode { 44 88 #[default] 45 89 Idle, ··· 54 98 pub const fn enter_sketch(sketch_id: SketchId) -> Self { 55 99 Self::Sketch { 56 100 sketch_id, 57 - session: SketchSession { tool: None }, 101 + session: SketchSession { 102 + tool: None, 103 + pending: None, 104 + drag: None, 105 + }, 58 106 } 59 107 } 60 108 61 109 #[must_use] 62 110 pub fn arm_tool(self, kind: SketchTool) -> Self { 63 111 match self { 64 - Self::Sketch { 112 + Self::Sketch { sketch_id, .. } => Self::Sketch { 113 + sketch_id, 114 + session: SketchSession { 115 + tool: Some(kind), 116 + pending: None, 117 + drag: None, 118 + }, 119 + }, 120 + Self::Idle => Self::Idle, 121 + } 122 + } 123 + 124 + #[must_use] 125 + pub const fn disarm_tool(self) -> Self { 126 + match self { 127 + Self::Sketch { sketch_id, .. } => Self::Sketch { 128 + sketch_id, 129 + session: SketchSession { 130 + tool: None, 131 + pending: None, 132 + drag: None, 133 + }, 134 + }, 135 + Self::Idle => Self::Idle, 136 + } 137 + } 138 + 139 + #[must_use] 140 + pub const fn clear_pending(self) -> Self { 141 + match self { 142 + Self::Sketch { sketch_id, session } => Self::Sketch { 143 + sketch_id, 144 + session: SketchSession { 145 + tool: session.tool, 146 + pending: None, 147 + drag: session.drag, 148 + }, 149 + }, 150 + Self::Idle => Self::Idle, 151 + } 152 + } 153 + 154 + #[must_use] 155 + pub const fn start_drag(self, entity: SketchEntityId) -> Self { 156 + match self { 157 + Self::Sketch { sketch_id, session } => Self::Sketch { 65 158 sketch_id, 66 - mut session, 67 - } => { 68 - session.tool = Some(kind); 69 - Self::Sketch { sketch_id, session } 70 - } 159 + session: SketchSession { 160 + tool: session.tool, 161 + pending: session.pending, 162 + drag: Some(entity), 163 + }, 164 + }, 71 165 Self::Idle => Self::Idle, 72 166 } 73 167 } 168 + 169 + #[must_use] 170 + pub const fn end_drag(self) -> Self { 171 + match self { 172 + Self::Sketch { sketch_id, session } => Self::Sketch { 173 + sketch_id, 174 + session: SketchSession { 175 + tool: session.tool, 176 + pending: session.pending, 177 + drag: None, 178 + }, 179 + }, 180 + Self::Idle => Self::Idle, 181 + } 182 + } 183 + 184 + #[must_use] 185 + pub const fn is_sketch(&self) -> bool { 186 + matches!(self, Self::Sketch { .. }) 187 + } 74 188 } 75 189 76 190 #[cfg(test)] 77 191 mod tests { 78 - use super::{Mode, SketchSession, SketchTool}; 79 - use bone_types::SketchId; 192 + use super::{Mode, Pending, Plane, SketchSession, SketchTool}; 193 + use bone_types::{Point2, SketchEntityId, SketchId}; 80 194 81 195 #[test] 82 196 fn arm_tool_in_sketch_records_kind() { ··· 93 207 } 94 208 95 209 #[test] 210 + fn arm_tool_clears_pending() { 211 + let session = SketchSession { 212 + tool: Some(SketchTool::Line), 213 + pending: Some(Pending::Position(Point2::from_mm(1.0, 2.0))), 214 + drag: None, 215 + }; 216 + let mode = Mode::Sketch { 217 + sketch_id: SketchId::default(), 218 + session, 219 + } 220 + .arm_tool(SketchTool::Point); 221 + let Mode::Sketch { session, .. } = mode else { 222 + panic!("expected sketch mode"); 223 + }; 224 + assert_eq!(session.tool, Some(SketchTool::Point)); 225 + assert_eq!(session.pending, None); 226 + } 227 + 228 + #[test] 229 + fn clear_pending_in_sketch_drops_pending_keeps_tool() { 230 + let session = SketchSession { 231 + tool: Some(SketchTool::Line), 232 + pending: Some(Pending::Position(Point2::from_mm(3.0, 4.0))), 233 + drag: None, 234 + }; 235 + let mode = Mode::Sketch { 236 + sketch_id: SketchId::default(), 237 + session, 238 + } 239 + .clear_pending(); 240 + let Mode::Sketch { session, .. } = mode else { 241 + panic!("expected sketch mode"); 242 + }; 243 + assert_eq!(session.tool, Some(SketchTool::Line)); 244 + assert_eq!(session.pending, None); 245 + } 246 + 247 + #[test] 248 + fn clear_pending_in_idle_is_noop() { 249 + assert_eq!(Mode::Idle.clear_pending(), Mode::Idle); 250 + } 251 + 252 + #[test] 253 + fn start_drag_records_entity_keeps_tool_and_pending() { 254 + let entity = SketchEntityId::default(); 255 + let mode = Mode::Sketch { 256 + sketch_id: SketchId::default(), 257 + session: SketchSession { 258 + tool: Some(SketchTool::Line), 259 + pending: Some(Pending::Position(Point2::from_mm(0.0, 0.0))), 260 + drag: None, 261 + }, 262 + } 263 + .start_drag(entity); 264 + let Mode::Sketch { session, .. } = mode else { 265 + panic!("expected sketch mode"); 266 + }; 267 + assert_eq!(session.drag, Some(entity)); 268 + assert_eq!(session.tool, Some(SketchTool::Line)); 269 + assert!(session.pending.is_some()); 270 + } 271 + 272 + #[test] 273 + fn end_drag_clears_drag_only() { 274 + let entity = SketchEntityId::default(); 275 + let mode = Mode::Sketch { 276 + sketch_id: SketchId::default(), 277 + session: SketchSession { 278 + tool: Some(SketchTool::Line), 279 + pending: None, 280 + drag: Some(entity), 281 + }, 282 + } 283 + .end_drag(); 284 + let Mode::Sketch { session, .. } = mode else { 285 + panic!("expected sketch mode"); 286 + }; 287 + assert_eq!(session.drag, None); 288 + assert_eq!(session.tool, Some(SketchTool::Line)); 289 + } 290 + 291 + #[test] 292 + fn start_drag_in_idle_is_noop() { 293 + assert_eq!( 294 + Mode::Idle.start_drag(SketchEntityId::default()), 295 + Mode::Idle 296 + ); 297 + } 298 + 299 + #[test] 96 300 fn default_mode_is_idle() { 97 301 assert_eq!(Mode::default(), Mode::Idle); 98 302 } ··· 110 314 #[test] 111 315 fn fresh_session_has_no_tool() { 112 316 assert_eq!(SketchSession::default().tool, None); 317 + assert_eq!(SketchSession::default().pending, None); 318 + } 319 + 320 + #[test] 321 + fn is_sketch_distinguishes_states() { 322 + assert!(!Mode::Idle.is_sketch()); 323 + assert!(Mode::enter_sketch(SketchId::default()).is_sketch()); 324 + } 325 + 326 + #[test] 327 + fn principal_planes_are_pairwise_distinct() { 328 + let planes = [Plane::Xy, Plane::Yz, Plane::Zx]; 329 + let bases: Vec<_> = planes.iter().copied().map(Plane::basis).collect(); 330 + bases.iter().enumerate().for_each(|(i, a)| { 331 + bases.iter().enumerate().skip(i + 1).for_each(|(j, b)| { 332 + assert_ne!(a, b, "{:?} == {:?}", planes[i], planes[j]); 333 + }); 334 + }); 113 335 } 114 336 }
+119
crates/bone-app/src/strings.rs
··· 7 7 pub const RIBBON_GROUP_ENTITIES: StringKey = StringKey::new("shell.ribbon.group.entities"); 8 8 pub const RIBBON_GROUP_RELATIONS: StringKey = StringKey::new("shell.ribbon.group.relations"); 9 9 pub const RIBBON_GROUP_DIMENSIONS: StringKey = StringKey::new("shell.ribbon.group.dimensions"); 10 + pub const RIBBON_GROUP_EXIT: StringKey = StringKey::new("shell.ribbon.group.exit"); 11 + pub const TOOL_EXIT_SKETCH: StringKey = StringKey::new("tool.exit_sketch"); 10 12 11 13 pub const TOOL_POINT: StringKey = StringKey::new("tool.point"); 12 14 pub const TOOL_LINE: StringKey = StringKey::new("tool.line"); ··· 48 50 pub const STATUS_READY: StringKey = StringKey::new("status.ready"); 49 51 pub const STATUS_SKETCH_ACTIVE: StringKey = StringKey::new("status.sketch_active"); 50 52 53 + pub const MENU_BAR_LABEL: StringKey = StringKey::new("shell.menu_bar"); 54 + pub const MENU_FILE: StringKey = StringKey::new("menu.file"); 55 + pub const MENU_EDIT: StringKey = StringKey::new("menu.edit"); 56 + pub const MENU_VIEW: StringKey = StringKey::new("menu.view"); 57 + pub const MENU_INSERT: StringKey = StringKey::new("menu.insert"); 58 + pub const MENU_TOOLS: StringKey = StringKey::new("menu.tools"); 59 + pub const MENU_WINDOW: StringKey = StringKey::new("menu.window"); 60 + pub const MENU_HELP: StringKey = StringKey::new("menu.help"); 61 + pub const MENU_FILE_NEW: StringKey = StringKey::new("menu.file.new"); 62 + pub const MENU_FILE_OPEN: StringKey = StringKey::new("menu.file.open"); 63 + pub const MENU_FILE_SAVE: StringKey = StringKey::new("menu.file.save"); 64 + pub const MENU_FILE_QUIT: StringKey = StringKey::new("menu.file.quit"); 65 + pub const MENU_EDIT_UNDO: StringKey = StringKey::new("menu.edit.undo"); 66 + pub const MENU_EDIT_REDO: StringKey = StringKey::new("menu.edit.redo"); 67 + pub const MENU_VIEW_ZOOM_FIT: StringKey = StringKey::new("menu.view.zoom_fit"); 68 + pub const MENU_PLACEHOLDER_COMING_SOON: StringKey = StringKey::new("menu.placeholder.coming_soon"); 69 + pub const SHORTCUT_QUIT: StringKey = StringKey::new("shortcut.quit"); 70 + pub const SHORTCUT_UNDO: StringKey = StringKey::new("shortcut.undo"); 71 + pub const SHORTCUT_REDO: StringKey = StringKey::new("shortcut.redo"); 72 + pub const SHORTCUT_ZOOM_FIT: StringKey = StringKey::new("shortcut.zoom_fit"); 73 + 74 + pub const PROPERTY_PANE_NO_SELECTION: StringKey = StringKey::new("property.no_selection"); 75 + pub const PROPERTY_ROW_KIND: StringKey = StringKey::new("property.row.kind"); 76 + pub const PROPERTY_ROW_X: StringKey = StringKey::new("property.row.x"); 77 + pub const PROPERTY_ROW_Y: StringKey = StringKey::new("property.row.y"); 78 + pub const PROPERTY_ROW_FROM: StringKey = StringKey::new("property.row.from"); 79 + pub const PROPERTY_ROW_TO: StringKey = StringKey::new("property.row.to"); 80 + pub const PROPERTY_ROW_CENTER: StringKey = StringKey::new("property.row.center"); 81 + pub const PROPERTY_ROW_START: StringKey = StringKey::new("property.row.start"); 82 + pub const PROPERTY_ROW_END: StringKey = StringKey::new("property.row.end"); 83 + pub const PROPERTY_ROW_RADIUS: StringKey = StringKey::new("property.row.radius"); 84 + pub const PROPERTY_ROW_CONSTRUCTION: StringKey = StringKey::new("property.row.construction"); 85 + pub const PROPERTY_KIND_POINT: StringKey = StringKey::new("property.kind.point"); 86 + pub const PROPERTY_KIND_LINE: StringKey = StringKey::new("property.kind.line"); 87 + pub const PROPERTY_KIND_ARC: StringKey = StringKey::new("property.kind.arc"); 88 + pub const PROPERTY_KIND_CIRCLE: StringKey = StringKey::new("property.kind.circle"); 89 + pub const PROPERTY_VALUE_YES: StringKey = StringKey::new("property.value.yes"); 90 + pub const PROPERTY_VALUE_NO: StringKey = StringKey::new("property.value.no"); 91 + 51 92 #[must_use] 52 93 pub fn make_strings(locale: Locale) -> StringTable { 53 94 let mut table = StringTable::for_locale(locale); ··· 68 109 (RIBBON_GROUP_ENTITIES, "Entities"), 69 110 (RIBBON_GROUP_RELATIONS, "Relations"), 70 111 (RIBBON_GROUP_DIMENSIONS, "Dimensions"), 112 + (RIBBON_GROUP_EXIT, "Exit"), 113 + (TOOL_EXIT_SKETCH, "Exit Sketch"), 71 114 (TOOL_POINT, "Point"), 72 115 (TOOL_LINE, "Line"), 73 116 (TOOL_CENTERPOINT_ARC, "Centerpoint Arc"), ··· 106 149 (STATUS_BAR_LABEL, "Status Bar"), 107 150 (STATUS_READY, "Ready"), 108 151 (STATUS_SKETCH_ACTIVE, "Editing Sketch"), 152 + (MENU_BAR_LABEL, "Menu Bar"), 153 + (MENU_FILE, "File"), 154 + (MENU_EDIT, "Edit"), 155 + (MENU_VIEW, "View"), 156 + (MENU_INSERT, "Insert"), 157 + (MENU_TOOLS, "Tools"), 158 + (MENU_WINDOW, "Window"), 159 + (MENU_HELP, "Help"), 160 + (MENU_FILE_NEW, "New"), 161 + (MENU_FILE_OPEN, "Open"), 162 + (MENU_FILE_SAVE, "Save"), 163 + (MENU_FILE_QUIT, "Quit"), 164 + (MENU_EDIT_UNDO, "Undo"), 165 + (MENU_EDIT_REDO, "Redo"), 166 + (MENU_VIEW_ZOOM_FIT, "Zoom to Fit"), 167 + (MENU_PLACEHOLDER_COMING_SOON, "Coming Soon"), 168 + (SHORTCUT_QUIT, "Ctrl+Q"), 169 + (SHORTCUT_UNDO, "Ctrl+Z"), 170 + (SHORTCUT_REDO, "Ctrl+Y"), 171 + (SHORTCUT_ZOOM_FIT, "F"), 172 + (PROPERTY_PANE_NO_SELECTION, "Nothing selected"), 173 + (PROPERTY_ROW_KIND, "Type"), 174 + (PROPERTY_ROW_X, "X"), 175 + (PROPERTY_ROW_Y, "Y"), 176 + (PROPERTY_ROW_FROM, "From"), 177 + (PROPERTY_ROW_TO, "To"), 178 + (PROPERTY_ROW_CENTER, "Center"), 179 + (PROPERTY_ROW_START, "Start"), 180 + (PROPERTY_ROW_END, "End"), 181 + (PROPERTY_ROW_RADIUS, "Radius"), 182 + (PROPERTY_ROW_CONSTRUCTION, "Construction"), 183 + (PROPERTY_KIND_POINT, "Point"), 184 + (PROPERTY_KIND_LINE, "Line"), 185 + (PROPERTY_KIND_ARC, "Arc"), 186 + (PROPERTY_KIND_CIRCLE, "Circle"), 187 + (PROPERTY_VALUE_YES, "Yes"), 188 + (PROPERTY_VALUE_NO, "No"), 109 189 ]; 110 190 111 191 const AR_XB: &[(StringKey, &str)] = &[ ··· 115 195 (RIBBON_GROUP_ENTITIES, "[!! Entîtîes !!]"), 116 196 (RIBBON_GROUP_RELATIONS, "[!! Relâtions !!]"), 117 197 (RIBBON_GROUP_DIMENSIONS, "[!! Dîmensions !!]"), 198 + (RIBBON_GROUP_EXIT, "[!! Êxit !!]"), 199 + (TOOL_EXIT_SKETCH, "[!! Êxit Skêtch !!]"), 118 200 (TOOL_POINT, "[!! Pôint !!]"), 119 201 (TOOL_LINE, "[!! Lîne !!]"), 120 202 (TOOL_CENTERPOINT_ARC, "[!! Cêntrepoint Arc !!]"), ··· 153 235 (STATUS_BAR_LABEL, "[!! Statûs Bar !!]"), 154 236 (STATUS_READY, "[!! Réady !!]"), 155 237 (STATUS_SKETCH_ACTIVE, "[!! Edîting Skêtch !!]"), 238 + (MENU_BAR_LABEL, "[!! Mênu Bâr !!]"), 239 + (MENU_FILE, "[!! Fîle !!]"), 240 + (MENU_EDIT, "[!! Édit !!]"), 241 + (MENU_VIEW, "[!! Vîew !!]"), 242 + (MENU_INSERT, "[!! Insêrt !!]"), 243 + (MENU_TOOLS, "[!! Tôols !!]"), 244 + (MENU_WINDOW, "[!! Wîndow !!]"), 245 + (MENU_HELP, "[!! Hêlp !!]"), 246 + (MENU_FILE_NEW, "[!! Néw !!]"), 247 + (MENU_FILE_OPEN, "[!! Ôpen !!]"), 248 + (MENU_FILE_SAVE, "[!! Sâve !!]"), 249 + (MENU_FILE_QUIT, "[!! Quît !!]"), 250 + (MENU_EDIT_UNDO, "[!! Undô !!]"), 251 + (MENU_EDIT_REDO, "[!! Redô !!]"), 252 + (MENU_VIEW_ZOOM_FIT, "[!! Zôom to Fît !!]"), 253 + (MENU_PLACEHOLDER_COMING_SOON, "[!! Côming Sôon !!]"), 254 + (SHORTCUT_QUIT, "Ctrl+Q"), 255 + (SHORTCUT_UNDO, "Ctrl+Z"), 256 + (SHORTCUT_REDO, "Ctrl+Y"), 257 + (SHORTCUT_ZOOM_FIT, "F"), 258 + (PROPERTY_PANE_NO_SELECTION, "[!! Nôthing sêlected !!]"), 259 + (PROPERTY_ROW_KIND, "[!! Týpe !!]"), 260 + (PROPERTY_ROW_X, "X"), 261 + (PROPERTY_ROW_Y, "Y"), 262 + (PROPERTY_ROW_FROM, "[!! Frôm !!]"), 263 + (PROPERTY_ROW_TO, "[!! Tô !!]"), 264 + (PROPERTY_ROW_CENTER, "[!! Cênter !!]"), 265 + (PROPERTY_ROW_START, "[!! Stârt !!]"), 266 + (PROPERTY_ROW_END, "[!! Énd !!]"), 267 + (PROPERTY_ROW_RADIUS, "[!! Râdius !!]"), 268 + (PROPERTY_ROW_CONSTRUCTION, "[!! Constrûction !!]"), 269 + (PROPERTY_KIND_POINT, "[!! Pôint !!]"), 270 + (PROPERTY_KIND_LINE, "[!! Lîne !!]"), 271 + (PROPERTY_KIND_ARC, "[!! Ârc !!]"), 272 + (PROPERTY_KIND_CIRCLE, "[!! Cîrcle !!]"), 273 + (PROPERTY_VALUE_YES, "[!! Yés !!]"), 274 + (PROPERTY_VALUE_NO, "[!! Nô !!]"), 156 275 ];