1use alloc::vec::Vec;
26
27use crate::consts::*;
28use crate::keys::*;
29use crate::{Sp, Ui};
30
31#[derive(Clone, Copy, Default, Debug)]
33pub struct NavState {
34 pub compact: bool,
36}
37
38#[derive(Clone, Copy, Debug)]
40pub struct CmdItem<'a> {
41 pub id: u32,
42 pub icon: u32,
43 pub label: &'a str,
44 pub flags: u32,
46}
47
48impl<'a> CmdItem<'a> {
49 pub const fn new(id: u32, icon: u32, label: &'a str) -> CmdItem<'a> {
50 CmdItem { id, icon, label, flags: 0 }
51 }
52 pub const fn flags(mut self, f: u32) -> CmdItem<'a> {
54 self.flags |= f;
55 self
56 }
57 pub const fn enabled(mut self, on: bool) -> CmdItem<'a> {
59 if !on {
60 self.flags |= ST_DISABLED;
61 }
62 self
63 }
64}
65
66#[derive(Clone, Copy, Default)]
67struct NavFrame {
68 compact: bool,
69 scrolled: bool,
70}
71
72#[derive(Clone, Copy, Default)]
73struct SplitFrame {
74 id: u32,
75 pos: i32,
76 vertical: bool,
77 rect: [i32; 4],
78 changed: bool,
79}
80
81#[derive(Default)]
83pub(crate) struct S {
84 nav: Vec<NavFrame>,
85 split: Vec<SplitFrame>,
86}
87
88fn part(id: u32, k: u32) -> u32 {
89 id.wrapping_mul(8).wrapping_add(k) | 0x8000_0000
90}
91
92impl Ui {
93 #[cfg_attr(jfgui_dev, track_caller)]
97 pub fn nav_begin(&mut self, id: u32, nav: &mut NavState) {
98 let _src = self.src_scope();
99 self.box_begin(id, BOX_V | BOX_CLIP | BOX_BLOCK, 0);
100 let r = self.box_rect();
101 let sh = self.style.shell();
102 let mut sp = Sp { flags: nav.compact as u32, ..Sp::default() };
103 sp.place(r);
104 sh.nav_pane(self, &sp);
105 self.next_size(-1, -1);
106 self.scroll_begin(part(id, 1), SC_V | pad(4));
107 self.m.shell.nav.push(NavFrame { compact: nav.compact, scrolled: true });
108 }
109
110 fn nav_top(&self) -> NavFrame {
111 self.m.shell.nav.last().copied().unwrap_or_default()
112 }
113
114 #[cfg_attr(jfgui_dev, track_caller)]
116 pub fn nav_toggle(&mut self, id: u32, label: &str, nav: &mut NavState) -> u32 {
117 let _src = self.src_scope();
118 self.next_icon(IC_MENU);
119 self.next_flags(ST_SUBTLE | ST_ICON_ONLY);
120 self.next_size(40, 36);
121 let ev = self.button(id, label);
122 if ev & EV_ACT != 0 {
123 nav.compact = !nav.compact;
124 self.repass();
125 }
126 ev
127 }
128
129 #[cfg_attr(jfgui_dev, track_caller)]
131 pub fn nav_section(&mut self, label: &str) {
132 let _src = self.src_scope();
133 let f = self.nav_top();
134 let sh = self.style.shell();
135 let mut sp = Sp { flags: if f.compact { NAV_COMPACT } else { 0 }, label: Some(label), ..Sp::default() };
136 let (w, h) = sh.nav_section_size(self, &mut sp);
137 self.next_size(-1, 0);
138 let r = self.place(w, h);
139 sp.place(r);
140 sh.nav_section(self, &sp);
141 self.wp.ctx.last_state = 0;
142 self.sem(K_TEXT, 0, Some(label), 0);
143 }
144
145 #[cfg_attr(jfgui_dev, track_caller)]
147 pub fn nav_item(&mut self, id: u32, icon: u32, label: &str, selected: bool, badge: Option<&str>) -> u32 {
148 let _src = self.src_scope();
149 let f = self.nav_top();
150 let flags = self.take_flags();
151 let sh = self.style.shell();
152 let mut sp = Sp {
153 flags: if f.compact { NAV_COMPACT } else { 0 },
154 icon,
155 label: Some(label),
156 label2: badge,
157 value: selected as i32,
158 ..Sp::default()
159 };
160 let (w, h) = sh.nav_item_size(self, &mut sp);
161 self.next_size(-1, 0);
162 self.place(w, h);
163 let hb = HB_FOCUS | if flags & ST_DISABLED != 0 { HB_DISABLED } else { 0 };
164 let ev = self.behave(id, hb);
165 self.sp_placed(&mut sp, ev);
166 sh.nav_item(self, &sp);
167 self.focus_ring_draw(ev);
168 if selected {
169 self.wp.ctx.last_state |= SS_SELECTED as u32;
170 }
171 self.sem(K_BUTTON, id, Some(label), selected as i32);
172 if f.compact {
173 self.tooltip(label);
174 }
175 ev
176 }
177
178 #[cfg_attr(jfgui_dev, track_caller)]
180 pub fn nav_footer(&mut self) {
181 let _src = self.src_scope();
182 let Some(f) = self.m.shell.nav.last_mut() else { return };
183 if f.scrolled {
184 f.scrolled = false;
185 self.scroll_end();
186 self.spacer(4);
187 }
188 }
189
190 #[cfg_attr(jfgui_dev, track_caller)]
192 pub fn nav_end(&mut self) {
193 let _src = self.src_scope();
194 let Some(f) = self.m.shell.nav.pop() else { return };
195 if f.scrolled {
196 self.scroll_end();
197 } else {
198 self.spacer(4);
199 }
200 self.box_end();
201 }
202
203 #[cfg_attr(jfgui_dev, track_caller)]
207 pub fn command_bar(&mut self, id: u32, items: &[CmdItem]) -> Option<u32> {
208 let _src = self.src_scope();
209 self.box_begin(id, BOX_H | BOX_XCENTER, 4);
210 let r = self.box_rect();
211 let sh = self.style.shell();
212 let mut sp = Sp::default();
213 sp.place(r);
214 sh.command_bar(self, &sp);
215 let st = self.style;
216 let mut widths = Vec::with_capacity(items.len());
218 for it in items {
219 let mut f = ST_SUBTLE;
220 if it.flags & ST_ACCENT != 0 {
221 f = ST_ACCENT;
222 }
223 let mut sp = Sp { flags: f, icon: it.icon, label: Some(it.label), ..Sp::default() };
224 let (w, _) = st.button_size(self, &mut sp);
225 widths.push(w);
226 }
227 let mut more_sp = Sp { flags: ST_SUBTLE | ST_ICON_ONLY, icon: IC_MORE, ..Sp::default() };
228 let (more_w, _) = st.button_size(self, &mut more_sp);
229 let gap = self.dp(4);
230 let sep = self.dp(9);
231 let avail = r[2];
232 let total: i32 = widths.iter().sum::<i32>()
234 + gap * items.len() as i32
235 + items.iter().filter(|i| i.flags & CBI_SEP != 0).count() as i32 * sep;
236 let mut shown = alloc::vec![true; items.len()];
237 if total > avail {
238 let mut used = more_w + gap;
239 for (i, it) in items.iter().enumerate() {
240 let w = widths[i] + gap + if it.flags & CBI_SEP != 0 { sep } else { 0 };
241 if used + w <= avail {
242 used += w;
243 } else {
244 shown[i] = false;
245 }
246 }
247 }
248 let mut clicked = None;
249 let mut ended = false;
250 for (i, it) in items.iter().enumerate() {
251 if !shown[i] {
252 continue;
253 }
254 if it.flags & CBI_END != 0 && !ended {
255 ended = true;
256 self.spacer(-1);
257 } else if it.flags & CBI_SEP != 0 {
258 self.separator();
259 }
260 self.next_icon(it.icon);
261 let mut f = it.flags & (ST_DISABLED | ST_CHECKED);
262 f |= if it.flags & ST_ACCENT != 0 { ST_ACCENT } else { ST_SUBTLE };
263 self.next_flags(f);
264 if self.button(it.id, it.label) & EV_ACT != 0 {
265 clicked = Some(it.id);
266 }
267 }
268 if shown.iter().any(|s| !s) {
269 if !ended {
270 self.spacer(-1);
271 }
272 self.next_icon(IC_MORE);
273 self.next_flags(ST_SUBTLE | ST_ICON_ONLY);
274 let mid = part(id, 1);
275 self.dropdown_button(mid, crate::i18n::tr("Weitere Befehle"));
276 if self.flyout_begin(mid, FL_ALIGN_END) {
277 for (i, it) in items.iter().enumerate() {
278 if shown[i] {
279 continue;
280 }
281 self.next_icon(it.icon);
282 self.next_flags(it.flags & (ST_DISABLED | ST_CHECKED));
283 if self.menu_item(it.id, it.label) & EV_ACT != 0 {
284 clicked = Some(it.id);
285 }
286 }
287 }
288 self.flyout_end();
289 }
290 self.box_end();
291 clicked
292 }
293
294 #[cfg_attr(jfgui_dev, track_caller)]
299 pub fn split_begin(&mut self, id: u32, pos: i32, vertical: bool) {
300 let _src = self.src_scope();
301 self.box_begin(part(id, 0), BOX_FIXED | if vertical { BOX_V } else { BOX_H }, 0);
303 let rect = self.box_rect();
304 let total = self.px2dip(if vertical { rect[3] } else { rect[2] });
305 let grip = 6;
306 let mut p = pos.max(80);
307 if total > 0 {
308 p = p.min((total - grip - 80).max(80));
309 }
310 if vertical {
311 self.next_size(-1, p);
312 } else {
313 self.next_size(p, -1);
314 }
315 self.box_begin(part(id, 1), BOX_V | BOX_CLIP | BOX_FIXED, 0);
316 self.focus_pane_begin(part(id, 1)); self.m.shell.split.push(SplitFrame { id, pos: p, vertical, rect, changed: false });
318 }
319
320 #[cfg_attr(jfgui_dev, track_caller)]
322 pub fn split_next(&mut self) {
323 let _src = self.src_scope();
324 let Some(mut f) = self.m.shell.split.pop() else { return };
325 self.focus_pane_end();
326 self.box_end();
327 let g = 6;
328 if f.vertical {
329 self.next_size(-1, g);
330 } else {
331 self.next_size(g, -1);
332 }
333 let r = self.place(self.dp(g), self.dp(g));
334 let cur = if f.vertical { CUR_SIZE_NS } else { CUR_SIZE_WE };
335 let ev = self.behave(part(f.id, 2), HB_SPLITTER | cur << HB_CURSOR_SHIFT);
336 let d = self.kb_grip(ev, !f.vertical);
338 if d != 0 {
339 let total = self.px2dip(if f.vertical { f.rect[3] } else { f.rect[2] });
340 let p = f.pos.saturating_add(d).clamp(80, (total - g - 80).max(80));
341 if p != f.pos {
342 f.pos = p;
343 f.changed = true;
344 self.repass();
345 }
346 }
347 if ev & EV_HELD != 0 {
348 let (mx, my) = self.mouse();
349 let p = if f.vertical { my - f.rect[1] } else { mx - f.rect[0] } - (self.dp(g) >> 1);
350 let total = self.px2dip(if f.vertical { f.rect[3] } else { f.rect[2] });
351 let p = self.px2dip(p).clamp(80, (total - g - 80).max(80));
352 if p != f.pos {
353 f.pos = p;
354 f.changed = true;
355 self.repass();
356 }
357 }
358 let sh = self.style.shell();
359 let mut sp = Sp { ev, flags: f.vertical as u32, ..Sp::default() };
360 sp.place(r);
361 sh.split_grip(self, &sp);
362 if self.focus_ring(ev) {
363 let st = self.style;
364 st.focus_ring(self, r, 0);
365 }
366 let total = self.px2dip(if f.vertical { f.rect[3] } else { f.rect[2] });
368 let rec = self.sem(K_SLIDER, part(f.id, 2), Some(crate::i18n::tr("Teiler")), f.pos);
369 rec.max = total;
370 self.next_size(-1, -1);
371 self.box_begin(part(f.id, 3), BOX_V | BOX_CLIP | BOX_FIXED, 0);
372 self.focus_pane_begin(part(f.id, 3));
373 self.m.shell.split.push(f);
374 }
375
376 #[cfg_attr(jfgui_dev, track_caller)]
379 pub fn split_end(&mut self, pos: &mut i32) -> bool {
380 let _src = self.src_scope();
381 let Some(f) = self.m.shell.split.pop() else { return false };
382 self.focus_pane_end();
383 self.box_end();
384 self.box_end();
385 if f.changed {
386 *pos = f.pos;
387 }
388 f.changed
389 }
390
391 #[cfg_attr(jfgui_dev, track_caller)]
396 pub fn breadcrumb(&mut self, id: u32, parts: &[&str]) -> Option<usize> {
397 let _src = self.src_scope();
398 self.box_begin(0, BOX_H | BOX_XCENTER, 2);
399 let mut out = None;
400 let n = parts.len();
401 for (i, p) in parts.iter().enumerate() {
402 if i + 1 < n {
403 self.next_flags(ST_SUBTLE | ST_COMPACT);
404 if self.label_button(id.wrapping_add(1 + i as u32), p, TS_BODY | TX_SECONDARY, 6) & EV_ACT != 0 {
405 out = Some(i);
406 }
407 let s = self.dp(16);
408 let r = self.place(s, s);
409 let sh = self.style.shell();
410 let mut sp = Sp::default();
411 sp.place(r);
412 sh.breadcrumb_sep(self, &sp);
413 } else {
414 self.text(TS_BODY_STRONG, p);
415 }
416 }
417 self.box_end();
418 out
419 }
420
421 #[cfg_attr(jfgui_dev, track_caller)]
425 pub fn segmented(&mut self, id: u32, sel: &mut i32, items: &[&str]) -> u32 {
426 let _src = self.src_scope();
427 let flags = self.take_flags();
428 let old = *sel;
429 let pad = self.dp(14);
430 self.ts_font(TS_BODY_STRONG);
431 let ws: Vec<i32> = items.iter().map(|s| self.text.width(s) + pad * 2).collect();
432 let total: i32 = ws.iter().sum();
433 let h = self.dp(if flags & ST_COMPACT != 0 { 28 } else { 32 });
434 let r = self.place(total, h);
435 let disabled = if flags & ST_DISABLED != 0 { HB_DISABLED } else { 0 };
436 let ev = self.behave(id, HB_FOCUS | disabled);
437 let state = self.wp.ctx.last_state;
438 let n = items.len() as i32;
439 if ev & EV_FOCUS != 0 && n > 0 {
440 let mut s = *sel;
441 if self.key(VK_LEFT) {
442 s -= 1;
443 }
444 if self.key(VK_RIGHT) {
445 s += 1;
446 }
447 s = s.clamp(0, n - 1);
448 if s != *sel {
449 *sel = s;
450 self.wp.ctx.focus_visible = true;
451 self.repass();
452 }
453 }
454 let sh = self.style.shell();
455 let mut tsp = Sp { value2: 1, ..Sp::default() };
456 tsp.place(r);
457 sh.segment(self, &tsp);
458 let scale = |w: i32| if total > 0 { (w as i64 * r[2] as i64 / total as i64) as i32 } else { w };
460 let mut x = r[0];
461 for (i, label) in items.iter().enumerate() {
462 let w = if i + 1 == items.len() { r[0] + r[2] - x } else { scale(ws[i]) };
463 let ir = [x, r[1], w, r[3]];
464 let iid = id.wrapping_add(1 + i as u32);
465 let iev = self.hit_rect(iid, ir[0], ir[1], ir[2], ir[3], disabled);
466 if iev & EV_ACT != 0 && *sel != i as i32 {
467 *sel = i as i32;
468 self.wp.ctx.focus = id;
469 self.wp.ctx.focus_visible = false;
470 self.repass();
471 }
472 let mut f = 0;
473 if i == 0 {
474 f |= SEG_FIRST;
475 }
476 if i + 1 == items.len() {
477 f |= SEG_LAST;
478 }
479 let mut sp = Sp { ev: iev, value: (*sel == i as i32) as i32, flags: f, label: Some(label), ..Sp::default() };
480 sp.place(ir);
481 sp.state = if disabled != 0 { 3 } else { 0 };
482 sh.segment(self, &sp);
483 if *sel == i as i32 {
484 self.wp.ctx.last_state |= SS_SELECTED as u32;
485 }
486 self.sem(K_TAB, iid, Some(label), i as i32);
487 x += w;
488 }
489 if ev & EV_FOCUS != 0 && self.wp.ctx.focus_visible {
490 let st = self.style;
491 st.focus_ring(self, r, 4);
492 }
493 self.wp.ctx.last_rect = r;
494 self.wp.ctx.last_state = state;
495 self.sem(K_BOX, id, None, *sel).max = n;
496 if *sel != old { (ev & !EV_ACT) | EV_ACT } else { ev & !EV_ACT }
497 }
498
499 #[cfg_attr(jfgui_dev, track_caller)]
504 pub fn wizard_steps(&mut self, id: u32, steps: &[&str], current: usize, states: &[i32]) -> Option<usize> {
505 let _src = self.src_scope();
506 let mut out = None;
507 let sh = self.style.shell();
508 let h = self.dp(48);
509 for (i, label) in steps.iter().enumerate() {
510 let s = states.get(i).copied().unwrap_or(if i < current {
511 WS_DONE
512 } else if i == current {
513 WS_CURRENT
514 } else {
515 WS_TODO
516 });
517 let s = if i == current { WS_CURRENT } else { s };
518 self.next_size(-1, 0);
519 let r = self.place(self.dp(200), h);
520 let can = i < current || (s == WS_DONE && i != current);
521 let iid = id.wrapping_add(1 + i as u32);
522 let ev = self.behave(iid, if can { HB_FOCUS } else { HB_DISABLED });
523 if ev & EV_ACT != 0 && can {
524 out = Some(i);
525 }
526 let mut sp = Sp {
527 ev,
528 value: i as i32 + 1,
529 value2: s,
530 label: Some(label),
531 flags: (i + 1 < steps.len()) as u32,
532 ..Sp::default()
533 };
534 sp.place(r);
535 sh.wizard_step(self, &sp);
536 self.focus_ring_draw(ev);
537 if i == current {
538 self.wp.ctx.last_state |= SS_SELECTED as u32;
539 }
540 self.sem(K_TAB, iid, Some(label), s);
541 }
542 out
543 }
544
545 #[cfg_attr(jfgui_dev, track_caller)]
548 pub fn page_header(&mut self, title: &str, subtitle: &str) {
549 let _src = self.src_scope();
550 self.box_begin(0, BOX_V, 2);
551 self.text(TS_TITLE, title);
552 if !subtitle.is_empty() {
553 self.text(TS_BODY | TX_SECONDARY | TX_ELLIPSIS, subtitle);
554 }
555 self.box_end();
556 }
557
558 #[cfg_attr(jfgui_dev, track_caller)]
561 pub fn key_value(&mut self, key: &str, value: &str, key_width: i32) {
562 let _src = self.src_scope();
563 self.box_begin(0, BOX_H | BOX_XCENTER, 12);
564 self.next_size(key_width, 0);
565 self.text(TS_BODY | TX_SECONDARY | TX_ELLIPSIS, key);
566 self.next_size(-1, 0);
567 self.text(TS_BODY | TX_ELLIPSIS, value);
568 self.box_end();
569 }
570
571 #[cfg_attr(jfgui_dev, track_caller)]
574 pub fn empty_state(&mut self, icon: u32, title: &str, text: &str) {
575 let _src = self.src_scope();
576 let avail = self.px2dip(self.box_rect()[2]);
578 self.box_begin(0, BOX_V | BOX_CENTER | BOX_XCENTER, 8);
579 if icon != 0 {
580 let s = self.dp(40);
581 let r = self.place(s, s);
582 let c = self.color(T_TEXT3);
583 crate::stock::icon_in(self, icon, r, s, c);
584 }
585 self.text(TS_SUBTITLE | TX_CENTER, title);
586 if !text.is_empty() {
587 self.next_size(if avail > 80 { avail.min(360) } else { 360 }, 0);
588 self.text(TS_BODY | TX_SECONDARY | TX_WRAP | TX_CENTER, text);
589 }
590 self.box_end();
591 }
592}