plushie_renderer_lib/
widget_ops.rs1use iced::Task;
7
8use plushie_widget_sdk::protocol::OutgoingEvent;
9use plushie_widget_sdk::runtime::Message;
10
11use crate::App;
12
13use crate::constants::{MAX_FONT_BYTES, MAX_LOADED_FONTS};
14
15impl App {
20 pub fn handle_widget_op(&mut self, op: &str, payload: &serde_json::Value) -> Task<Message> {
24 let get_target = || {
25 payload
26 .get("target")
27 .and_then(|v| v.as_str())
28 .unwrap_or_default()
29 .to_string()
30 };
31
32 match op {
33 "focus" => {
34 let target = get_target();
35 if target.contains('/') {
38 self.registry.handle_widget_op(&target, "focus", payload);
39 let canvas_id = self
41 .registry
42 .get_for_node_id(&target)
43 .map(|(_, matched)| matched.to_string())
44 .unwrap_or(target);
45 iced::widget::operation::focus::<Message>(iced::widget::Id::from(canvas_id))
46 } else {
47 iced::widget::operation::focus::<Message>(iced::widget::Id::from(target))
48 }
49 }
50 "focus_next" => iced::widget::operation::focus_next(),
51 "focus_previous" => iced::widget::operation::focus_previous(),
52 "focus_next_within" => {
53 let scope = payload
54 .get("scope")
55 .and_then(|v| v.as_str())
56 .unwrap_or_default()
57 .to_string();
58 iced::widget::operation::focus_next_within(iced::widget::Id::from(scope))
59 }
60 "focus_previous_within" => {
61 let scope = payload
62 .get("scope")
63 .and_then(|v| v.as_str())
64 .unwrap_or_default()
65 .to_string();
66 iced::widget::operation::focus_previous_within(iced::widget::Id::from(scope))
67 }
68 "scroll_to" => {
69 let target = get_target();
70 let offset_x = payload
71 .get("offset_x")
72 .and_then(|v| v.as_f64())
73 .map(|v| v as f32);
74 let offset_y = payload
75 .get("offset_y")
76 .and_then(|v| v.as_f64())
77 .map(|v| v as f32);
78 iced::widget::operation::scroll_to(
79 iced::widget::Id::from(target),
80 iced::widget::operation::AbsoluteOffset {
81 x: offset_x.unwrap_or(0.0),
82 y: offset_y.unwrap_or(0.0),
83 },
84 )
85 }
86 "scroll_by" => {
87 let target = get_target();
88 let offset_x = payload
89 .get("offset_x")
90 .and_then(|v| v.as_f64())
91 .unwrap_or(0.0) as f32;
92 let offset_y = payload
93 .get("offset_y")
94 .and_then(|v| v.as_f64())
95 .unwrap_or(0.0) as f32;
96 iced::widget::operation::scroll_by(
97 iced::widget::Id::from(target),
98 iced::widget::operation::AbsoluteOffset {
99 x: offset_x,
100 y: offset_y,
101 },
102 )
103 }
104 "snap_to" => {
105 let target = get_target();
106 let x = payload.get("x").and_then(|v| v.as_f64()).map(|v| v as f32);
107 let y = payload.get("y").and_then(|v| v.as_f64()).map(|v| v as f32);
108 iced::widget::operation::snap_to(
109 iced::widget::Id::from(target),
110 iced::widget::operation::RelativeOffset { x, y },
111 )
112 }
113 "snap_to_end" => {
114 let target = get_target();
115 iced::widget::operation::snap_to_end(iced::widget::Id::from(target))
116 }
117 "select_all" => {
118 iced::widget::operation::select_all(iced::widget::Id::from(get_target()))
119 }
120 "select_range" => {
121 let target = get_target();
122 let start = payload.get("start").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
123 let end = payload.get("end").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
124 iced::widget::operation::select_range(iced::widget::Id::from(target), start, end)
125 }
126 "move_cursor_to_front" => {
127 iced::widget::operation::move_cursor_to_front(iced::widget::Id::from(get_target()))
128 }
129 "move_cursor_to_end" => {
130 iced::widget::operation::move_cursor_to_end(iced::widget::Id::from(get_target()))
131 }
132 "move_cursor_to" => {
133 let target = get_target();
134 let position = payload
135 .get("position")
136 .and_then(|v| v.as_u64())
137 .unwrap_or(0) as usize;
138 iced::widget::operation::move_cursor_to(iced::widget::Id::from(target), position)
139 }
140 "announce" => {
141 let text = payload
148 .get("text")
149 .and_then(|v| v.as_str())
150 .unwrap_or_default()
151 .to_string();
152 if let Some(p) = payload.get("politeness").and_then(|v| v.as_str()) {
153 log::trace!("announce politeness={p} (renderer routes to assertive)");
154 }
155 iced::announce(text)
156 }
157 "exit" => iced::exit(),
158 "pane_split" | "pane_close" | "pane_swap" | "pane_maximize" | "pane_restore" => {
161 let target = get_target();
162 self.registry.handle_widget_op(&target, op, payload);
163 Task::none()
164 }
165 "find_focused" => {
166 let tag = payload
167 .get("tag")
168 .and_then(|v| v.as_str())
169 .unwrap_or("find_focused")
170 .to_string();
171 let sink = self.emitter.sink();
172 iced::widget::operation::find_focused().map(move |maybe_id| {
173 let focused = maybe_id.map(|id| id.to_string());
174 let mut guard = sink.lock();
176 if let Err(e) = guard.emit_query_response(
177 "find_focused",
178 &tag,
179 &serde_json::json!({"focused": focused}),
180 ) {
181 log::error!("write error: {e}");
182 }
183 Message::NoOp
184 })
185 }
186 "load_font" => {
192 let family = payload
193 .get("family")
194 .and_then(|v| v.as_str())
195 .unwrap_or_default();
196 let data = payload
197 .get("data")
198 .and_then(crate::settings::decode_font_data)
199 .unwrap_or_default();
200 if family.is_empty() {
201 log::warn!("load_font: missing family name");
202 Task::none()
203 } else if data.is_empty() {
204 log::warn!("load_font: no font data provided for family {family}");
205 Task::none()
206 } else if data.len() > MAX_FONT_BYTES {
207 log::warn!(
208 "load_font: font data for {family} ({} bytes) exceeds {} byte limit, rejecting",
209 data.len(),
210 MAX_FONT_BYTES
211 );
212 Task::none()
213 } else if !crate::constants::try_reserve_font_slot() {
214 log::warn!(
215 "load_font: already loaded {MAX_LOADED_FONTS} fonts, \
216 rejecting to prevent unbounded memory growth"
217 );
218 Task::none()
219 } else {
220 plushie_widget_sdk::fonts::register_loaded_family(family);
221 let family_for_log = family.to_string();
222 iced::font::load(data).map(move |result| {
223 match result {
224 Ok(()) => log::info!("font {family_for_log} loaded successfully"),
225 Err(e) => {
226 log::error!("font {family_for_log} load failed: {e:?}")
227 }
228 }
229 Message::NoOp
230 })
231 }
232 }
233 "tree_hash" => {
234 let tag = payload
235 .get("tag")
236 .and_then(|v| v.as_str())
237 .unwrap_or("tree_hash")
238 .to_string();
239 let hash = self.core.tree_hash();
240 if let Err(e) = self.emitter.emit_query_response(
241 "tree_hash",
242 &tag,
243 &serde_json::json!({"hash": hash}),
244 ) {
245 log::error!("write error: {e}");
246 return iced::exit();
247 }
248 Task::none()
249 }
250 "list_images" => {
251 let tag = payload
252 .get("tag")
253 .and_then(|v| v.as_str())
254 .unwrap_or("list_images")
255 .to_string();
256 let handles: Vec<String> = self.image_registry.handle_names();
257 if let Err(e) = self.emitter.emit_query_response(
258 "list_images",
259 &tag,
260 &serde_json::json!({"handles": handles}),
261 ) {
262 log::error!("write error: {e}");
263 return iced::exit();
264 }
265 Task::none()
266 }
267 "clear_images" => {
268 self.image_registry.clear();
269 Task::none()
270 }
271 other => {
272 log::warn!("unknown widget_op: {other}");
273 Task::none()
274 }
275 }
276 }
277
278 pub fn handle_image_op(
285 &mut self,
286 op: &str,
287 handle: &str,
288 data: Option<Vec<u8>>,
289 pixels: Option<Vec<u8>>,
290 width: Option<u32>,
291 height: Option<u32>,
292 ) {
293 if let Err(error) = self
294 .image_registry
295 .apply_op(op, handle, data, pixels, width, height)
296 {
297 if let Err(e) = self.emitter.emit_event(OutgoingEvent::generic(
300 "image_error".to_string(),
301 handle.to_string(),
302 Some(serde_json::json!({ "error": error })),
303 )) {
304 log::error!("write error: {e}");
305 }
306 }
307 }
308}
309
310