1use std::marker::PhantomData;
9
10use crux_core::{
11 App, Command,
12 capability::Operation,
13 macros::effect,
14 render::{RenderOperation, render},
15};
16use facet::Facet;
17use serde::{Deserialize, Serialize, de::DeserializeOwned};
18
19pub use mobiler_ui::{
20 Action, BoxAlign, ButtonStyle, CardStyle, Icon, ImageRatio, ImageShape, InputValue,
21 ProjectColor, Spacing, Tab, TextStyle, Tone, Widget,
22};
23
24#[effect(facet_typegen)]
28#[derive(Debug)]
29pub enum Effect {
30 Render(RenderOperation),
31 PluginNotify(PluginNotify),
33 Plugin(PluginCall),
35}
36
37#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
38pub struct PluginNotify {
39 pub plugin: String,
40 pub op: String,
41 pub input: String,
42}
43impl Operation for PluginNotify {
44 type Output = ();
45}
46
47#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
48pub struct PluginCall {
49 pub plugin: String,
50 pub op: String,
51 pub input: String,
52}
53impl Operation for PluginCall {
54 type Output = PluginResponse;
55}
56
57#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
58pub struct PluginResponse {
59 pub ok: bool,
60 pub output: String,
61}
62
63type Continuation<E> = Box<dyn FnOnce(PluginResponse) -> E + Send>;
64
65pub struct Cx<E> {
68 notifications: Vec<PluginNotify>,
69 requests: Vec<(PluginCall, Continuation<E>)>,
70}
71
72impl<E> Default for Cx<E> {
73 fn default() -> Self {
74 Self { notifications: Vec::new(), requests: Vec::new() }
75 }
76}
77
78impl<E> Cx<E> {
79 pub fn notify(&mut self, plugin: impl Into<String>, op: impl Into<String>, input: impl Into<String>) {
81 self.notifications.push(PluginNotify { plugin: plugin.into(), op: op.into(), input: input.into() });
82 }
83
84 pub fn plugin(
87 &mut self,
88 plugin: impl Into<String>,
89 op: impl Into<String>,
90 input: impl Into<String>,
91 then: impl FnOnce(PluginResponse) -> E + Send + 'static,
92 ) {
93 self.requests
94 .push((PluginCall { plugin: plugin.into(), op: op.into(), input: input.into() }, Box::new(then)));
95 }
96
97 pub fn save(&mut self, data: impl Into<String>) {
99 self.notify("storage", "save", data);
100 }
101}
102
103pub trait MobilerApp: Default {
108 type Event: Serialize + DeserializeOwned + Send + 'static;
109 type Model: Default;
110
111 fn update(&self, event: Self::Event, model: &mut Self::Model, cx: &mut Cx<Self::Event>);
112
113 fn input(&self, id: &str, value: InputValue, model: &mut Self::Model, cx: &mut Cx<Self::Event>) {
114 let _ = (id, value, model, cx);
115 }
116
117 fn restore(&self, data: &str, model: &mut Self::Model) {
120 let _ = (data, model);
121 }
122
123 fn view(&self, model: &Self::Model) -> Widget;
124}
125
126pub struct MobilerShell<A>(PhantomData<fn() -> A>);
128
129impl<A> Default for MobilerShell<A> {
130 fn default() -> Self {
131 Self(PhantomData)
132 }
133}
134
135impl<A: MobilerApp> App for MobilerShell<A> {
136 type Event = Action;
137 type Model = A::Model;
138 type ViewModel = Widget;
139 type Effect = Effect;
140
141 fn update(&self, action: Action, model: &mut Self::Model) -> Command<Effect, Action> {
142 let app = A::default();
143 let mut cx = Cx::<A::Event>::default();
144 match action {
145 Action::Fired { token } => {
146 if let Ok(event) = serde_json::from_str::<A::Event>(&token) {
147 app.update(event, model, &mut cx);
148 }
149 }
150 Action::Input { id, value } => app.input(&id, value, model, &mut cx),
151 Action::Restore { data } => app.restore(&data, model),
152 }
153 let mut commands: Vec<Command<Effect, Action>> = Vec::new();
154 for op in cx.notifications {
155 commands.push(Command::notify_shell(op).build());
156 }
157 for (op, then) in cx.requests {
158 commands.push(Command::request_from_shell(op).then_send(move |response: PluginResponse| {
159 Action::Fired { token: serde_json::to_string(&then(response)).expect("serialize event") }
160 }));
161 }
162 commands.push(render());
163 Command::all(commands)
164 }
165
166 fn view(&self, model: &Self::Model) -> Widget {
167 A::default().view(model)
168 }
169}
170
171fn tok<E: Serialize>(event: E) -> String {
175 serde_json::to_string(&event).expect("serialize event")
176}
177
178#[must_use]
179pub fn styled(content: impl Into<String>, style: TextStyle) -> Widget {
180 Widget::Text { content: content.into(), style }
181}
182#[must_use]
183pub fn text(content: impl Into<String>) -> Widget { styled(content, TextStyle::Body) }
184#[must_use]
185pub fn title(content: impl Into<String>) -> Widget { styled(content, TextStyle::Title) }
186#[must_use]
187pub fn subtitle(content: impl Into<String>) -> Widget { styled(content, TextStyle::Subtitle) }
188#[must_use]
189pub fn caption(content: impl Into<String>) -> Widget { styled(content, TextStyle::Caption) }
190#[must_use]
191pub fn emphasis(content: impl Into<String>) -> Widget { styled(content, TextStyle::Emphasis) }
192
193#[must_use]
194pub fn image(source: impl Into<String>, shape: ImageShape, ratio: ImageRatio) -> Widget {
195 Widget::Image { source: source.into(), shape, ratio }
196}
197#[must_use]
198pub fn badge(label: impl Into<String>, tone: Tone) -> Widget {
199 Widget::Badge { label: label.into(), tone }
200}
201#[must_use]
203pub fn color_dot(color: ProjectColor) -> Widget {
204 Widget::ColorDot { color }
205}
206#[must_use]
207pub fn divider() -> Widget { Widget::Divider }
208#[must_use]
209pub fn spacer(size: Spacing) -> Widget { Widget::Spacer { size } }
210
211#[must_use]
212pub fn row(children: Vec<Widget>) -> Widget { Widget::Row { children } }
213#[must_use]
214pub fn column(children: Vec<Widget>) -> Widget { Widget::Column { children } }
215#[must_use]
216pub fn card(child: Widget, style: CardStyle) -> Widget {
217 Widget::Card { child: Box::new(child), style, on_press: None }
218}
219#[must_use]
221pub fn card_button<E: Serialize>(child: Widget, style: CardStyle, on_press: E) -> Widget {
222 Widget::Card { child: Box::new(child), style, on_press: Some(tok(on_press)) }
223}
224#[must_use]
227pub fn stack(align: BoxAlign, scrim: bool, children: Vec<Widget>) -> Widget {
228 Widget::Box { children, align, scrim }
229}
230#[must_use]
231pub fn grid(children: Vec<Widget>) -> Widget { Widget::Grid { children } }
232
233#[must_use]
234pub fn button<E: Serialize>(label: impl Into<String>, style: ButtonStyle, on_press: E) -> Widget {
235 Widget::Button { label: label.into(), style, on_press: tok(on_press) }
236}
237#[must_use]
238pub fn icon_button<E: Serialize>(icon: Icon, on_press: E) -> Widget {
239 Widget::IconButton { icon, on_press: tok(on_press) }
240}
241#[must_use]
242pub fn chip<E: Serialize>(label: impl Into<String>, selected: bool, on_press: E) -> Widget {
243 Widget::Chip { label: label.into(), selected, on_press: tok(on_press) }
244}
245#[must_use]
246pub fn text_field(id: impl Into<String>, placeholder: impl Into<String>, value: impl Into<String>) -> Widget {
247 Widget::TextField { id: id.into(), placeholder: placeholder.into(), value: value.into() }
248}
249#[must_use]
250pub fn switch(id: impl Into<String>, label: impl Into<String>, value: bool) -> Widget {
251 Widget::Switch { id: id.into(), label: label.into(), value }
252}
253#[must_use]
254pub fn checkbox(id: impl Into<String>, label: impl Into<String>, value: bool) -> Widget {
255 Widget::Checkbox { id: id.into(), label: label.into(), value }
256}
257#[must_use]
258pub fn slider(id: impl Into<String>, value: i32, max: i32) -> Widget {
259 Widget::Slider { id: id.into(), value, max }
260}
261#[must_use]
262pub fn stepper<E: Serialize>(value: i32, on_decrement: E, on_increment: E) -> Widget {
263 Widget::Stepper { value, on_decrement: tok(on_decrement), on_increment: tok(on_increment) }
264}
265
266#[must_use]
268pub fn tab<E: Serialize>(label: impl Into<String>, selected: bool, on_select: E) -> Tab {
269 Tab { label: label.into(), selected, on_select: tok(on_select) }
270}
271
272#[must_use]
275pub fn scaffold(title: impl Into<String>, dark_mode: bool, tabs: Vec<Tab>, body: Widget) -> Widget {
276 Widget::Scaffold { title: title.into(), body: Box::new(body), tabs, back: None, dark_mode }
277}
278
279#[must_use]
282pub fn scaffold_back<E: Serialize>(title: impl Into<String>, dark_mode: bool, tabs: Vec<Tab>, body: Widget, back: E) -> Widget {
283 Widget::Scaffold { title: title.into(), body: Box::new(body), tabs, back: Some(tok(back)), dark_mode }
284}