1use iced::Task;
9
10use plushie_core::ops::*;
11use plushie_widget_sdk::runtime::Message;
12
13use crate::App;
14
15impl App {
16 pub fn execute(&mut self, op: RendererOp) -> Task<Message> {
21 use iced::widget::operation;
22
23 match op {
24 RendererOp::Command {
26 ref id,
27 ref family,
28 ref value,
29 } => self.execute_command(id, family, value),
30 RendererOp::Commands(commands) => {
31 self.emitter.begin_batch();
35 let tasks: Vec<_> = commands
36 .iter()
37 .map(|cmd| self.execute_command(&cmd.id, &cmd.family, &cmd.value))
38 .collect();
39 self.emitter.end_batch();
40 Task::batch(tasks)
41 }
42
43 RendererOp::FocusNext => operation::focus_next(),
45 RendererOp::FocusPrevious => operation::focus_previous(),
46 RendererOp::FocusNextWithin { scope } => {
47 operation::focus_next_within(iced::advanced::widget::Id::from(scope))
48 }
49 RendererOp::FocusPreviousWithin { scope } => {
50 operation::focus_previous_within(iced::advanced::widget::Id::from(scope))
51 }
52
53 RendererOp::Announce { text, .. } => iced::announce(text),
62
63 RendererOp::Window(op) => self.dispatch_window_op(op),
65 RendererOp::WindowQuery(query) => self.dispatch_window_query(query),
66
67 RendererOp::SystemOp(op) => self.dispatch_system_op(op),
69 RendererOp::SystemQuery(query) => self.dispatch_system_query(query),
70
71 RendererOp::Effect { tag, request, .. } => {
73 if self.effect_handler.is_async(&request) {
74 let future = self.effect_handler.handle_async(tag, request);
75 let sink = self.emitter.sink();
76 Task::perform(future, move |response| {
77 let mut guard = sink.lock();
81 if let Err(e) = guard.emit_effect_response(response) {
82 log::error!("effect response write error: {e}");
83 }
84 Message::NoOp
85 })
86 } else if let Some(response) = self.effect_handler.handle_sync(&tag, &request) {
87 if let Err(e) = self.emitter.emit_effect_response(response) {
88 log::error!("effect response write error: {e}");
89 return iced::exit();
90 }
91 Task::none()
92 } else {
93 Task::none()
94 }
95 }
96
97 RendererOp::Image(op) => self.execute_image_op(op),
99
100 RendererOp::LoadFont { family, bytes } => {
102 plushie_widget_sdk::fonts::register_loaded_family(&family);
103 iced::font::load(bytes).map(|_| Message::NoOp)
104 }
105
106 RendererOp::Subscribe {
108 kind,
109 tag,
110 max_rate,
111 window_id,
112 } => {
113 use plushie_widget_sdk::protocol::IncomingMessage;
114 self.core.apply(IncomingMessage::Subscribe {
115 kind,
116 tag,
117 window_id,
118 max_rate,
119 });
120 self.sync_subscription_rates();
121 self.cleanup_subscription_rates();
122 Task::none()
123 }
124 RendererOp::Unsubscribe { kind, tag } => {
125 use plushie_widget_sdk::protocol::IncomingMessage;
126 self.core.apply(IncomingMessage::Unsubscribe {
127 kind,
128 tag: Some(tag),
129 });
130 self.sync_subscription_rates();
131 self.cleanup_subscription_rates();
132 Task::none()
133 }
134
135 RendererOp::TreeHash { tag } => {
137 self.handle_widget_op("tree_hash", &serde_json::json!({"target": tag}))
138 }
139 RendererOp::FindFocused { tag } => {
140 self.handle_widget_op("find_focused", &serde_json::json!({"target": tag}))
141 }
142 RendererOp::AdvanceFrame { timestamp } => self.handle_widget_op(
143 "advance_frame",
144 &serde_json::json!({"timestamp": timestamp}),
145 ),
146 _ => Task::none(),
147 }
148 }
149
150 fn execute_image_op(&mut self, op: ImageOp) -> Task<Message> {
151 match op {
152 ImageOp::Create { handle, data } => {
153 self.handle_image_op("create_image", &handle, Some(data), None, None, None);
154 Task::none()
155 }
156 ImageOp::CreateRaw {
157 handle,
158 width,
159 height,
160 pixels,
161 } => {
162 self.handle_image_op(
163 "create_image",
164 &handle,
165 None,
166 Some(pixels),
167 Some(width),
168 Some(height),
169 );
170 Task::none()
171 }
172 ImageOp::Update { handle, data } => {
173 self.handle_image_op("update_image", &handle, Some(data), None, None, None);
174 Task::none()
175 }
176 ImageOp::UpdateRaw {
177 handle,
178 width,
179 height,
180 pixels,
181 } => {
182 self.handle_image_op(
183 "update_image",
184 &handle,
185 None,
186 Some(pixels),
187 Some(width),
188 Some(height),
189 );
190 Task::none()
191 }
192 ImageOp::Delete(handle) => {
193 self.handle_image_op("delete_image", &handle, None, None, None, None);
194 Task::none()
195 }
196 ImageOp::List { tag } => {
197 self.handle_widget_op("list_images", &serde_json::json!({"tag": tag}))
198 }
199 ImageOp::Clear => self.handle_widget_op("clear_images", &serde_json::json!({})),
200 _ => Task::none(),
201 }
202 }
203
204 pub(crate) fn execute_command(
209 &mut self,
210 id: &str,
211 family: &str,
212 value: &serde_json::Value,
213 ) -> Task<Message> {
214 use iced::widget::Id as WId;
215 use iced::widget::operation;
216
217 match family {
218 "focus" => {
219 if id.contains('/') {
220 self.registry
221 .handle_widget_op(id, "focus", &serde_json::json!({}));
222 let canvas_id = self
223 .registry
224 .get_for_node_id(id)
225 .map(|(_, matched)| matched.to_string())
226 .unwrap_or_else(|| id.to_string());
227 operation::focus::<Message>(WId::from(canvas_id))
228 } else {
229 operation::focus::<Message>(WId::from(id.to_string()))
230 }
231 }
232 "select_all" => operation::select_all(WId::from(id.to_string())),
233 "move_cursor_to_front" => operation::move_cursor_to_front(WId::from(id.to_string())),
234 "move_cursor_to_end" => operation::move_cursor_to_end(WId::from(id.to_string())),
235 "move_cursor_to" => {
236 let pos = value.as_u64().unwrap_or(0) as usize;
237 operation::move_cursor_to(WId::from(id.to_string()), pos)
238 }
239 "select_range" => {
240 let start = value.get("start_pos").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
241 let end = value.get("end_pos").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
242 operation::select_range(WId::from(id.to_string()), start, end)
243 }
244 "scroll_to" => {
245 let x = value.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
246 let y = value.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
247 operation::scroll_to(
248 WId::from(id.to_string()),
249 operation::AbsoluteOffset { x, y },
250 )
251 }
252 "scroll_by" => {
253 let x = value.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
254 let y = value.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
255 operation::scroll_by(
256 WId::from(id.to_string()),
257 operation::AbsoluteOffset { x, y },
258 )
259 }
260 "snap_to" => {
261 let x = value.get("x").and_then(|v| v.as_f64()).map(|v| v as f32);
262 let y = value.get("y").and_then(|v| v.as_f64()).map(|v| v as f32);
263 operation::snap_to(
264 WId::from(id.to_string()),
265 operation::RelativeOffset { x, y },
266 )
267 }
268 "snap_to_end" => operation::snap_to_end(WId::from(id.to_string())),
269 _ => {
272 self.registry.handle_widget_op(id, family, value);
273 Task::none()
274 }
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use std::future::Future;
283 use std::pin::Pin;
284 use std::sync::Arc;
285
286 use parking_lot::Mutex;
287 use plushie_core::ops::EffectRequest;
288 use plushie_widget_sdk::protocol::{DiagnosticMessage, EffectResponse, OutgoingEvent};
289
290 struct NullEffectHandler;
291
292 impl crate::effects::EffectHandler for NullEffectHandler {
293 fn handle_sync(&self, _: &str, _: &EffectRequest) -> Option<EffectResponse> {
294 None
295 }
296
297 fn handle_async(
298 &self,
299 _: String,
300 _: EffectRequest,
301 ) -> Pin<Box<dyn Future<Output = EffectResponse> + Send>> {
302 Box::pin(async { unreachable!() })
303 }
304
305 fn is_async(&self, _: &EffectRequest) -> bool {
306 false
307 }
308 }
309
310 struct NullSink;
311
312 impl crate::emitters::EventSink for NullSink {
313 fn emit_event(&mut self, _: OutgoingEvent) -> std::io::Result<()> {
314 Ok(())
315 }
316
317 fn emit_effect_response(&mut self, _: EffectResponse) -> std::io::Result<()> {
318 Ok(())
319 }
320
321 fn emit_query_response(
322 &mut self,
323 _: &str,
324 _: &str,
325 _: &serde_json::Value,
326 ) -> std::io::Result<()> {
327 Ok(())
328 }
329
330 fn emit_screenshot_response(
331 &mut self,
332 _: &str,
333 _: &str,
334 _: &str,
335 _: u32,
336 _: u32,
337 _: &[u8],
338 ) -> std::io::Result<()> {
339 Ok(())
340 }
341
342 fn emit_hello(
343 &mut self,
344 _: &str,
345 _: &str,
346 _: &[&str],
347 _: &[&str],
348 _: &str,
349 ) -> std::io::Result<()> {
350 Ok(())
351 }
352
353 fn emit_diagnostic(&mut self, _: DiagnosticMessage) -> std::io::Result<()> {
354 Ok(())
355 }
356
357 fn write_raw(&mut self, _: &[u8]) -> std::io::Result<()> {
358 Ok(())
359 }
360 }
361
362 fn test_app() -> App {
363 let sink = Arc::new(Mutex::new(
364 Box::new(NullSink) as Box<dyn crate::emitters::EventSink>
365 ));
366 App::new(
367 plushie_widget_sdk::registry::WidgetRegistry::new(),
368 Box::new(NullEffectHandler),
369 sink,
370 )
371 }
372
373 #[test]
374 fn execute_subscribe_updates_emitter_rate() {
375 let mut app = test_app();
376
377 let _ = app.execute(RendererOp::Subscribe {
378 kind: "on_pointer_move".to_string(),
379 tag: "on_pointer_move".to_string(),
380 max_rate: Some(30),
381 window_id: None,
382 });
383
384 assert_eq!(
385 app.emitter.subscription_rate_for("on_pointer_move"),
386 Some(30)
387 );
388 }
389
390 #[test]
391 fn execute_unsubscribe_removes_emitter_rate() {
392 let mut app = test_app();
393
394 let _ = app.execute(RendererOp::Subscribe {
395 kind: "on_pointer_move".to_string(),
396 tag: "on_pointer_move".to_string(),
397 max_rate: Some(30),
398 window_id: None,
399 });
400 let _ = app.execute(RendererOp::Unsubscribe {
401 kind: "on_pointer_move".to_string(),
402 tag: "on_pointer_move".to_string(),
403 });
404
405 assert_eq!(app.emitter.subscription_rate_for("on_pointer_move"), None);
406 }
407
408 #[test]
409 fn execute_subscribe_without_rate_removes_existing_rate() {
410 let mut app = test_app();
411
412 let _ = app.execute(RendererOp::Subscribe {
413 kind: "on_pointer_move".to_string(),
414 tag: "on_pointer_move".to_string(),
415 max_rate: Some(30),
416 window_id: None,
417 });
418 let _ = app.execute(RendererOp::Subscribe {
419 kind: "on_pointer_move".to_string(),
420 tag: "on_pointer_move".to_string(),
421 max_rate: None,
422 window_id: None,
423 });
424
425 assert_eq!(app.emitter.subscription_rate_for("on_pointer_move"), None);
426 }
427}