1use std::rc::Rc;
5
6use nifty_runtime::{
7 AppTemplate, Menu, MenuBar, MenuEntry, Size, StateId, StateSet, Template, Templates,
8 ToggleStyle, Widget, WindowTemplate,
9};
10
11use crate::context::AppError;
12use crate::ffi::{Slice, Str};
13use crate::logic::{Binding, Compiled, Frame, Region, ViewTemplate};
14use crate::program::{
15 ABI_VERSION, ActionFn, BindingDesc, MenuDesc, MenuEntryDesc, MenuEntryKind, ProgramDesc,
16 TemplateDesc, TemplateKind,
17};
18
19pub(crate) struct Lowered {
20 pub app: AppTemplate<Compiled>,
21 pub views: Vec<ViewTemplate>,
23}
24
25pub(crate) fn lower(program: &'static ProgramDesc) -> Result<Lowered, AppError> {
26 if program.abi_version != ABI_VERSION {
27 return Err(AppError::malformed(&format!(
28 "it was built for runtime version {}, not {ABI_VERSION}",
29 program.abi_version
30 )));
31 }
32 let lowerer = Lowerer {
33 states: program.states.items().len(),
34 };
35 let windows = program
36 .windows
37 .items()
38 .iter()
39 .map(|window| {
40 Ok(WindowTemplate {
41 title: text(&window.title)?.to_owned(),
42 size: Size::new(window.width, window.height),
43 backdrop: window.backdrop,
44 style: window.style,
45 children: lowerer.templates(&window.children)?,
46 frame: Rc::new(Frame::empty(&window.frame)),
47 starts_open: window.starts_open,
48 })
49 })
50 .collect::<Result<_, AppError>>()?;
51 let views = program
52 .views
53 .items()
54 .iter()
55 .map(|view| {
56 Ok(ViewTemplate {
57 body: lowerer.templates(&view.body)?,
58 frame: &view.frame,
59 })
60 })
61 .collect::<Result<_, AppError>>()?;
62 let menus = program
63 .menus
64 .items()
65 .iter()
66 .map(menu)
67 .collect::<Result<_, AppError>>()?;
68 let menu_bar = MenuBar {
69 menus,
70 frame: Frame::empty(&program.menu_frame),
71 };
72 Ok(Lowered {
73 app: AppTemplate { windows, menu_bar },
74 views,
75 })
76}
77
78fn menu(desc: &'static MenuDesc) -> Result<Menu<ActionFn>, AppError> {
79 Ok(Menu {
80 title: text(&desc.title)?.to_owned(),
81 entries: menu_entries(&desc.entries)?,
82 })
83}
84
85fn menu_entries(
86 entries: &'static Slice<MenuEntryDesc>,
87) -> Result<Vec<MenuEntry<ActionFn>>, AppError> {
88 entries
89 .items()
90 .iter()
91 .map(|entry| {
92 Ok(match entry.kind {
93 MenuEntryKind::Item => MenuEntry::Item {
94 title: text(&entry.title)?.to_owned(),
95 shortcut: entry.shortcut.present.then_some(entry.shortcut.shortcut),
96 action: required(entry.action, "a menu item's action")?,
97 },
98 MenuEntryKind::Separator => MenuEntry::Separator,
99 MenuEntryKind::Submenu => MenuEntry::Submenu(Menu {
100 title: text(&entry.title)?.to_owned(),
101 entries: menu_entries(&entry.entries)?,
102 }),
103 })
104 })
105 .collect()
106}
107
108struct Lowerer {
109 states: usize,
111}
112
113impl Lowerer {
114 fn templates(
115 &self,
116 list: &'static Slice<TemplateDesc>,
117 ) -> Result<Templates<Compiled>, AppError> {
118 list.items()
119 .iter()
120 .map(|desc| self.template(desc))
121 .collect()
122 }
123
124 fn template(&self, desc: &'static TemplateDesc) -> Result<Template<Compiled>, AppError> {
125 let deps = self.deps(&desc.deps)?;
126 let widget = match desc.kind {
127 TemplateKind::Stack | TemplateKind::Scroll => {
128 return Ok(Template::Stack {
129 style: desc.stack,
130 children: self.templates(&desc.children)?,
131 scrolls: desc.kind == TemplateKind::Scroll,
132 });
133 }
134 TemplateKind::Region => {
135 let region = Region {
136 expand: required(desc.region.expand, "a region's expansion")?,
137 lists: desc
138 .region
139 .lists
140 .items()
141 .iter()
142 .map(|list| self.templates(list))
143 .collect::<Result<_, _>>()?,
144 };
145 return Ok(Template::Region { region, deps });
146 }
147 TemplateKind::Label => Widget::Label {
148 text: required(desc.text, "a label's text")?,
149 style: desc.label,
150 },
151 TemplateKind::Button => Widget::Button {
152 title: required(desc.text, "a button's title")?,
153 style: desc.button,
154 icon: desc.icon,
155 selected: desc.value,
156 action: required(desc.action, "a button's action")?,
157 },
158 TemplateKind::TextField => Widget::TextField {
159 value: binding(desc.binding, "a text field's binding")?,
160 placeholder: text(&desc.placeholder)?.to_owned(),
161 lines: desc.lines,
162 action: desc.action,
163 },
164 TemplateKind::Checkbox | TemplateKind::Switch => Widget::Checkbox {
165 checked: binding(desc.binding, "a checkbox's binding")?,
166 title: desc.text,
167 style: if desc.kind == TemplateKind::Switch {
168 ToggleStyle::Switch
169 } else {
170 ToggleStyle::Checkbox
171 },
172 },
173 TemplateKind::Slider => Widget::Slider {
174 value: binding(desc.binding, "a slider's binding")?,
175 min: desc.min,
176 max: desc.max,
177 },
178 TemplateKind::Picker => Widget::Picker {
179 selection: binding(desc.binding, "a picker's binding")?,
180 options: required(desc.value, "a picker's options")?,
181 },
182 TemplateKind::NavItem => Widget::NavItem {
183 title: required(desc.text, "a nav item's title")?,
184 value: required(desc.value, "a nav item's value")?,
185 selection: binding(desc.binding, "a nav item's selection")?,
186 icon: desc.icon,
187 badge: desc.badge,
188 },
189 TemplateKind::Image => Widget::Image {
190 path: required(desc.text, "an image's path")?,
191 size: desc.image,
192 },
193 TemplateKind::Progress => Widget::Progress { value: desc.value },
194 TemplateKind::Spacer => Widget::Spacer,
195 TemplateKind::Divider => Widget::Divider,
196 TemplateKind::ContextMenu => Widget::ContextMenu {
197 entries: menu_entries(&desc.menu)?,
198 },
199 TemplateKind::DropTarget => Widget::DropTarget {
200 dropped: binding(desc.binding, "a drop target's binding")?,
201 action: desc.action,
202 },
203 };
204 Ok(Template::Widget { widget, deps })
205 }
206
207 fn deps(&self, deps: &Slice<u32>) -> Result<StateSet, AppError> {
208 let mut set = StateSet::default();
209 for state in deps.items() {
210 let state = *state as usize;
211 if state >= self.states {
212 return Err(AppError::malformed(&format!(
213 "UI depends on state {state}, but there are only {}",
214 self.states
215 )));
216 }
217 set.insert(StateId(state));
218 }
219 Ok(set)
220 }
221}
222
223fn required<T>(part: Option<T>, what: &str) -> Result<T, AppError> {
224 part.ok_or_else(|| AppError::malformed(&format!("{what} is missing")))
225}
226
227fn binding(binding: BindingDesc, what: &str) -> Result<Binding, AppError> {
228 match (binding.read, binding.write) {
229 (Some(read), Some(write)) => Ok(Binding { read, write }),
230 _ => Err(AppError::malformed(&format!("{what} is missing"))),
231 }
232}
233
234fn text(text: &Str) -> Result<&'static str, AppError> {
235 text.text()
236 .map_err(|error| AppError::malformed(&format!("some text isn't UTF-8: {error}")))
237}