1use crate::popup::PPF_FOCUS;
4use crate::{
5 Sp, Ui,
6 consts::*,
7 keys::{IC_MORE, IC_OPEN_IN_NEW_WINDOW, VK_DOWN, VK_F4},
8};
9use alloc::vec::Vec;
10
11#[derive(Clone, Copy, Debug)]
14pub struct RibbonSample {
15 pub font: u32,
16 pub size: i32,
17 pub color: u32,
18 pub highlight: Option<u32>,
19 pub baseline_shift: i32,
20 pub underline: bool,
21 pub strike: bool,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct RibbonMetrics {
28 pub content_height: i32,
29 pub caption_height: i32,
30 pub group_gap: i32,
31 pub content_gap: i32,
32 pub inset_x: i32,
34 pub inset_y: i32,
35 pub collapse_groups: bool,
36}
37impl Default for RibbonMetrics {
38 fn default() -> Self {
39 Self {
40 content_height: 66,
41 caption_height: 18,
42 group_gap: 8,
43 content_gap: 2,
44 inset_x: 0,
45 inset_y: 0,
46 collapse_groups: true,
47 }
48 }
49}
50
51#[derive(Clone, Copy)]
53pub struct RibbonGroup<'a> {
54 pub id: u32,
56 pub label: &'a str,
58 pub width: i32,
60 pub compact_width: i32,
62}
63impl<'a> RibbonGroup<'a> {
64 pub const fn new(id: u32, label: &'a str, width: i32, compact_width: i32) -> Self {
66 Self {
67 id,
68 label,
69 width,
70 compact_width,
71 }
72 }
73}
74
75#[derive(Clone, Copy, PartialEq, Eq)]
77#[non_exhaustive]
78pub enum RibbonGroupMode {
79 Inline(i32),
81 Flyout(i32),
83}
84impl RibbonGroupMode {
85 pub fn width(self) -> i32 {
87 match self {
88 Self::Inline(w) | Self::Flyout(w) => w,
89 }
90 }
91}
92
93pub struct RibbonLayout {
95 widths: Vec<i32>,
96 collapsed: Vec<bool>,
97}
98impl RibbonLayout {
99 pub fn new(available_width: i32, groups: &[RibbonGroup<'_>]) -> Self {
102 Self::with_metrics(available_width, groups, RibbonMetrics::default())
103 }
104 pub fn with_metrics(
106 available_width: i32,
107 groups: &[RibbonGroup<'_>],
108 metrics: RibbonMetrics,
109 ) -> Self {
110 let mut widths: Vec<i32> = groups.iter().map(|g| g.width.max(48)).collect();
111 let mut collapsed = alloc::vec![false; groups.len()];
112 if !metrics.collapse_groups {
113 return Self { widths, collapsed };
114 }
115 let gaps = groups.len().saturating_sub(1) as i64 * i64::from(metrics.group_gap.max(0));
116 let mut total = widths.iter().map(|&w| i64::from(w)).sum::<i64>() + gaps;
117 for (i, g) in groups.iter().enumerate().rev() {
118 if total <= i64::from(available_width) {
119 break;
120 }
121 let smaller = g.compact_width.clamp(48, widths[i]);
122 total -= i64::from(widths[i] - smaller);
123 widths[i] = smaller;
124 }
125 for i in (0..groups.len()).rev() {
126 if total <= i64::from(available_width) {
127 break;
128 }
129 let smaller = widths[i].min(96);
130 total -= i64::from(widths[i] - smaller);
131 widths[i] = smaller;
132 collapsed[i] = true;
133 }
134 Self { widths, collapsed }
135 }
136 pub fn is_collapsed(&self, index: usize) -> bool {
138 self.collapsed.get(index).copied().unwrap_or(false)
139 }
140 pub fn group_width(&self, index: usize) -> Option<i32> {
142 self.widths.get(index).copied()
143 }
144}
145
146impl Ui {
147 #[cfg_attr(jfgui_dev, track_caller)]
150 pub fn ribbon_begin(&mut self, id: u32, groups: &[RibbonGroup<'_>]) -> RibbonLayout {
151 let _src = self.src_scope();
152 let m = self.style.ribbon_metrics();
153 let py = if m.inset_y > 0 {
154 m.inset_y as u32
155 } else {
156 self.style.ribbon_padding()
157 };
158 self.next_size(-1, self.style.ribbon_height());
159 self.scroll_begin(
160 id,
161 SC_H | BOX_H | BOX_LAYER | pad(py) | if m.collapse_groups { 0 } else { SC_NOBAR },
162 );
163 if m.inset_x > py as i32 {
164 self.spacer(m.inset_x - py as i32);
165 }
166 let available = self.px2dip(self.box_rect()[2]);
167 RibbonLayout::with_metrics(available, groups, m)
168 }
169
170 #[cfg_attr(jfgui_dev, track_caller)]
173 pub fn ribbon_group_begin(
174 &mut self,
175 layout: &RibbonLayout,
176 index: usize,
177 group: &RibbonGroup<'_>,
178 ) -> Option<RibbonGroupMode> {
179 let _src = self.src_scope();
180 let width = layout.group_width(index)?;
181 let m = self.style.ribbon_metrics();
182 let gap = if m.collapse_groups { 4 } else { 0 };
183 let height = m.content_height + m.caption_height + gap;
184 if index != 0 {
185 self.spacer(m.group_gap);
186 }
187 if layout.is_collapsed(index) {
188 self.next_size(width, height);
189 self.box_begin(group.id, BOX_V | BOX_FIXED, gap);
190 self.next_size(-1, m.content_height);
191 if self.ribbon_button(group.id + 1, group.label, IC_MORE, ST_LARGE) & EV_ACT != 0 {
192 self.popup_open(group.id + 2);
193 }
194 let anchor = self.last_rect();
195 self.next_size(-1, m.caption_height);
196 self.text(
197 TS_CAPTION | TX_SECONDARY | TX_CENTER | TX_ELLIPSIS,
198 crate::i18n::tr("Weitere Befehle"),
199 );
200 self.box_end();
201 self.wp.ctx.last_rect = anchor;
202 if !self.flyout_begin(group.id + 2, 0) {
203 self.flyout_end();
204 return None;
205 }
206 self.next_size(group.width.max(48), m.content_height);
207 self.box_begin(group.id + 3, BOX_V | BOX_FIXED, m.content_gap);
208 Some(RibbonGroupMode::Flyout(group.width.max(48)))
209 } else {
210 if self.popup_is_open(group.id + 2) {
213 self.popup_close();
214 }
215 self.next_size(width, height);
216 self.box_begin(group.id, BOX_V | BOX_FIXED, gap);
217 let r = self.last_rect();
218 let c = self.color(T_DIVIDER);
219 self.draw_color(c);
220 if m.collapse_groups || index + 1 < layout.widths.len() {
221 self.draw_rect(
222 r[0] + r[2] + self.dp((m.group_gap - 2).max(0) / 2),
223 r[1] + self.dp(2),
224 self.hair(),
225 r[3] - self.dp(4),
226 );
227 }
228 self.next_size(-1, m.content_height);
229 self.box_begin(group.id + 3, BOX_V | BOX_FIXED, m.content_gap);
230 Some(RibbonGroupMode::Inline(width))
231 }
232 }
233
234 #[cfg_attr(jfgui_dev, track_caller)]
238 pub fn ribbon_group_end(&mut self, mode: RibbonGroupMode, label: &str) {
239 let _src = self.src_scope();
240 self.box_end();
241 match mode {
242 RibbonGroupMode::Inline(_) => {
243 self.next_size(-1, self.style.ribbon_metrics().caption_height);
244 self.text(TS_CAPTION | TX_SECONDARY | TX_CENTER | TX_ELLIPSIS, label);
245 self.box_end();
246 }
247 RibbonGroupMode::Flyout(_) => self.flyout_end(),
248 }
249 }
250
251 #[cfg_attr(jfgui_dev, track_caller)]
256 pub fn ribbon_group_end_launcher(
257 &mut self,
258 mode: RibbonGroupMode,
259 label: &str,
260 id: u32,
261 action_label: &str,
262 flags: u32,
263 ) -> u32 {
264 let _src = self.src_scope();
265 self.box_end();
266 let caption = self.style.ribbon_metrics().caption_height;
267 self.next_size(-1, caption);
268 self.box_begin(0, BOX_H, 2);
269 self.next_size(-1, caption);
270 self.text(TS_CAPTION | TX_SECONDARY | TX_CENTER | TX_ELLIPSIS, label);
271 self.next_size(caption, caption);
272 let ev = self.ribbon_button(
273 id,
274 action_label,
275 IC_OPEN_IN_NEW_WINDOW,
276 flags | ST_ICON_ONLY,
277 );
278 self.box_end();
279 match mode {
280 RibbonGroupMode::Inline(_) => self.box_end(),
281 RibbonGroupMode::Flyout(_) => self.flyout_end(),
282 }
283 ev
284 }
285
286 #[cfg_attr(jfgui_dev, track_caller)]
288 pub fn ribbon_end(&mut self) {
289 let _src = self.src_scope();
290 let id = self.box_id();
291 let m = self.style.ribbon_metrics();
292 let py = if m.inset_y > 0 {
293 m.inset_y
294 } else {
295 self.style.ribbon_padding() as i32
296 };
297 if m.inset_x > py {
298 self.spacer(m.inset_x - py);
299 }
300 self.scroll_end();
301 if !m.collapse_groups {
302 let r = self.last_rect();
303 let (extent, _) = self.scroll_extent(id);
304 let (offset, _) = self.scroll_pos(id);
305 let viewport = self.px2dip(r[2]);
306 for (part, shown, x, icon, label, target) in [
307 (
308 4,
309 offset > 0,
310 r[0],
311 crate::keys::IC_CHEV_LEFT,
312 "Vorherige Menübandbefehle",
313 (offset - 160).max(0),
314 ),
315 (
316 5,
317 offset + viewport < extent,
318 r[0] + r[2] - self.dp(18),
319 crate::keys::IC_CHEV_RIGHT,
320 "Weitere Menübandbefehle",
321 (offset + 160).min((extent - viewport).max(0)),
322 ),
323 ] {
324 if !shown {
325 continue;
326 }
327 self.draw_color(self.color(T_LAYER));
328 self.draw_rect(x, r[1], self.dp(18), r[3]);
329 self.next_rect_px(x, r[1], self.dp(18), r[3]);
330 if self.ribbon_button(
331 id.wrapping_mul(8).wrapping_add(part) | 0x8000_0000,
332 label,
333 icon,
334 ST_ICON_ONLY,
335 ) & EV_ACT
336 != 0
337 {
338 self.scroll_to(id, target, 0);
339 }
340 }
341 self.wp.ctx.last_rect = r;
342 }
343 }
344
345 pub fn ribbon_button_width(&mut self, label: &str, icon: u32, flags: u32) -> i32 {
349 let saved = self.text.get_font();
350 let mut sp = Sp {
351 flags: flags | ST_SUBTLE | ST_COMPACT,
352 icon,
353 label: Some(label),
354 ..Sp::default()
355 };
356 let st = self.style;
357 let (width, _) = st.button_size(self, &mut sp);
358 self.text.set_font(saved.0, saved.1);
359 let dpi = self.dpi().max(1) as i64;
361 ((width.max(0) as i64 * 96 + dpi - 1) / dpi).min(i32::MAX as i64) as i32
362 }
363
364 #[cfg_attr(jfgui_dev, track_caller)]
367 pub fn ribbon_button(&mut self, id: u32, label: &str, icon: u32, flags: u32) -> u32 {
368 let _src = self.src_scope();
369 let flags = flags | self.take_flags() | ST_SUBTLE | ST_COMPACT;
370 let st = self.style;
371 let mut sp = Sp {
372 flags,
373 icon,
374 label: Some(label),
375 ..Sp::default()
376 };
377 let (w, h) = st.button_size(self, &mut sp);
378 let r = self.place(w, h);
379 let ev = self.behave(
380 id,
381 HB_FOCUS
382 | if flags & ST_DISABLED != 0 {
383 HB_DISABLED
384 } else {
385 0
386 },
387 );
388 self.sp_placed(&mut sp, ev);
389 self.draw_clip(r[0], r[1], r[2], r[3]);
390 st.button(self, &sp);
391 self.draw_unclip();
392 self.focus_ring_draw(ev);
393 if flags & ST_CHECKED != 0 {
394 self.wp.ctx.last_state |= SS_CHECKED as u32;
395 }
396 self.sem(K_BUTTON, id, Some(label), 0);
397 self.tooltip(label);
398 ev
399 }
400
401 #[cfg_attr(jfgui_dev, track_caller)]
405 pub fn ribbon_large_button(&mut self, id: u32, label: &str, icon: u32, flags: u32) -> u32 {
406 let _src = self.src_scope();
407 let flags = flags | self.take_flags();
408 self.ribbon_button(id, label, icon, (flags | ST_LARGE) & !ST_ICON_ONLY)
409 }
410
411 #[cfg_attr(jfgui_dev, track_caller)]
414 pub fn ribbon_menu_button(&mut self, id: u32, label: &str, icon: u32, flags: u32) -> u32 {
415 let _src = self.src_scope();
416 let flags = flags | self.take_flags();
417 let ev = self.ribbon_button(id, label, icon, flags | ST_MENU);
418 if flags & ST_DISABLED == 0 && (ev & EV_ACT != 0 || ev & EV_FOCUS != 0 && self.open_key()) {
419 self.popup_open(id);
420 if self.wp.ctx.focus_visible {
421 self.pp_flag(id, PPF_FOCUS);
422 }
423 }
424 if self.popup_is_open(id) {
425 self.wp.ctx.last_state |= SS_OPEN as u32;
426 if let Some(semantic) = self.sems.last_mut() {
427 if semantic.id == id {
428 semantic.state |= SS_OPEN;
429 }
430 }
431 }
432 ev | if self.popup_is_open(id) { EV_OPEN } else { 0 }
433 }
434
435 #[cfg_attr(jfgui_dev, track_caller)]
439 pub fn ribbon_stacked_split_button(
440 &mut self,
441 id: u32,
442 menu_id: u32,
443 label: &str,
444 menu_label: &str,
445 icon: u32,
446 flags: u32,
447 ) -> u32 {
448 let _src = self.src_scope();
449 let flags = flags | self.take_flags() | ST_LARGE | ST_COMPACT | ST_SUBTLE | ST_MENU;
450 let st = self.style;
451 let mut sp = Sp {
452 flags,
453 icon,
454 label: Some(label),
455 ..Sp::default()
456 };
457 let (w, h) = st.button_size(self, &mut sp);
458 let whole = self.place(w, h);
459 let top = self.dp(40).min(whole[3]);
460 let hit_flags = HB_FOCUS
461 | if flags & ST_DISABLED != 0 {
462 HB_DISABLED
463 } else {
464 0
465 };
466 self.wp.ctx.last_rect = [whole[0], whole[1], whole[2], top];
467 let ev = self.behave(id, hit_flags);
468 self.sp_placed(&mut sp, ev);
469 self.sem(K_BUTTON, id, Some(label), 0);
470 self.focus_ring_draw(ev);
471 self.tooltip(label);
472 self.wp.ctx.last_rect = [whole[0], whole[1] + top, whole[2], whole[3] - top];
473 let menu = self.behave(menu_id, hit_flags);
474 let keyboard = (ev | menu) & EV_FOCUS != 0 && self.open_key();
475 if flags & ST_DISABLED == 0 && (menu & EV_ACT != 0 || keyboard) {
476 if keyboard {
477 self.focus(menu_id);
478 }
479 self.popup_open(menu_id);
480 if self.wp.ctx.focus_visible {
481 self.pp_flag(menu_id, PPF_FOCUS);
482 }
483 }
484 if self.popup_is_open(menu_id) {
485 self.wp.ctx.last_state |= SS_OPEN as u32;
486 }
487 self.sem(K_BUTTON, menu_id, Some(menu_label), 0);
488 self.focus_ring_draw(menu);
489 self.tooltip(menu_label);
490 if menu & EV_HOVER != 0 {
491 self.sp_placed(&mut sp, menu);
492 }
493 sp.place(whole);
494 self.draw_clip(whole[0], whole[1], whole[2], whole[3]);
495 st.button(self, &sp);
496 self.draw_unclip();
497 self.wp.ctx.last_rect = whole;
498 ev | if self.popup_is_open(menu_id) {
499 EV_OPEN
500 } else {
501 0
502 }
503 }
504
505 #[cfg_attr(jfgui_dev, track_caller)]
514 pub fn ribbon_split_button(
515 &mut self,
516 id: u32,
517 menu_id: u32,
518 label: &str,
519 menu_label: &str,
520 icon: u32,
521 flags: u32,
522 ) -> u32 {
523 let _src = self.src_scope();
524 let flags = flags | self.take_flags();
525 let st = self.style;
526 let mut sp = Sp {
527 flags: (flags | ST_LARGE | ST_COMPACT | ST_SUBTLE) & !ST_ICON_ONLY,
528 icon,
529 label: Some(label),
530 ..Sp::default()
531 };
532 let (width, height) = st.button_size(self, &mut sp);
533 let arrow = self.dp(20);
534 let whole = self.place(width + arrow, height);
535 let arrow = arrow.min(whole[2].max(0));
536 let main = [whole[0], whole[1], (whole[2] - arrow).max(0), whole[3]];
537 self.next_rect_px(main[0], main[1], main[2], main[3]);
538 let ev = self.ribbon_large_button(id, label, icon, flags);
539 if ev & EV_FOCUS != 0 && flags & ST_DISABLED == 0 {
540 let key = self.key_first();
541 let unbound_f4 = key == VK_F4
542 && !self
543 .desc
544 .cmds
545 .iter()
546 .take_while(|command| command.id != 0)
547 .any(|command| command.key as u32 == VK_F4 && command.mods == 0);
548 if key == (VK_DOWN | (MOD_ALT << 16)) || unbound_f4 {
549 self.focus(menu_id);
550 }
551 }
552 self.next_rect_px(whole[0] + main[2], whole[1], arrow, whole[3]);
553 self.next_flags((flags | ST_SUBTLE | ST_COMPACT | ST_ICON_ONLY) & !ST_LARGE);
554 let menu = self.dropdown_button(menu_id, menu_label);
555 self.wp.ctx.last_rect = whole;
558 ev | (menu & EV_OPEN)
559 }
560
561 #[cfg_attr(jfgui_dev, track_caller)]
565 pub fn ribbon_gallery_begin(&mut self, id: u32) {
566 let _src = self.src_scope();
567 self.box_begin(id, BOX_H | BOX_FIXED | pad(3), 2);
568 let r = self.box_rect();
569 let inset = self.dp(3);
570 let stroke = self.color(T_LAYER_STROKE);
571 self.cmd_rrect(r[0] - inset, r[1] - inset, r[2] + 2 * inset, r[3] + 2 * inset,
572 self.dp(4), self.dp(1), self.color(T_INPUT_ACTIVE), stroke, stroke);
573 }
574
575 #[cfg_attr(jfgui_dev, track_caller)]
578 pub fn ribbon_tile(
579 &mut self,
580 id: u32,
581 label: &str,
582 preview: &str,
583 typography: u32,
584 selected: bool,
585 ) -> u32 {
586 let _src = self.src_scope();
587 self.ribbon_font_tile(id, label, preview, typography, selected, None)
588 }
589
590 #[cfg_attr(jfgui_dev, track_caller)]
593 pub fn ribbon_font_tile(&mut self, id: u32, label: &str, preview: &str, typography: u32,
594 selected: bool, face: Option<(u32, i32, u32)>) -> u32 {
595 let _src = self.src_scope();
596 self.ribbon_sample_tile(id, label, preview, typography, selected, face.map(|(font, size, color)|
597 RibbonSample { font, size, color, highlight: None, baseline_shift: 0, underline: false, strike: false }))
598 }
599 #[cfg_attr(jfgui_dev, track_caller)]
601 pub fn ribbon_sample_tile(&mut self, id: u32, label: &str, preview: &str, typography: u32,
602 selected: bool, sample: Option<RibbonSample>) -> u32 {
603 let _src = self.src_scope();
604 let flags = self.take_flags() | ST_SUBTLE | if selected { ST_CHECKED } else { 0 };
605 let r = self.place(self.dp(106), self.dp(62));
606 let ev = self.behave(
607 id,
608 HB_FOCUS
609 | if flags & ST_DISABLED != 0 {
610 HB_DISABLED
611 } else {
612 0
613 },
614 );
615 let mut sp = Sp {
616 flags,
617 ..Sp::default()
618 };
619 self.sp_placed(&mut sp, ev);
620 let st = self.style;
621 st.button(self, &sp);
622 if let Some(sample) = sample.or_else(|| st.ribbon_preview(typography).map(|(font, size, color)|
623 RibbonSample { font, size, color, highlight: None, baseline_shift: 0, underline: false, strike: false })) {
624 self.draw_color(0xffffffff);
627 self.draw_rect(
628 r[0] + self.dp(3),
629 r[1] + self.dp(4),
630 (r[2] - self.dp(6)).max(0),
631 self.dp(32).min((r[3] - self.dp(4)).max(0)),
632 );
633 self.draw_clip(r[0] + self.dp(3), r[1], (r[2] - self.dp(6)).max(0), r[3]);
634 self.text.set_font(sample.font, self.dp_fx(sample.size));
635 let asc = self.text.ascent();
636 let desc = self.text.descent();
637 let x = r[0] + self.dp(4);
638 let baseline = r[1] + self.dp(4) + (self.dp(34) - asc - desc) / 2 + asc
639 + self.dp_fx(sample.baseline_shift) / 256;
640 let width = self.text.width(preview);
641 if let Some(color) = sample.highlight {
642 self.draw_color(color);
643 self.draw_rect(x, baseline - asc, width, asc + desc);
644 }
645 self.emit_text(
646 x,
647 baseline,
648 preview,
649 sample.color,
650 );
651 self.draw_color(sample.color);
652 let thickness = (self.dp_fx(sample.size) / 256 / 16).max(1);
653 if sample.underline { self.draw_rect(x, baseline + thickness, width, thickness); }
654 if sample.strike { self.draw_rect(x, baseline - (asc / 3).max(1), width, thickness); }
655 self.text.set_font(FONT_REG, self.dp_fx(2688)); let room = (r[2] - self.dp(6)).max(0);
657 let truncated = if self.text.width(label) > room {
658 Some(self.ellipsize(label.as_bytes(), room).0)
659 } else {
660 None
661 };
662 let caption = truncated.as_deref().unwrap_or(label);
663 let w = self.text.width(caption);
664 let baseline = r[1] + r[3] - self.dp(9)
665 + (self.text.ascent() - self.text.descent()) / 2;
666 self.emit_text(
667 r[0] + ((r[2] - w) / 2).max(self.dp(3)),
668 baseline,
669 caption,
670 self.color(T_TEXT),
671 );
672 self.draw_unclip();
673 self.focus_ring_draw(ev);
674 if selected {
675 self.wp.ctx.last_state |= SS_CHECKED as u32;
676 }
677 self.sem(K_BUTTON, id, Some(label), i32::from(selected));
678 self.tooltip(label);
679 return ev;
680 }
681 self.draw_clip(r[0] + self.dp(6), r[1], (r[2] - self.dp(12)).max(0), r[3]);
682 let c = self.color(if sp.state == 3 {
683 T_TEXT_DISABLED
684 } else if typography == TS_SUBTITLE {
685 T_ACCENT_TEXT
686 } else {
687 T_TEXT
688 });
689 let base = self.ts_base(typography);
690 let line = self.ts_line(typography);
691 self.emit_text(
692 r[0] + self.dp(8),
693 r[1] + self.dp(4) + (self.dp(34) - line) / 2 + base,
694 preview,
695 c,
696 );
697 let base = self.ts_base(TS_CAPTION);
698 let c = self.color(if sp.state == 3 {
699 T_TEXT_DISABLED
700 } else {
701 T_TEXT2
702 });
703 self.emit_text(r[0] + self.dp(8), r[1] + self.dp(42) + base, label, c);
704 self.draw_unclip();
705 self.focus_ring_draw(ev);
706 if selected {
707 self.wp.ctx.last_state |= SS_CHECKED as u32;
708 }
709 self.sem(K_BUTTON, id, Some(label), i32::from(selected));
710 self.tooltip(label);
711 ev
712 }
713}