1use iced::{Task, Theme, window};
5
6use plushie_widget_sdk::protocol::{IncomingMessage, OutgoingEvent};
7use plushie_widget_sdk::runtime::{Message, StdinEvent};
8
9use crate::App;
10use crate::constants::*;
11use crate::emitter::CoalesceKey;
12
13fn incoming_requires_flush(message: &IncomingMessage) -> bool {
20 match message {
21 IncomingMessage::Subscribe { .. }
22 | IncomingMessage::Unsubscribe { .. }
23 | IncomingMessage::RegisterEffectStub { .. }
24 | IncomingMessage::UnregisterEffectStub { .. } => false,
25 IncomingMessage::Snapshot { .. }
26 | IncomingMessage::Patch { .. }
27 | IncomingMessage::Effect { .. }
28 | IncomingMessage::WidgetOp { .. }
29 | IncomingMessage::WindowOp { .. }
30 | IncomingMessage::SystemOp { .. }
31 | IncomingMessage::SystemQuery { .. }
32 | IncomingMessage::Settings { .. }
33 | IncomingMessage::Query { .. }
34 | IncomingMessage::Interact { .. }
35 | IncomingMessage::TreeHash { .. }
36 | IncomingMessage::Screenshot { .. }
37 | IncomingMessage::Reset { .. }
38 | IncomingMessage::ImageOp { .. }
39 | IncomingMessage::Command { .. }
40 | IncomingMessage::Commands { .. }
41 | IncomingMessage::AdvanceFrame { .. }
42 | IncomingMessage::LoadFont { .. } => true,
43 }
44}
45
46fn emit_write_failure(emitter: &crate::emitter::EventEmitter, where_: &str, err: &std::io::Error) {
52 let event = OutgoingEvent::generic(
53 "session_error",
54 "",
55 Some(serde_json::json!({
56 "code": "renderer_write_failed",
57 "where": where_,
58 "error": err.to_string(),
59 })),
60 );
61 let _ = emitter.emit_event(event);
62}
63
64impl App {
65 pub fn update(&mut self, message: Message) -> Task<Message> {
66 let widget_task = self.dispatch_to_widget_subscribers(&message);
72 let host_task = self.update_inner(message);
73 Task::batch([widget_task, host_task])
74 }
75
76 fn update_inner(&mut self, message: Message) -> Task<Message> {
77 match message {
78 Message::Stdin(event) => self.handle_stdin(event),
79 Message::NoOp | Message::TimerTick(_) => Task::none(),
80 Message::FlushCoalesce => self.emitter.flush(),
81
82 msg @ (Message::CanvasElementFocusChanged { .. }
91 | Message::Diagnostic { .. }
92 | Message::TextEditorAction(..)
93 | Message::Event { .. }
94 | Message::PaneFocusCycle(..)
95 | Message::PaneResized(..)
96 | Message::PaneDragged(..)
97 | Message::PaneClicked(..)) => {
98 let disabled_target: Option<&str> = match &msg {
107 Message::Event { id, .. } => Some(id.as_str()),
108 Message::TextEditorAction(_, id, _) => Some(id.as_str()),
109 _ => None,
110 };
111 if let Some(id) = disabled_target
112 && self.is_widget_disabled_for_interception(id)
113 {
114 log::trace!("disabled widget {id} suppressed event");
115 return Task::none();
116 }
117
118 let events = self.registry.process_message(&msg);
119 let mut task = Task::none();
120 for event in events {
121 let t = if event.coalesce_hint().is_some() {
122 if !event.id.is_empty()
124 && !self.emitter.has_widget_rate(&event.id)
125 && let Some(rate) = self.lookup_widget_event_rate(&event.id)
126 {
127 self.emitter.set_widget_rate(&event.id, rate);
128 }
129 let key = crate::emitter::widget_coalesce_key(&event);
130 self.emitter.coalesce(key, event)
131 } else {
132 self.emitter.emit_immediate(event)
133 };
134 task = Task::batch([task, t]);
135 }
136 task
137 }
138
139 Message::MarkdownUrl(url) => {
140 log::debug!("markdown link clicked: {url}");
141 Task::none()
142 }
143
144 Message::KeyPressed(data, iced_id) => self.handle_key_pressed(data, iced_id),
146 Message::KeyReleased(data, iced_id) => self.handle_key_released(data, iced_id),
147 Message::ModifiersChanged(mods, iced_id, captured) => {
148 self.handle_modifiers_changed(mods, iced_id, captured)
149 }
150
151 Message::CursorMoved(pos, iced_id, captured) => {
153 self.handle_cursor_moved(pos, iced_id, captured)
154 }
155 Message::CursorEntered(iced_id, captured) => {
156 self.handle_cursor_entered(iced_id, captured)
157 }
158 Message::CursorLeft(iced_id, captured) => self.handle_cursor_left(iced_id, captured),
159 Message::MouseButtonPressed(button, iced_id, captured) => {
160 self.handle_mouse_button_pressed(button, iced_id, captured)
161 }
162 Message::MouseButtonReleased(button, iced_id, captured) => {
163 self.handle_mouse_button_released(button, iced_id, captured)
164 }
165 Message::WheelScrolled(delta, iced_id, captured) => {
166 self.handle_wheel_scrolled(delta, iced_id, captured)
167 }
168
169 Message::FingerPressed(finger, pos, iced_id, captured) => {
171 self.handle_finger_pressed(finger, pos, iced_id, captured)
172 }
173 Message::FingerMoved(finger, pos, iced_id, captured) => {
174 self.handle_finger_moved(finger, pos, iced_id, captured)
175 }
176 Message::FingerLifted(finger, pos, iced_id, captured) => {
177 self.handle_finger_lifted(finger, pos, iced_id, captured)
178 }
179 Message::FingerLost(finger, pos, iced_id, captured) => {
180 self.handle_finger_lost(finger, pos, iced_id, captured)
181 }
182
183 Message::ImeOpened(iced_id, captured) => self.handle_ime_opened(iced_id, captured),
185 Message::ImePreedit(text, cursor, iced_id, captured) => {
186 self.handle_ime_preedit(text, cursor, iced_id, captured)
187 }
188 Message::ImeCommit(text, iced_id, captured) => {
189 self.handle_ime_commit(text, iced_id, captured)
190 }
191 Message::ImeClosed(iced_id, captured) => self.handle_ime_closed(iced_id, captured),
192
193 Message::WindowCloseRequested(iced_id) => {
195 let Some(window_id) = self.windows.get_window_id(&iced_id) else {
200 return Task::none();
201 };
202 let entries = self
203 .core
204 .matching_entries(SUB_WINDOW_CLOSE, Some(window_id));
205 if entries.is_empty() {
206 return Task::none();
207 }
208 let owned_window_id = window_id.to_string();
209 let tasks: Vec<_> = entries
210 .into_iter()
211 .map(|entry| {
212 self.emitter
213 .emit_direct(OutgoingEvent::window_close_requested(
214 entry.tag.as_str(),
215 owned_window_id.as_str(),
216 ))
217 })
218 .collect();
219 Task::batch(tasks)
220 }
221 Message::WindowClosed(iced_id) => {
222 if let Some(window_id) = self.windows.remove_by_iced(&iced_id) {
223 let wid = Some(window_id.as_str());
224 let tasks: Vec<_> = self
225 .core
226 .matching_entries(SUB_WINDOW_EVENT, wid)
227 .into_iter()
228 .map(|entry| {
229 self.emitter.emit_direct(OutgoingEvent::window_closed(
230 entry.tag.as_str(),
231 window_id.as_str(),
232 ))
233 })
234 .collect();
235 if !tasks.is_empty() {
236 return Task::batch(tasks);
237 }
238 log::info!("window closed: {window_id}");
239 }
240 if self.windows.is_empty() && self.core.tree.root().is_some() {
245 log::info!("all windows closed, notifying host");
246 return self.emitter.emit_direct(OutgoingEvent::generic(
247 "all_windows_closed".to_string(),
248 String::new(),
249 None,
250 ));
251 }
252 Task::none()
253 }
254 Message::WindowOpened(iced_id, window_id) => {
255 log::info!("window opened: {window_id} -> {iced_id:?}");
256 self.windows.insert(window_id, iced_id);
257 Task::none()
258 }
259 Message::WindowEvent(iced_id, evt) => self.handle_window_event(iced_id, evt),
260
261 Message::AnimationFrame(instant) => {
263 let completions = self
265 .transition_manager
266 .advance_all(instant, &mut self.core.caches.interpolated_props);
267
268 for c in completions {
270 let event = OutgoingEvent::generic(
271 "transition_complete",
272 c.widget_id.clone(),
273 Some(serde_json::json!({
274 "tag": c.tag,
275 "prop": c.prop_name,
276 })),
277 );
278 let _ = self.emitter.emit_immediate(event);
279 }
280
281 let entries = self.core.matching_entries(SUB_ANIMATION_FRAME, None);
283 if let Some(entry) = entries.first() {
284 let epoch = *self.animation_epoch.get_or_insert(instant);
285 let millis = u64::try_from(instant.duration_since(epoch).as_millis())
286 .unwrap_or(u64::MAX);
287 let event = OutgoingEvent::animation_frame(entry.tag.as_str(), millis);
288 self.emitter.coalesce(
289 CoalesceKey::Subscription(SUB_ANIMATION_FRAME.to_string()),
290 event,
291 )
292 } else {
293 Task::none()
294 }
295 }
296 Message::ThemeChanged(mode) => {
297 self.system_theme = match mode {
299 iced::theme::Mode::Light => Theme::Light,
300 iced::theme::Mode::Dark => Theme::Dark,
301 _ => Theme::Dark,
302 };
303 let entries = self.core.matching_entries(SUB_THEME_CHANGE, None);
305 if let Some(entry) = entries.first() {
306 let mode_str = match mode {
307 iced::theme::Mode::Light => "light",
308 iced::theme::Mode::Dark => "dark",
309 _ => "system",
310 };
311 let event = OutgoingEvent::theme_changed(entry.tag.as_str(), mode_str);
312 self.emitter.coalesce(
313 CoalesceKey::Subscription(SUB_THEME_CHANGE.to_string()),
314 event,
315 )
316 } else {
317 Task::none()
318 }
319 }
320 }
321 }
322
323 fn dispatch_to_widget_subscribers(&mut self, message: &Message) -> Task<Message> {
337 let (kind, catchall, window_id): (&str, bool, Option<&str>) = match message {
341 Message::KeyPressed(_, iced_id) => {
342 (SUB_KEY_PRESS, true, self.windows.get_window_id(iced_id))
343 }
344 Message::KeyReleased(_, iced_id) => {
345 (SUB_KEY_RELEASE, true, self.windows.get_window_id(iced_id))
346 }
347 Message::ModifiersChanged(_, iced_id, _) => (
348 SUB_MODIFIERS_CHANGED,
349 true,
350 self.windows.get_window_id(iced_id),
351 ),
352 Message::CursorMoved(_, iced_id, _)
353 | Message::CursorEntered(iced_id, _)
354 | Message::CursorLeft(iced_id, _) => {
355 (SUB_POINTER_MOVE, true, self.windows.get_window_id(iced_id))
356 }
357 Message::MouseButtonPressed(_, iced_id, _)
358 | Message::MouseButtonReleased(_, iced_id, _) => (
359 SUB_POINTER_BUTTON,
360 true,
361 self.windows.get_window_id(iced_id),
362 ),
363 Message::WheelScrolled(_, iced_id, _) => (
364 SUB_POINTER_SCROLL,
365 true,
366 self.windows.get_window_id(iced_id),
367 ),
368 Message::FingerPressed(_, _, iced_id, _)
369 | Message::FingerMoved(_, _, iced_id, _)
370 | Message::FingerLifted(_, _, iced_id, _)
371 | Message::FingerLost(_, _, iced_id, _) => {
372 (SUB_POINTER_TOUCH, true, self.windows.get_window_id(iced_id))
373 }
374 Message::ImeOpened(iced_id, _)
375 | Message::ImePreedit(_, _, iced_id, _)
376 | Message::ImeCommit(_, iced_id, _)
377 | Message::ImeClosed(iced_id, _) => {
378 (SUB_IME, true, self.windows.get_window_id(iced_id))
379 }
380 Message::WindowEvent(iced_id, _) => {
381 (SUB_WINDOW_EVENT, false, self.windows.get_window_id(iced_id))
382 }
383 Message::WindowCloseRequested(iced_id) => {
384 (SUB_WINDOW_CLOSE, false, self.windows.get_window_id(iced_id))
385 }
386 Message::AnimationFrame(_) => (SUB_ANIMATION_FRAME, false, None),
387 Message::ThemeChanged(_) => (SUB_THEME_CHANGE, false, None),
388 _ => return Task::none(),
389 };
390 let specific = crate::app::dispatch_widget_subscription_into(
394 &mut self.registry,
395 &mut self.emitter,
396 kind,
397 window_id,
398 message,
399 );
400 if catchall {
401 let event = crate::app::dispatch_widget_subscription_into(
402 &mut self.registry,
403 &mut self.emitter,
404 SUB_EVENT,
405 window_id,
406 message,
407 );
408 Task::batch([specific, event])
409 } else {
410 specific
411 }
412 }
413
414 pub fn handle_stdin(&mut self, event: StdinEvent) -> Task<Message> {
415 match event {
416 StdinEvent::Message(incoming) => {
417 let flush_task = if incoming_requires_flush(&incoming) {
431 self.emitter.flush()
432 } else {
433 Task::none()
434 };
435 let branch_task = match incoming {
438 IncomingMessage::Query {
439 id,
440 target,
441 selector,
442 } => {
443 if let Err(e) = crate::scripting::handle_query(
444 &self.emitter,
445 &self.codec,
446 &self.core,
447 id,
448 target,
449 selector,
450 ) {
451 log::error!("write error: {e}");
452 emit_write_failure(&self.emitter, "query", &e);
453 return iced::exit();
454 }
455 Task::none()
456 }
457 IncomingMessage::Interact {
458 id,
459 action,
460 selector,
461 payload,
462 } => {
463 if let Err(e) = crate::scripting::handle_interact(
464 &self.emitter,
465 &self.codec,
466 &self.core,
467 id,
468 action,
469 selector,
470 payload,
471 ) {
472 log::error!("write error: {e}");
473 emit_write_failure(&self.emitter, "interact", &e);
474 return iced::exit();
475 }
476 Task::none()
477 }
478 IncomingMessage::Reset { id } => {
479 let pre_reset_flush = self.emitter.flush();
481
482 if let Err(e) = crate::scripting::handle_reset(
484 &self.emitter,
485 &self.codec,
486 &mut self.core,
487 id,
488 ) {
489 log::error!("write error: {e}");
490 emit_write_failure(&self.emitter, "reset", &e);
491 return iced::exit();
492 }
493
494 let mut close_tasks: Vec<Task<Message>> = self
496 .windows
497 .iced_ids()
498 .map(|&iced_id| window::close(iced_id))
499 .collect();
500 close_tasks.push(pre_reset_flush);
501 self.windows.clear();
502
503 self.image_registry =
505 plushie_widget_sdk::image_registry::ImageRegistry::new();
506 self.theme = DEFAULT_THEME;
507 self.theme_follows_system = false;
508 self.scale_factor = 1.0;
509 self.pending_tasks.clear();
510 self.animation_epoch = None;
511 self.emitter = crate::emitter::EventEmitter::new(self.emitter.sink());
512
513 Task::batch(close_tasks)
514 }
515 IncomingMessage::TreeHash { id, name, .. } => {
516 if let Err(e) = crate::scripting::handle_tree_hash(
517 &self.emitter,
518 &self.codec,
519 &self.core,
520 id,
521 name,
522 ) {
523 log::error!("write error: {e}");
524 emit_write_failure(&self.emitter, "tree_hash", &e);
525 return iced::exit();
526 }
527 Task::none()
528 }
529 IncomingMessage::Screenshot { id, name, .. } => {
530 if let Some((_, &iced_id)) = self.windows.iter().next() {
532 let sink = self.emitter.sink();
533 window::screenshot(iced_id).map(move |shot| {
534 use sha2::{Digest, Sha256};
535 let rgba: &[u8] = &shot.rgba;
536 let mut hasher = Sha256::new();
537 hasher.update(rgba);
538 let hash = format!("{:x}", hasher.finalize());
539 let w = shot.size.width;
540 let h = shot.size.height;
541 let mut guard = sink.lock();
544 if let Err(e) =
545 guard.emit_screenshot_response(&id, &name, &hash, w, h, rgba)
546 {
547 log::error!("write error in screenshot: {e}");
548 }
549 Message::NoOp
550 })
551 } else {
552 if let Err(e) =
553 self.emitter
554 .emit_screenshot_response(&id, &name, "", 0, 0, &[])
555 {
556 log::error!("write error: {e}");
557 return iced::exit();
558 }
559 Task::none()
560 }
561 }
562 other => {
563 if let Err(e) = self.apply(other) {
564 log::error!("write error: {e}");
565 return iced::exit();
566 }
567 let tasks: Vec<Task<Message>> = self.pending_tasks.drain(..).collect();
568 Task::batch(tasks)
569 }
570 };
571 Task::batch([flush_task, branch_task])
572 }
573 StdinEvent::Warning(msg) => {
574 log::warn!("stdin warning: {msg}");
575 Task::none()
576 }
577 StdinEvent::Closed => {
578 log::info!("stdin closed, exiting");
579 iced::exit()
580 }
581 }
582 }
583}