plushie_renderer_lib/
apply.rs1use 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 match &message {
16 IncomingMessage::Command { id, family, value } => {
17 let task = self.execute_command(id, family, value);
21 self.pending_tasks.push(task);
22 return Ok(());
23 }
24 IncomingMessage::Commands { commands } => {
25 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 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 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}