1use std::sync::Arc;
8
9use iced::{Task, Theme, keyboard, window};
10
11use plushie_widget_sdk::protocol::OutgoingEvent;
12use plushie_widget_sdk::registry::WidgetRegistry;
13use plushie_widget_sdk::runtime::{Message, ThemeChrome};
14
15use crate::constants::*;
16use crate::effects::EffectHandler;
17use crate::emitter::{CoalesceKey, EventEmitter};
18use crate::emitters::SinkMutex;
19use crate::window_map;
20
21pub fn validate_scale_factor(sf: f32) -> f32 {
24 if sf <= 0.0 || !sf.is_finite() {
25 log::warn!("invalid scale_factor {sf}, using 1.0");
26 1.0
27 } else {
28 sf
29 }
30}
31
32pub struct App {
40 pub core: plushie_renderer_engine::Core,
41 pub theme: Theme,
42 pub theme_chrome: ThemeChrome,
43 pub pending_tasks: Vec<Task<Message>>,
47 pub windows: window_map::WindowMap,
49 pub image_registry: plushie_widget_sdk::image_registry::ImageRegistry,
51 pub system_theme: Theme,
53 pub theme_follows_system: bool,
55 pub scale_factor: f32,
57 pub registry: WidgetRegistry,
60 pub animation_epoch: Option<iced::time::Instant>,
62 pub emitter: EventEmitter,
64 pub effect_handler: Box<dyn EffectHandler>,
68 pub transition_manager: plushie_widget_sdk::animation::TransitionManager,
72 pub current_modifiers: keyboard::Modifiers,
75 pub codec: plushie_renderer_engine::Codec,
78}
79
80impl App {
81 pub fn new(
82 registry: WidgetRegistry,
83 effect_handler: Box<dyn EffectHandler>,
84 sink: Arc<SinkMutex>,
85 ) -> Self {
86 Self {
87 core: plushie_renderer_engine::Core::new(),
88 theme: DEFAULT_THEME,
89 theme_chrome: ThemeChrome::default(),
90 pending_tasks: Vec::new(),
91 windows: window_map::WindowMap::new(),
92 image_registry: plushie_widget_sdk::image_registry::ImageRegistry::new(),
93 system_theme: DEFAULT_THEME,
94 theme_follows_system: false,
95 scale_factor: 1.0,
96 registry,
97 animation_epoch: None,
98 emitter: EventEmitter::new(sink),
99 effect_handler,
100 transition_manager: plushie_widget_sdk::animation::TransitionManager::new(),
101 current_modifiers: keyboard::Modifiers::default(),
102 codec: plushie_renderer_engine::Codec::MsgPack,
103 }
104 }
105
106 pub fn set_codec(&mut self, codec: plushie_renderer_engine::Codec) {
109 self.codec = codec;
110 }
111
112 pub fn title_for_window(&self, iced_id: window::Id) -> String {
113 if let Some(window_id) = self.windows.get_window_id(&iced_id)
114 && let Some(node) = self.core.tree.find_window(window_id)
115 && let Some(title) = node.props.get_str("title")
116 {
117 return title.chars().filter(|c| !c.is_control()).collect();
118 }
119 DEFAULT_WINDOW_TITLE.to_string()
120 }
121
122 pub fn theme_for_window(&self, iced_id: window::Id) -> Theme {
123 self.theme_ref_for_window(iced_id).clone()
124 }
125
126 pub fn theme_ref_for_window(&self, iced_id: window::Id) -> &Theme {
127 if let Some(window_id) = self.windows.get_window_id(&iced_id)
128 && self.windows.theme_follows_system(window_id)
129 {
130 return &self.system_theme;
131 }
132 if let Some(window_id) = self.windows.get_window_id(&iced_id)
133 && let Some(cached) = self.windows.cached_theme(window_id)
134 {
135 return cached;
136 }
137 if self.theme_follows_system {
138 &self.system_theme
139 } else {
140 &self.theme
141 }
142 }
143
144 pub fn theme_chrome_for_window(&self, iced_id: window::Id) -> ThemeChrome {
145 if let Some(window_id) = self.windows.get_window_id(&iced_id)
146 && self.windows.theme_follows_system(window_id)
147 {
148 return ThemeChrome::default();
149 }
150 if let Some(window_id) = self.windows.get_window_id(&iced_id)
151 && let Some(chrome) = self.windows.cached_theme_chrome(window_id)
152 {
153 return chrome;
154 }
155 if self.theme_follows_system {
156 ThemeChrome::default()
157 } else {
158 self.theme_chrome
159 }
160 }
161
162 pub fn scale_factor_for_window(&self, iced_id: window::Id) -> f32 {
163 let window_id = self.windows.get_window_id(&iced_id);
164
165 if let Some(sf) = window_id.and_then(|jid| self.windows.scale_factor(jid)) {
167 return validate_scale_factor(sf);
168 }
169
170 let sf = window_id
172 .and_then(|jid| self.core.tree.find_window(jid))
173 .and_then(|node| node.props.get_f32("scale_factor"))
174 .unwrap_or(self.scale_factor);
175 validate_scale_factor(sf)
176 }
177
178 pub fn emit_subscription(
186 &self,
187 key: &str,
188 captured: bool,
189 event_fn: impl Fn(&str) -> OutgoingEvent,
190 ) -> Task<Message> {
191 self.emit_subscription_for_window(key, None, captured, event_fn)
192 }
193
194 pub fn emit_subscription_for_window(
196 &self,
197 key: &str,
198 window_id: Option<&str>,
199 captured: bool,
200 event_fn: impl Fn(&str) -> OutgoingEvent,
201 ) -> Task<Message> {
202 let entries = self
203 .core
204 .matching_entries_with_catchall(key, SUB_EVENT, window_id);
205 match entries.len() {
208 0 => Task::none(),
209 1 => {
210 let entry = &entries[0];
211 self.emitter
212 .emit_direct(event_fn(entry.tag.as_str()).with_captured(captured))
213 }
214 _ => {
215 let tasks: Vec<_> = entries
216 .into_iter()
217 .map(|entry| {
218 self.emitter
219 .emit_direct(event_fn(entry.tag.as_str()).with_captured(captured))
220 })
221 .collect();
222 Task::batch(tasks)
223 }
224 }
225 }
226
227 pub fn lookup_widget_event_rate(&self, widget_id: &str) -> Option<u32> {
228 let node = self.core.tree.find_by_id(widget_id)?;
229 node.props
230 .get("event_rate")
231 .and_then(|v| v.as_u64())
232 .map(|v| v as u32)
233 }
234
235 pub fn is_widget_disabled_for_interception(&self, widget_id: &str) -> bool {
247 let Some(node) = self.core.tree.find_by_id(widget_id) else {
248 return false;
249 };
250 if !matches!(
251 node.type_name.as_str(),
252 "text_input" | "text_editor" | "combo_box" | "pick_list"
253 ) {
254 return false;
255 }
256 node.props
257 .get("disabled")
258 .and_then(|v| v.as_bool())
259 .unwrap_or(false)
260 }
261
262 pub fn coalesce_subscription(
264 &mut self,
265 key: &str,
266 captured: bool,
267 event_fn: impl Fn(&str) -> OutgoingEvent,
268 ) -> Task<Message> {
269 coalesce_subscription_into(&self.core, &mut self.emitter, key, None, captured, event_fn)
270 }
271
272 pub fn coalesce_subscription_for_window(
276 &mut self,
277 key: &str,
278 window_id: Option<&str>,
279 captured: bool,
280 event_fn: impl Fn(&str) -> OutgoingEvent,
281 ) -> Task<Message> {
282 coalesce_subscription_into(
283 &self.core,
284 &mut self.emitter,
285 key,
286 window_id,
287 captured,
288 event_fn,
289 )
290 }
291
292 pub fn dispatch_widget_subscription(
300 &mut self,
301 kind: &str,
302 window_id: Option<&str>,
303 msg: &Message,
304 ) -> Task<Message> {
305 dispatch_widget_subscription_into(
306 &mut self.registry,
307 &mut self.emitter,
308 kind,
309 window_id,
310 msg,
311 )
312 }
313}
314
315pub(crate) fn dispatch_widget_subscription_into(
320 registry: &mut WidgetRegistry,
321 emitter: &mut EventEmitter,
322 kind: &str,
323 window_id: Option<&str>,
324 msg: &Message,
325) -> Task<Message> {
326 if !registry.has_widget_subscription(kind) {
327 return Task::none();
328 }
329 let events = registry.dispatch_widget_subscription(kind, window_id, msg);
330 if events.is_empty() {
331 return Task::none();
332 }
333 let tasks: Vec<_> = events
334 .into_iter()
335 .map(|event| emitter.emit_immediate(event))
336 .collect();
337 Task::batch(tasks)
338}
339
340pub(crate) fn coalesce_subscription_into(
347 core: &plushie_renderer_engine::Core,
348 emitter: &mut EventEmitter,
349 key: &str,
350 window_id: Option<&str>,
351 captured: bool,
352 event_fn: impl Fn(&str) -> OutgoingEvent,
353) -> Task<Message> {
354 let entries = core.matching_entries_with_catchall(key, SUB_EVENT, window_id);
355 match entries.len() {
363 0 => Task::none(),
364 1 => {
365 let entry = &entries[0];
366 let event = event_fn(entry.tag.as_str()).with_captured(captured);
367 emitter.coalesce(CoalesceKey::Subscription(entry.tag.clone()), event)
368 }
369 _ => {
370 let tasks: Vec<_> = entries
371 .into_iter()
372 .map(|entry| {
373 let event = event_fn(entry.tag.as_str()).with_captured(captured);
374 emitter.coalesce(CoalesceKey::Subscription(entry.tag.clone()), event)
375 })
376 .collect();
377 Task::batch(tasks)
378 }
379 }
380}