1use std::borrow::Cow;
20use std::collections::{HashMap, HashSet};
21
22use iced::font::Weight as FontWeight;
23use iced::widget::{
24 button, checkbox, column, container, pick_list, rule, scrollable, slider as iced_slider,
25 span as iced_span, text, text_input, tooltip, Column, Row, Space, Stack,
26};
27use iced::{Color, Element, Font};
28
29use crate::theme::palette_to_iced_theme;
30use oxiui_core::response::{
31 CheckboxResponse, DropdownResponse, SliderResponse, TextAreaResponse, TextInputResponse,
32 WidgetResponse,
33};
34use oxiui_core::{ButtonResponse, Palette, UiCtx};
35
36#[inline]
44fn palettes_equal(a: &Palette, b: &Palette) -> bool {
45 a.background == b.background
46 && a.surface == b.surface
47 && a.primary == b.primary
48 && a.on_primary == b.on_primary
49 && a.text == b.text
50 && a.muted == b.muted
51}
52
53#[derive(Default)]
77pub struct ThemeCache {
78 last_palette: Option<Palette>,
79 cached_theme: Option<iced::Theme>,
80}
81
82impl ThemeCache {
83 pub fn get_or_compute(&mut self, palette: &Palette) -> iced::Theme {
89 let hit = self
90 .last_palette
91 .as_ref()
92 .is_some_and(|prev| palettes_equal(prev, palette));
93
94 if !hit {
95 let theme = palette_to_iced_theme(palette);
96 self.last_palette = Some(palette.clone());
97 self.cached_theme = Some(theme);
98 }
99
100 self.cached_theme.clone().unwrap_or(iced::Theme::Light)
104 }
105}
106
107#[derive(Debug, Clone)]
111pub enum Message {
112 ButtonPressed(usize),
114 TextChanged(usize, String),
116 CheckboxToggled(usize, bool),
118 SliderChanged(usize, f64),
120 DropdownSelected(usize, usize),
122 TextAreaChanged(usize, String),
124}
125
126#[derive(Debug, Clone)]
130pub enum WidgetState {
131 Text(String),
133 Checked(bool),
135 Slider(f64),
137 Selected(usize),
139 TextArea(String),
141}
142
143#[derive(Debug, Default, Clone)]
150pub struct IcedConfig {
151 pub pending_clicks: HashSet<usize>,
153 pub state: HashMap<usize, WidgetState>,
155 pub spacing: f32,
157 pub padding: f32,
159 pub title: String,
173 pub spec_capacity_hint: usize,
179}
180
181impl IcedConfig {
182 #[must_use]
186 pub fn with_spacing(mut self, px: f32) -> Self {
187 self.spacing = px;
188 self
189 }
190
191 #[must_use]
195 pub fn with_padding(mut self, px: f32) -> Self {
196 self.padding = px;
197 self
198 }
199
200 #[must_use]
204 pub fn with_title(mut self, title: impl Into<String>) -> Self {
205 self.title = title.into();
206 self
207 }
208
209 #[must_use]
214 pub fn with_spec_capacity(mut self, hint: usize) -> Self {
215 self.spec_capacity_hint = hint;
216 self
217 }
218}
219
220pub fn apply_message(
226 state: &mut HashMap<usize, WidgetState>,
227 clicks: &mut HashSet<usize>,
228 msg: &Message,
229) {
230 match msg {
231 Message::ButtonPressed(id) => {
232 clicks.insert(*id);
233 }
234 Message::TextChanged(id, s) => {
235 state.insert(*id, WidgetState::Text(s.clone()));
236 }
237 Message::CheckboxToggled(id, b) => {
238 state.insert(*id, WidgetState::Checked(*b));
239 }
240 Message::SliderChanged(id, v) => {
241 state.insert(*id, WidgetState::Slider(*v));
242 }
243 Message::DropdownSelected(id, i) => {
244 state.insert(*id, WidgetState::Selected(*i));
245 }
246 Message::TextAreaChanged(id, s) => {
247 state.insert(*id, WidgetState::TextArea(s.clone()));
248 }
249 }
250}
251
252#[derive(Clone, Debug)]
261pub struct IcedSpan {
262 pub text: String,
264 pub color: Option<[u8; 4]>,
266 pub bold: bool,
268 pub size: Option<f32>,
270}
271
272#[derive(Debug, Clone)]
279pub enum WidgetSpec {
280 Heading(Cow<'static, str>),
282 Label(Cow<'static, str>),
284 Button {
286 id: usize,
288 label: Cow<'static, str>,
290 },
291 TextInput {
293 id: usize,
295 value: Cow<'static, str>,
297 placeholder: Cow<'static, str>,
299 },
300 TextArea {
310 id: usize,
312 value: Cow<'static, str>,
314 min_rows: usize,
317 },
318 Checkbox {
320 id: usize,
322 label: Cow<'static, str>,
324 checked: bool,
326 },
327 Slider {
329 id: usize,
331 value: f64,
333 start: f64,
335 end: f64,
337 },
338 Dropdown {
340 id: usize,
342 options: Vec<String>,
344 selected: usize,
346 },
347 Image {
349 uri: Cow<'static, str>,
351 size: Option<oxiui_core::geometry::Size>,
353 },
354 Separator,
356 Spacer {
358 size: f32,
360 },
361 Scroll {
363 children: Vec<WidgetSpec>,
365 },
366 Tooltip {
368 inner: Box<WidgetSpec>,
370 text: Cow<'static, str>,
372 },
373 Popup {
375 children: Vec<WidgetSpec>,
377 },
378 Modal {
380 title: Cow<'static, str>,
382 children: Vec<WidgetSpec>,
384 },
385 Horizontal(Vec<WidgetSpec>),
387 Vertical(Vec<WidgetSpec>),
389 Grid {
391 cols: usize,
393 children: Vec<WidgetSpec>,
395 },
396 RichText(Vec<IcedSpan>),
398}
399
400pub struct IcedUiCtx {
408 specs: Vec<WidgetSpec>,
409 next_id: usize,
411 pending_clicks: HashSet<usize>,
412 state: HashMap<usize, WidgetState>,
413 spacing: f32,
414 padding: f32,
415}
416
417impl IcedUiCtx {
418 pub fn new(config: IcedConfig) -> Self {
426 let capacity = config.spec_capacity_hint.max(8);
427 Self {
428 specs: Vec::with_capacity(capacity),
429 next_id: 0,
430 pending_clicks: config.pending_clicks,
431 state: config.state,
432 spacing: config.spacing,
433 padding: config.padding,
434 }
435 }
436
437 pub fn spec_count(&self) -> usize {
456 self.specs.len()
457 }
458
459 fn alloc_id(&mut self) -> usize {
461 let i = self.next_id;
462 self.next_id += 1;
463 i
464 }
465
466 fn child(&self) -> IcedUiCtx {
468 IcedUiCtx {
469 specs: Vec::new(),
470 next_id: self.next_id,
471 pending_clicks: self.pending_clicks.clone(),
472 state: self.state.clone(),
473 spacing: self.spacing,
474 padding: self.padding,
475 }
476 }
477
478 pub fn into_specs(self) -> Vec<WidgetSpec> {
483 self.specs
484 }
485
486 pub fn into_iced_element(self) -> Element<'static, Message> {
491 build_column(self.specs, self.spacing)
492 }
493}
494
495impl UiCtx for IcedUiCtx {
496 fn heading(&mut self, t: &str) {
497 self.specs
498 .push(WidgetSpec::Heading(Cow::Owned(t.to_owned())));
499 }
500
501 fn label(&mut self, t: &str) {
502 self.specs.push(WidgetSpec::Label(Cow::Owned(t.to_owned())));
503 }
504
505 fn button(&mut self, label: &str) -> ButtonResponse {
506 let id = self.alloc_id();
507 self.specs.push(WidgetSpec::Button {
508 id,
509 label: Cow::Owned(label.to_owned()),
510 });
511 ButtonResponse {
512 clicked: self.pending_clicks.contains(&id),
513 hovered: false,
514 }
515 }
516
517 fn text_input(&mut self, text: &str) -> TextInputResponse {
518 let id = self.alloc_id();
519 let cur = match self.state.get(&id) {
520 Some(WidgetState::Text(s)) => s.clone(),
521 _ => text.to_owned(),
522 };
523 let changed = cur != text;
524 self.specs.push(WidgetSpec::TextInput {
525 id,
526 value: Cow::Owned(cur.clone()),
527 placeholder: Cow::Borrowed(""),
528 });
529 TextInputResponse::supported(cur, changed)
530 }
531
532 fn text_area(&mut self, text: &str, min_rows: usize) -> TextAreaResponse {
533 let id = self.alloc_id();
534 let cur = match self.state.get(&id) {
535 Some(WidgetState::TextArea(s)) => s.clone(),
536 _ => text.to_owned(),
537 };
538 let changed = cur != text;
539 let cursor_pos = {
541 let lines: Vec<&str> = cur.lines().collect();
542 let row = lines.len().saturating_sub(1);
543 let col = lines.last().map(|l| l.len()).unwrap_or(0);
544 (row, col)
545 };
546 self.specs.push(WidgetSpec::TextArea {
547 id,
548 value: Cow::Owned(cur.clone()),
549 min_rows: min_rows.max(1),
550 });
551 TextAreaResponse::supported(cur, changed, cursor_pos)
552 }
553
554 fn checkbox(&mut self, label: &str, checked: bool) -> CheckboxResponse {
555 let id = self.alloc_id();
556 let cur = match self.state.get(&id) {
557 Some(WidgetState::Checked(b)) => *b,
558 _ => checked,
559 };
560 let changed = cur != checked;
561 self.specs.push(WidgetSpec::Checkbox {
562 id,
563 label: Cow::Owned(label.to_owned()),
564 checked: cur,
565 });
566 CheckboxResponse::supported(cur, changed)
567 }
568
569 fn slider(&mut self, value: f64, range: std::ops::RangeInclusive<f64>) -> SliderResponse {
570 let id = self.alloc_id();
571 let cur = match self.state.get(&id) {
572 Some(WidgetState::Slider(v)) => *v,
573 _ => value,
574 };
575 let changed = (cur - value).abs() > f64::EPSILON;
576 self.specs.push(WidgetSpec::Slider {
577 id,
578 value: cur,
579 start: *range.start(),
580 end: *range.end(),
581 });
582 SliderResponse::supported(cur, changed)
583 }
584
585 fn dropdown(&mut self, options: &[&str], selected: usize) -> DropdownResponse {
586 let id = self.alloc_id();
587 let cur = match self.state.get(&id) {
588 Some(WidgetState::Selected(i)) => *i,
589 _ => selected,
590 };
591 let changed = cur != selected;
592 let opts: Vec<String> = options.iter().map(|s| s.to_string()).collect();
593 self.specs.push(WidgetSpec::Dropdown {
594 id,
595 options: opts,
596 selected: cur,
597 });
598 DropdownResponse::supported(cur, changed)
599 }
600
601 fn image(&mut self, uri: &str, size: Option<oxiui_core::geometry::Size>) -> WidgetResponse {
602 self.specs.push(WidgetSpec::Image {
603 uri: Cow::Owned(uri.to_owned()),
604 size,
605 });
606 WidgetResponse::supported()
607 }
608
609 fn separator(&mut self) -> WidgetResponse {
610 self.specs.push(WidgetSpec::Separator);
611 WidgetResponse::supported()
612 }
613
614 fn spacer(&mut self, size: f32) -> WidgetResponse {
615 self.specs.push(WidgetSpec::Spacer { size });
616 WidgetResponse::supported()
617 }
618
619 fn scroll_area(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
620 let mut child = self.child();
621 content(&mut child);
622 self.next_id = child.next_id;
623 self.specs.push(WidgetSpec::Scroll {
624 children: child.specs,
625 });
626 WidgetResponse::supported()
627 }
628
629 fn tooltip(&mut self, text: &str) -> WidgetResponse {
630 if let Some(inner) = self.specs.pop() {
631 self.specs.push(WidgetSpec::Tooltip {
632 inner: Box::new(inner),
633 text: Cow::Owned(text.to_owned()),
634 });
635 WidgetResponse::supported()
636 } else {
637 WidgetResponse::unsupported()
638 }
639 }
640
641 fn popup(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
642 let mut child = self.child();
643 content(&mut child);
644 self.next_id = child.next_id;
645 self.specs.push(WidgetSpec::Popup {
646 children: child.specs,
647 });
648 WidgetResponse::supported()
649 }
650
651 fn modal(&mut self, title: &str, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
652 let mut child = self.child();
653 content(&mut child);
654 self.next_id = child.next_id;
655 self.specs.push(WidgetSpec::Modal {
656 title: Cow::Owned(title.to_owned()),
657 children: child.specs,
658 });
659 WidgetResponse::supported()
660 }
661
662 fn horizontal(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
663 let mut child = self.child();
664 content(&mut child);
665 self.next_id = child.next_id;
666 self.specs.push(WidgetSpec::Horizontal(child.specs));
667 WidgetResponse::supported()
668 }
669
670 fn vertical(&mut self, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
671 let mut child = self.child();
672 content(&mut child);
673 self.next_id = child.next_id;
674 self.specs.push(WidgetSpec::Vertical(child.specs));
675 WidgetResponse::supported()
676 }
677
678 fn grid(&mut self, cols: usize, content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
679 let mut child = self.child();
680 content(&mut child);
681 self.next_id = child.next_id;
682 self.specs.push(WidgetSpec::Grid {
683 cols,
684 children: child.specs,
685 });
686 WidgetResponse::supported()
687 }
688
689 fn rich_text(&mut self, spans: &[oxiui_core::RichTextSpan]) -> WidgetResponse {
690 let iced_spans: Vec<IcedSpan> = spans
691 .iter()
692 .map(|s| IcedSpan {
693 text: s.text.clone(),
694 color: Some(s.color),
695 bold: s.bold,
696 size: Some(s.font_size),
697 })
698 .collect();
699 self.specs.push(WidgetSpec::RichText(iced_spans));
700 WidgetResponse::supported()
701 }
702}
703
704pub fn spec_fingerprint(spec: &WidgetSpec) -> u64 {
727 use std::collections::hash_map::DefaultHasher;
728 use std::hash::{Hash, Hasher};
729 let mut h = DefaultHasher::new();
730 format!("{spec:?}").hash(&mut h);
734 h.finish()
735}
736
737#[derive(Debug, Default, Clone)]
767pub struct SpecCache {
768 fingerprints: Vec<u64>,
770 rebuild_count: usize,
772}
773
774impl SpecCache {
775 pub fn sync(&mut self, specs: &[WidgetSpec]) -> bool {
784 let changed = if specs.len() != self.fingerprints.len() {
785 true
786 } else {
787 specs
788 .iter()
789 .zip(self.fingerprints.iter())
790 .any(|(spec, &cached)| spec_fingerprint(spec) != cached)
791 };
792
793 if changed {
794 self.fingerprints = specs.iter().map(spec_fingerprint).collect();
795 self.rebuild_count += 1;
796 }
797
798 changed
799 }
800
801 pub fn rebuild_count(&self) -> usize {
805 self.rebuild_count
806 }
807}
808
809fn build_one(spec: WidgetSpec, spacing: f32) -> Element<'static, Message> {
819 match spec {
820 WidgetSpec::Heading(t) => text(t.into_owned()).size(24).into(),
821 WidgetSpec::Label(t) => text(t.into_owned()).size(14).into(),
822 WidgetSpec::Button { id, label } => button(text(label.into_owned()))
823 .on_press(Message::ButtonPressed(id))
824 .into(),
825 WidgetSpec::TextInput {
826 id,
827 value,
828 placeholder,
829 } => {
830 let placeholder_owned = placeholder.into_owned();
833 let value_owned = value.into_owned();
834 text_input(&placeholder_owned, &value_owned)
835 .on_input(move |s| Message::TextChanged(id, s))
836 .into()
837 }
838 WidgetSpec::TextArea {
839 id,
840 value,
841 min_rows,
842 } => {
843 let lines: Vec<String> = {
850 let raw: Vec<&str> = value.as_ref().lines().collect();
851 let count = raw.len().max(min_rows);
852 let mut v: Vec<String> = raw.iter().map(|l| l.to_string()).collect();
853 v.resize(count, String::new());
854 v
855 };
856 let total_lines = lines.len();
857 let mut col: Column<'static, Message> = column![].spacing(2);
858 for (row_idx, line) in lines.into_iter().enumerate() {
859 let line_clone = line.clone();
860 let input = text_input("", &line).on_input(move |new_line| {
863 let _ = (total_lines, row_idx, line_clone.as_str());
870 Message::TextAreaChanged(id, new_line)
871 });
872 col = col.push(input);
873 }
874 col.into()
875 }
876 WidgetSpec::Checkbox { id, label, checked } => checkbox(checked)
877 .label(label.into_owned())
878 .on_toggle(move |b| Message::CheckboxToggled(id, b))
879 .into(),
880 WidgetSpec::Slider {
881 id,
882 value,
883 start,
884 end,
885 } => {
886 iced_slider((start as f32)..=(end as f32), value as f32, move |v| {
888 Message::SliderChanged(id, v as f64)
889 })
890 .into()
891 }
892 WidgetSpec::Dropdown {
893 id,
894 options,
895 selected,
896 } => {
897 let sel = options.get(selected).cloned();
898 let opts_clone = options.clone();
899 pick_list(options, sel, move |chosen: String| {
900 let idx = opts_clone.iter().position(|o| *o == chosen).unwrap_or(0);
901 Message::DropdownSelected(id, idx)
902 })
903 .into()
904 }
905 WidgetSpec::Image { uri, .. } => {
906 let handle = iced::widget::image::Handle::from_path(uri.as_ref());
907 iced::widget::image(handle).into()
908 }
909 WidgetSpec::Separator => rule::horizontal(1.0_f32).into(),
910 WidgetSpec::Spacer { size } => Space::new().height(size).into(),
911 WidgetSpec::Scroll { children } => {
912 let col = build_column(children, spacing);
913 scrollable(col).into()
914 }
915 WidgetSpec::Tooltip { inner, text: tip } => {
916 let tip_widget = container(text(tip.into_owned()));
917 tooltip(
918 build_one(*inner, spacing),
919 tip_widget,
920 tooltip::Position::Top,
921 )
922 .into()
923 }
924 WidgetSpec::Popup { children } => {
925 let col = build_column(children, spacing);
926 Stack::with_children([container(col).into()]).into()
927 }
928 WidgetSpec::Modal { title, children } => {
929 let mut col: Column<'static, Message> =
930 column![text(title.into_owned()).size(18)].spacing(spacing);
931 for c in children {
932 col = col.push(build_one(c, spacing));
933 }
934 container(col).padding(12).into()
935 }
936 WidgetSpec::Horizontal(specs) => {
937 let children: Vec<Element<'static, Message>> =
938 specs.into_iter().map(|s| build_one(s, spacing)).collect();
939 Row::with_children(children).spacing(spacing).into()
940 }
941 WidgetSpec::Vertical(specs) => build_column(specs, spacing),
942 WidgetSpec::Grid { cols, children } => {
943 let safe_cols = cols.max(1);
946 let row_elements: Vec<Element<'static, Message>> = children
947 .chunks(safe_cols)
948 .map(|row_specs| {
949 let row_children: Vec<Element<'static, Message>> = row_specs
950 .iter()
951 .map(|s| build_one(s.clone(), spacing))
952 .collect();
953 Row::with_children(row_children).spacing(spacing).into()
954 })
955 .collect();
956 build_column_from_elements(row_elements, spacing)
957 }
958 WidgetSpec::RichText(spans) => {
959 let iced_spans: Vec<iced::widget::text::Span<'static, (), Font>> = spans
961 .into_iter()
962 .map(|s| {
963 let mut sp = iced_span::<(), Font>(s.text);
964 if let Some([r, g, b, a]) = s.color {
965 sp = sp.color(Color::from_rgba8(r, g, b, a as f32 / 255.0));
966 }
967 if s.bold {
968 sp = sp.font(Font {
969 weight: FontWeight::Bold,
970 ..Font::default()
971 });
972 }
973 if let Some(sz) = s.size {
974 sp = sp.size(sz);
975 }
976 sp
977 })
978 .collect();
979 iced::widget::rich_text(iced_spans).into()
980 }
981 }
982}
983
984fn build_column_from_elements(
986 elements: Vec<Element<'static, Message>>,
987 spacing: f32,
988) -> Element<'static, Message> {
989 let mut col: Column<'static, Message> = column![].spacing(spacing);
990 for el in elements {
991 col = col.push(el);
992 }
993 col.into()
994}
995
996fn build_column(specs: Vec<WidgetSpec>, spacing: f32) -> Element<'static, Message> {
998 let mut col: Column<'static, Message> = column![].spacing(spacing);
999 for spec in specs {
1000 col = col.push(build_one(spec, spacing));
1001 }
1002 col.into()
1003}
1004
1005#[derive(Default)]
1012pub struct IcedNullCtx {
1013 pub log: Option<Vec<(&'static str, String)>>,
1015}
1016
1017impl IcedNullCtx {
1018 pub fn recording() -> Self {
1020 Self {
1021 log: Some(Vec::new()),
1022 }
1023 }
1024
1025 fn record(&mut self, method: &'static str, arg: impl Into<String>) {
1027 if let Some(l) = self.log.as_mut() {
1028 l.push((method, arg.into()));
1029 }
1030 }
1031}
1032
1033impl UiCtx for IcedNullCtx {
1034 fn heading(&mut self, t: &str) {
1035 self.record("heading", t);
1036 }
1037
1038 fn label(&mut self, t: &str) {
1039 self.record("label", t);
1040 }
1041
1042 fn button(&mut self, label: &str) -> ButtonResponse {
1043 self.record("button", label);
1044 ButtonResponse::default()
1045 }
1046
1047 fn text_input(&mut self, text: &str) -> TextInputResponse {
1048 self.record("text_input", text);
1049 TextInputResponse::unsupported()
1050 }
1051
1052 fn text_area(&mut self, text: &str, min_rows: usize) -> TextAreaResponse {
1053 self.record("text_area", format!("{text}|rows={min_rows}"));
1054 TextAreaResponse::unsupported()
1055 }
1056
1057 fn checkbox(&mut self, label: &str, _checked: bool) -> CheckboxResponse {
1058 self.record("checkbox", label);
1059 CheckboxResponse::unsupported()
1060 }
1061
1062 fn slider(&mut self, value: f64, _range: std::ops::RangeInclusive<f64>) -> SliderResponse {
1063 self.record("slider", value.to_string());
1064 SliderResponse::unsupported()
1065 }
1066
1067 fn dropdown(&mut self, _options: &[&str], selected: usize) -> DropdownResponse {
1068 self.record("dropdown", selected.to_string());
1069 DropdownResponse::unsupported()
1070 }
1071
1072 fn image(&mut self, uri: &str, _size: Option<oxiui_core::geometry::Size>) -> WidgetResponse {
1073 self.record("image", uri);
1074 WidgetResponse::supported()
1075 }
1076
1077 fn separator(&mut self) -> WidgetResponse {
1078 self.record("separator", "");
1079 WidgetResponse::unsupported()
1080 }
1081
1082 fn spacer(&mut self, size: f32) -> WidgetResponse {
1083 self.record("spacer", size.to_string());
1084 WidgetResponse::unsupported()
1085 }
1086
1087 fn scroll_area(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
1088 self.record("scroll_area", "");
1089 WidgetResponse::unsupported()
1090 }
1091
1092 fn tooltip(&mut self, text: &str) -> WidgetResponse {
1093 self.record("tooltip", text);
1094 WidgetResponse::unsupported()
1095 }
1096
1097 fn popup(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
1098 self.record("popup", "");
1099 WidgetResponse::unsupported()
1100 }
1101
1102 fn modal(&mut self, title: &str, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
1103 self.record("modal", title);
1104 WidgetResponse::unsupported()
1105 }
1106
1107 fn horizontal(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
1108 self.record("horizontal", "");
1109 WidgetResponse::unsupported()
1110 }
1111
1112 fn vertical(&mut self, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
1113 self.record("vertical", "");
1114 WidgetResponse::unsupported()
1115 }
1116
1117 fn grid(&mut self, cols: usize, _content: &mut dyn FnMut(&mut dyn UiCtx)) -> WidgetResponse {
1118 self.record("grid", cols.to_string());
1119 WidgetResponse::unsupported()
1120 }
1121
1122 fn rich_text(&mut self, spans: &[oxiui_core::RichTextSpan]) -> WidgetResponse {
1123 self.record("rich_text", spans.len().to_string());
1124 WidgetResponse::unsupported()
1125 }
1126}
1127
1128use iced::advanced::{layout, renderer, widget as adv_widget};
1131
1132pub struct OxiIcedWidget {
1137 spec: WidgetSpec,
1138 width: iced::Length,
1139 height: iced::Length,
1140}
1141
1142impl OxiIcedWidget {
1143 pub fn new(spec: WidgetSpec) -> Self {
1145 OxiIcedWidget {
1146 spec,
1147 width: iced::Length::Shrink,
1148 height: iced::Length::Shrink,
1149 }
1150 }
1151
1152 pub fn spec(&self) -> &WidgetSpec {
1154 &self.spec
1155 }
1156
1157 pub fn width(mut self, w: iced::Length) -> Self {
1159 self.width = w;
1160 self
1161 }
1162
1163 pub fn height(mut self, h: iced::Length) -> Self {
1165 self.height = h;
1166 self
1167 }
1168}
1169
1170impl<Msg, Theme, Renderer> iced::advanced::Widget<Msg, Theme, Renderer> for OxiIcedWidget
1171where
1172 Renderer: iced::advanced::Renderer,
1173{
1174 fn size(&self) -> iced::Size<iced::Length> {
1175 iced::Size::new(self.width, self.height)
1176 }
1177
1178 fn layout(
1179 &mut self,
1180 _tree: &mut adv_widget::Tree,
1181 _renderer: &Renderer,
1182 limits: &layout::Limits,
1183 ) -> layout::Node {
1184 let size = limits.resolve(self.width, self.height, iced::Size::ZERO);
1185 layout::Node::new(size)
1186 }
1187
1188 fn draw(
1189 &self,
1190 _tree: &adv_widget::Tree,
1191 _renderer: &mut Renderer,
1192 _theme: &Theme,
1193 _style: &renderer::Style,
1194 _layout: iced::advanced::Layout<'_>,
1195 _cursor: iced::advanced::mouse::Cursor,
1196 _viewport: &iced::Rectangle,
1197 ) {
1198 }
1202}
1203
1204pub fn oxi_widget(spec: WidgetSpec) -> OxiIcedWidget {
1209 OxiIcedWidget::new(spec)
1210}
1211
1212pub fn map_iced_keyboard_event(ev: &iced::keyboard::Event) -> Option<oxiui_core::UiEvent> {
1219 use iced::keyboard::Event as KbEv;
1220 match ev {
1221 KbEv::KeyPressed {
1222 key,
1223 modifiers,
1224 repeat,
1225 ..
1226 } => Some(oxiui_core::UiEvent::KeyDown {
1227 key: map_iced_key(key),
1228 modifiers: map_iced_modifiers(*modifiers),
1229 repeat: *repeat,
1230 }),
1231 KbEv::KeyReleased { key, modifiers, .. } => Some(oxiui_core::UiEvent::KeyUp {
1232 key: map_iced_key(key),
1233 modifiers: map_iced_modifiers(*modifiers),
1234 }),
1235 KbEv::ModifiersChanged(_) => None,
1236 }
1237}
1238
1239pub fn map_iced_key(key: &iced::keyboard::Key) -> oxiui_core::events::Key {
1241 use iced::keyboard::key::Named;
1242 use iced::keyboard::Key as IK;
1243 use oxiui_core::events::Key as OxiKey;
1244
1245 match key {
1246 IK::Character(s) => OxiKey::Character(s.as_str().to_owned()),
1247 IK::Named(named) => match named {
1248 Named::Enter => OxiKey::Enter,
1249 Named::Tab => OxiKey::Tab,
1250 Named::Space => OxiKey::Space,
1251 Named::Backspace => OxiKey::Backspace,
1252 Named::Delete => OxiKey::Delete,
1253 Named::Escape => OxiKey::Escape,
1254 Named::ArrowLeft => OxiKey::ArrowLeft,
1255 Named::ArrowRight => OxiKey::ArrowRight,
1256 Named::ArrowUp => OxiKey::ArrowUp,
1257 Named::ArrowDown => OxiKey::ArrowDown,
1258 Named::Home => OxiKey::Home,
1259 Named::End => OxiKey::End,
1260 Named::PageUp => OxiKey::PageUp,
1261 Named::PageDown => OxiKey::PageDown,
1262 Named::F1 => OxiKey::Function(1),
1263 Named::F2 => OxiKey::Function(2),
1264 Named::F3 => OxiKey::Function(3),
1265 Named::F4 => OxiKey::Function(4),
1266 Named::F5 => OxiKey::Function(5),
1267 Named::F6 => OxiKey::Function(6),
1268 Named::F7 => OxiKey::Function(7),
1269 Named::F8 => OxiKey::Function(8),
1270 Named::F9 => OxiKey::Function(9),
1271 Named::F10 => OxiKey::Function(10),
1272 Named::F11 => OxiKey::Function(11),
1273 Named::F12 => OxiKey::Function(12),
1274 other => OxiKey::Named(format!("{other:?}")),
1275 },
1276 IK::Unidentified => OxiKey::Named("Unidentified".to_owned()),
1277 }
1278}
1279
1280pub fn map_iced_modifiers(mods: iced::keyboard::Modifiers) -> oxiui_core::events::Modifiers {
1282 oxiui_core::events::Modifiers {
1283 ctrl: mods.control(),
1284 shift: mods.shift(),
1285 alt: mods.alt(),
1286 meta: mods.logo(),
1287 }
1288}
1289
1290#[cfg(test)]
1293mod tests {
1294 use super::*;
1295
1296 #[test]
1299 fn image_ctx_returns_supported() {
1300 let mut ctx = IcedUiCtx::new(IcedConfig::default());
1301 let resp = ctx.image("test.png", None);
1302 assert!(resp.supported, "IcedUiCtx::image() must return supported");
1303 }
1304
1305 #[test]
1306 fn image_null_ctx_returns_supported() {
1307 let mut ctx = IcedNullCtx::recording();
1308 let resp = ctx.image("test.png", None);
1309 assert!(resp.supported, "IcedNullCtx::image() must return supported");
1310 }
1311
1312 #[test]
1315 fn oxi_widget_constructs_with_shrink_defaults() {
1316 let w = oxi_widget(WidgetSpec::Label(Cow::Borrowed("hello")));
1317 assert_eq!(w.width, iced::Length::Shrink);
1318 assert_eq!(w.height, iced::Length::Shrink);
1319 }
1320
1321 #[test]
1322 fn oxi_widget_builder_overrides_dimensions() {
1323 let w = oxi_widget(WidgetSpec::Label(Cow::Borrowed("hi")))
1324 .width(iced::Length::Fill)
1325 .height(iced::Length::Fixed(100.0));
1326 assert_eq!(w.width, iced::Length::Fill);
1327 assert_eq!(w.height, iced::Length::Fixed(100.0));
1328 }
1329
1330 #[test]
1333 fn map_character_key_a() {
1334 let key = iced::keyboard::Key::Character("a".into());
1335 let result = map_iced_key(&key);
1336 assert_eq!(result, oxiui_core::events::Key::Character("a".to_owned()));
1337 }
1338
1339 #[test]
1340 fn map_character_key_z() {
1341 let key = iced::keyboard::Key::Character("z".into());
1342 let result = map_iced_key(&key);
1343 assert_eq!(result, oxiui_core::events::Key::Character("z".to_owned()));
1344 }
1345
1346 #[test]
1347 fn map_named_enter() {
1348 let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::Enter);
1349 let result = map_iced_key(&key);
1350 assert_eq!(result, oxiui_core::events::Key::Enter);
1351 }
1352
1353 #[test]
1354 fn map_named_escape() {
1355 let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape);
1356 let result = map_iced_key(&key);
1357 assert_eq!(result, oxiui_core::events::Key::Escape);
1358 }
1359
1360 #[test]
1361 fn map_named_arrow_left() {
1362 let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::ArrowLeft);
1363 let result = map_iced_key(&key);
1364 assert_eq!(result, oxiui_core::events::Key::ArrowLeft);
1365 }
1366
1367 #[test]
1368 fn map_named_arrow_right() {
1369 let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::ArrowRight);
1370 let result = map_iced_key(&key);
1371 assert_eq!(result, oxiui_core::events::Key::ArrowRight);
1372 }
1373
1374 #[test]
1375 fn map_named_f1() {
1376 let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::F1);
1377 let result = map_iced_key(&key);
1378 assert_eq!(result, oxiui_core::events::Key::Function(1));
1379 }
1380
1381 #[test]
1382 fn map_named_f12() {
1383 let key = iced::keyboard::Key::Named(iced::keyboard::key::Named::F12);
1384 let result = map_iced_key(&key);
1385 assert_eq!(result, oxiui_core::events::Key::Function(12));
1386 }
1387
1388 #[test]
1389 fn map_unidentified_key() {
1390 let key = iced::keyboard::Key::Unidentified;
1391 let result = map_iced_key(&key);
1392 assert_eq!(
1393 result,
1394 oxiui_core::events::Key::Named("Unidentified".to_owned())
1395 );
1396 }
1397
1398 #[test]
1399 fn map_modifiers_ctrl_shift() {
1400 use iced::keyboard::Modifiers;
1401 let mods = Modifiers::CTRL | Modifiers::SHIFT;
1402 let result = map_iced_modifiers(mods);
1403 assert!(result.ctrl, "ctrl must be set");
1404 assert!(result.shift, "shift must be set");
1405 assert!(!result.alt, "alt must not be set");
1406 assert!(!result.meta, "meta must not be set");
1407 }
1408
1409 #[test]
1410 fn map_modifiers_none() {
1411 use iced::keyboard::Modifiers;
1412 let result = map_iced_modifiers(Modifiers::NONE);
1413 assert!(!result.ctrl);
1414 assert!(!result.shift);
1415 assert!(!result.alt);
1416 assert!(!result.meta);
1417 }
1418
1419 #[test]
1420 fn map_modifiers_alt_logo() {
1421 use iced::keyboard::Modifiers;
1422 let mods = Modifiers::ALT | Modifiers::LOGO;
1423 let result = map_iced_modifiers(mods);
1424 assert!(result.alt, "alt must be set");
1425 assert!(result.meta, "meta must be set");
1426 assert!(!result.ctrl);
1427 assert!(!result.shift);
1428 }
1429}