Skip to main content

plushie_renderer_lib/
apply.rs

1//! Processes incoming protocol messages (snapshots, patches, settings,
2//! widget commands) by delegating to Core and handling resulting effects.
3
4use std::io;
5
6use plushie_renderer_engine::CoreEffect;
7use plushie_widget_sdk::protocol::IncomingMessage;
8
9use crate::App;
10
11impl App {
12    pub fn apply(&mut self, message: IncomingMessage) -> io::Result<()> {
13        // Widget commands bypass the normal tree update / diff / patch cycle.
14        // Route through the unified widget registry.
15        match &message {
16            IncomingMessage::Command { id, family, value } => {
17                // Route through execute_command which handles both
18                // built-in ops (focus, scroll, text cursor) and
19                // native widget commands (via registry fallback).
20                let task = self.execute_command(id, family, value);
21                self.pending_tasks.push(task);
22                return Ok(());
23            }
24            IncomingMessage::Commands { commands } => {
25                // Atomic batch: suppress outgoing events until all
26                // commands have been applied, then flush in order.
27                self.emitter.begin_batch();
28                for cmd in commands {
29                    let task = self.execute_command(&cmd.id, &cmd.family, &cmd.value);
30                    self.pending_tasks.push(task);
31                }
32                self.emitter.end_batch();
33                return Ok(());
34            }
35            _ => {}
36        }
37
38        let is_snapshot = matches!(message, IncomingMessage::Snapshot { .. });
39        let is_tree_change = matches!(
40            message,
41            IncomingMessage::Snapshot { .. } | IncomingMessage::Patch { .. }
42        );
43        let is_subscribe = matches!(message, IncomingMessage::Subscribe { .. });
44        let is_unsubscribe = matches!(message, IncomingMessage::Unsubscribe { .. });
45        let is_settings = matches!(message, IncomingMessage::Settings { .. });
46
47        if is_snapshot {
48            self.pending_tasks.push(self.emitter.flush());
49            self.emitter.clear_widget_rates();
50        }
51
52        let effects = self.core.apply(message);
53
54        if is_subscribe || is_settings {
55            self.sync_subscription_rates();
56        }
57        if is_subscribe || is_unsubscribe {
58            self.cleanup_subscription_rates();
59        }
60        for effect in effects {
61            use plushie_renderer_engine::{Dispatch, Emit, StateChange};
62            match effect {
63                CoreEffect::Emit(Emit::Event(event)) => self.emitter.emit_event(event)?,
64                CoreEffect::Emit(Emit::EffectResponse(response)) => {
65                    self.emitter.emit_effect_response(response)?;
66                }
67                CoreEffect::Emit(Emit::StubAck(ack)) => {
68                    let bytes = self.codec.encode(&ack).map_err(io::Error::other)?;
69                    self.emitter.write_raw(&bytes)?;
70                }
71                CoreEffect::Dispatch(Dispatch::Effect {
72                    request_id,
73                    kind,
74                    payload,
75                }) => {
76                    match plushie_core::ops::validate_effect_request_from_wire(&kind, &payload) {
77                        Ok(request) => {
78                            if self.effect_handler.is_async(&request) {
79                                let future = self.effect_handler.handle_async(request_id, request);
80                                let sink = self.emitter.sink();
81                                let task = plushie_widget_sdk::iced::Task::perform(
82                                    future,
83                                    move |response| {
84                                        // sink lock is the innermost; no
85                                        // nested locks in this continuation.
86                                        let mut guard = sink.lock();
87                                        if let Err(e) = guard.emit_effect_response(response) {
88                                            log::error!("effect response write error: {e}");
89                                        }
90                                        plushie_widget_sdk::runtime::Message::NoOp
91                                    },
92                                );
93                                self.pending_tasks.push(task);
94                            } else if let Some(response) =
95                                self.effect_handler.handle_sync(&request_id, &request)
96                            {
97                                self.emitter.emit_effect_response(response)?;
98                            }
99                        }
100                        Err(err) if request_id.is_empty() => {
101                            log::warn!("invalid effect request without response id: {err}");
102                        }
103                        Err(err) => {
104                            log::warn!("invalid effect request: {err}");
105                            self.emitter.emit_effect_response(
106                                plushie_widget_sdk::protocol::EffectResponse::error(
107                                    request_id,
108                                    err.to_string(),
109                                ),
110                            )?;
111                        }
112                    }
113                }
114                CoreEffect::Dispatch(Dispatch::WidgetOp { op, payload }) => {
115                    let task = self.handle_widget_op(&op, &payload);
116                    self.pending_tasks.push(task);
117                }
118                CoreEffect::Dispatch(Dispatch::Window(op)) => {
119                    let task = self.dispatch_window_op(op);
120                    self.pending_tasks.push(task);
121                }
122                CoreEffect::Dispatch(Dispatch::WindowQuery(q)) => {
123                    let task = self.dispatch_window_query(q);
124                    self.pending_tasks.push(task);
125                }
126                CoreEffect::Dispatch(Dispatch::System(op)) => {
127                    let task = self.dispatch_system_op(op);
128                    self.pending_tasks.push(task);
129                }
130                CoreEffect::Dispatch(Dispatch::SystemQuery(q)) => {
131                    let task = self.dispatch_system_query(q);
132                    self.pending_tasks.push(task);
133                }
134                CoreEffect::Dispatch(Dispatch::Image {
135                    op,
136                    handle,
137                    data,
138                    pixels,
139                    width,
140                    height,
141                }) => {
142                    self.handle_image_op(&op, &handle, data, pixels, width, height);
143                }
144                CoreEffect::StateChange(StateChange::SyncWindows) => {
145                    let task = self.sync_windows();
146                    self.pending_tasks.push(task);
147                }
148                CoreEffect::StateChange(StateChange::ThemeChanged(theme, chrome)) => {
149                    self.theme = theme;
150                    self.theme_chrome = chrome;
151                    self.theme_follows_system = false;
152                }
153                CoreEffect::StateChange(StateChange::ThemeFollowsSystem) => {
154                    self.theme_chrome = plushie_widget_sdk::runtime::ThemeChrome::default();
155                    self.theme_follows_system = true;
156                }
157                CoreEffect::StateChange(StateChange::WidgetConfig(config)) => {
158                    let ctx = plushie_widget_sdk::registry::InitCtx {
159                        config: &config,
160                        theme: &self.theme,
161                        default_text_size: self.core.default_text_size,
162                        default_font: self.core.default_font,
163                    };
164                    self.registry.init_all(&ctx);
165                    for diag in self.registry.family_collision_diagnostics() {
166                        self.emitter.emit_event(diag)?;
167                    }
168                }
169                CoreEffect::StateChange(StateChange::ExitNodes(nodes)) => {
170                    for (parent_id, index, node) in nodes {
171                        self.transition_manager
172                            .ghosts
173                            .add_ghost(&parent_id, node, index);
174                    }
175                }
176            }
177        }
178
179        if is_tree_change {
180            self.windows.clear_theme_cache();
181            let window_ids = self.core.tree.window_ids();
182            log::debug!("window sync: {} windows", window_ids.len());
183            for win_id in window_ids {
184                if let Some(node) = self.core.tree.find_window(&win_id)
185                    && let Some(theme_val) = node.props.get_value("theme")
186                {
187                    match plushie_widget_sdk::runtime::resolve_theme_resolution(&theme_val) {
188                        plushie_widget_sdk::runtime::ThemeResolution::Theme(theme, chrome) => {
189                            self.windows.set_theme(&win_id, theme, chrome);
190                        }
191                        plushie_widget_sdk::runtime::ThemeResolution::System => {
192                            self.windows.set_theme_follows_system(&win_id);
193                        }
194                        plushie_widget_sdk::runtime::ThemeResolution::Invalid => {}
195                    }
196                }
197            }
198
199            if is_snapshot {
200                self.transition_manager.clear();
201            }
202            // Single depth-first walk drives both the widget-prepare
203            // pass and the animation-descriptor scan. Each concern is
204            // isolated behind its own `TreeTransform`.
205            let validate_props = self.core.is_validate_props_enabled();
206            if let Some(root) = self.core.tree.root_mut() {
207                self.registry.prepare_and_scan_with_validation(
208                    root,
209                    &mut self.core.caches,
210                    &self.theme,
211                    &mut self.transition_manager,
212                    validate_props,
213                );
214            }
215        }
216
217        Ok(())
218    }
219
220    pub(crate) fn sync_subscription_rates(&mut self) {
221        self.emitter.set_default_rate(self.core.default_event_rate);
222        for (tag, rate) in self.core.subscription_rates() {
223            self.emitter.set_subscription_rate(tag, rate);
224        }
225    }
226
227    pub(crate) fn cleanup_subscription_rates(&mut self) {
228        let active_rate_tags: std::collections::HashSet<String> = self
229            .core
230            .subscription_rate_tags()
231            .map(|s| s.to_string())
232            .collect();
233        let emitter_keys: Vec<String> = self
234            .emitter
235            .subscription_rate_keys()
236            .map(|s| s.to_string())
237            .collect();
238        for key in emitter_keys {
239            if !active_rate_tags.contains(&key) {
240                self.emitter.remove_subscription_rate(&key);
241                let task = self
242                    .emitter
243                    .flush_key(&crate::emitter::CoalesceKey::Subscription(key));
244                self.pending_tasks.push(task);
245            }
246        }
247    }
248}