Skip to main content

nifty_rt/
lower.rs

1//! Turns a program descriptor's windows and views into the runtime's templates, checking
2//! each template has what its kind needs.
3
4use 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    /// Indexed by view id.
22    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    /// How many states the program declares, which every dependency must be one of.
110    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}