use std::sync::Arc;
use iced::{Task, Theme, keyboard, window};
use plushie_widget_sdk::protocol::OutgoingEvent;
use plushie_widget_sdk::registry::WidgetRegistry;
use plushie_widget_sdk::runtime::{Message, ThemeChrome};
use crate::constants::*;
use crate::effects::EffectHandler;
use crate::emitter::{CoalesceKey, EventEmitter};
use crate::emitters::SinkMutex;
use crate::window_map;
pub fn validate_scale_factor(sf: f32) -> f32 {
if sf <= 0.0 || !sf.is_finite() {
log::warn!("invalid scale_factor {sf}, using 1.0");
1.0
} else {
sf
}
}
pub struct App {
pub core: plushie_renderer_engine::Core,
pub theme: Theme,
pub theme_chrome: ThemeChrome,
pub pending_tasks: Vec<Task<Message>>,
pub windows: window_map::WindowMap,
pub image_registry: plushie_widget_sdk::image_registry::ImageRegistry,
pub system_theme: Theme,
pub theme_follows_system: bool,
pub scale_factor: f32,
pub registry: WidgetRegistry,
pub animation_epoch: Option<iced::time::Instant>,
pub emitter: EventEmitter,
pub effect_handler: Box<dyn EffectHandler>,
pub transition_manager: plushie_widget_sdk::animation::TransitionManager,
pub current_modifiers: keyboard::Modifiers,
pub codec: plushie_renderer_engine::Codec,
}
impl App {
pub fn new(
registry: WidgetRegistry,
effect_handler: Box<dyn EffectHandler>,
sink: Arc<SinkMutex>,
) -> Self {
Self {
core: plushie_renderer_engine::Core::new(),
theme: DEFAULT_THEME,
theme_chrome: ThemeChrome::default(),
pending_tasks: Vec::new(),
windows: window_map::WindowMap::new(),
image_registry: plushie_widget_sdk::image_registry::ImageRegistry::new(),
system_theme: DEFAULT_THEME,
theme_follows_system: false,
scale_factor: 1.0,
registry,
animation_epoch: None,
emitter: EventEmitter::new(sink),
effect_handler,
transition_manager: plushie_widget_sdk::animation::TransitionManager::new(),
current_modifiers: keyboard::Modifiers::default(),
codec: plushie_renderer_engine::Codec::MsgPack,
}
}
pub fn set_codec(&mut self, codec: plushie_renderer_engine::Codec) {
self.codec = codec;
}
pub fn title_for_window(&self, iced_id: window::Id) -> String {
if let Some(window_id) = self.windows.get_window_id(&iced_id)
&& let Some(node) = self.core.tree.find_window(window_id)
&& let Some(title) = node.props.get_str("title")
{
return title.chars().filter(|c| !c.is_control()).collect();
}
DEFAULT_WINDOW_TITLE.to_string()
}
pub fn theme_for_window(&self, iced_id: window::Id) -> Theme {
self.theme_ref_for_window(iced_id).clone()
}
pub fn theme_ref_for_window(&self, iced_id: window::Id) -> &Theme {
if let Some(window_id) = self.windows.get_window_id(&iced_id)
&& self.windows.theme_follows_system(window_id)
{
return &self.system_theme;
}
if let Some(window_id) = self.windows.get_window_id(&iced_id)
&& let Some(cached) = self.windows.cached_theme(window_id)
{
return cached;
}
if self.theme_follows_system {
&self.system_theme
} else {
&self.theme
}
}
pub fn theme_chrome_for_window(&self, iced_id: window::Id) -> ThemeChrome {
if let Some(window_id) = self.windows.get_window_id(&iced_id)
&& self.windows.theme_follows_system(window_id)
{
return ThemeChrome::default();
}
if let Some(window_id) = self.windows.get_window_id(&iced_id)
&& let Some(chrome) = self.windows.cached_theme_chrome(window_id)
{
return chrome;
}
if self.theme_follows_system {
ThemeChrome::default()
} else {
self.theme_chrome
}
}
pub fn scale_factor_for_window(&self, iced_id: window::Id) -> f32 {
let window_id = self.windows.get_window_id(&iced_id);
if let Some(sf) = window_id.and_then(|jid| self.windows.scale_factor(jid)) {
return validate_scale_factor(sf);
}
let sf = window_id
.and_then(|jid| self.core.tree.find_window(jid))
.and_then(|node| node.props.get_f32("scale_factor"))
.unwrap_or(self.scale_factor);
validate_scale_factor(sf)
}
pub fn emit_subscription(
&self,
key: &str,
captured: bool,
event_fn: impl Fn(&str) -> OutgoingEvent,
) -> Task<Message> {
self.emit_subscription_for_window(key, None, captured, event_fn)
}
pub fn emit_subscription_for_window(
&self,
key: &str,
window_id: Option<&str>,
captured: bool,
event_fn: impl Fn(&str) -> OutgoingEvent,
) -> Task<Message> {
let entries = self
.core
.matching_entries_with_catchall(key, SUB_EVENT, window_id);
match entries.len() {
0 => Task::none(),
1 => {
let entry = &entries[0];
self.emitter
.emit_direct(event_fn(entry.tag.as_str()).with_captured(captured))
}
_ => {
let tasks: Vec<_> = entries
.into_iter()
.map(|entry| {
self.emitter
.emit_direct(event_fn(entry.tag.as_str()).with_captured(captured))
})
.collect();
Task::batch(tasks)
}
}
}
pub fn lookup_widget_event_rate(&self, widget_id: &str) -> Option<u32> {
let node = self.core.tree.find_by_id(widget_id)?;
node.props
.get("event_rate")
.and_then(|v| v.as_u64())
.map(|v| v as u32)
}
pub fn is_widget_disabled_for_interception(&self, widget_id: &str) -> bool {
let Some(node) = self.core.tree.find_by_id(widget_id) else {
return false;
};
if !matches!(
node.type_name.as_str(),
"text_input" | "text_editor" | "combo_box" | "pick_list"
) {
return false;
}
node.props
.get("disabled")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
pub fn coalesce_subscription(
&mut self,
key: &str,
captured: bool,
event_fn: impl Fn(&str) -> OutgoingEvent,
) -> Task<Message> {
coalesce_subscription_into(&self.core, &mut self.emitter, key, None, captured, event_fn)
}
pub fn coalesce_subscription_for_window(
&mut self,
key: &str,
window_id: Option<&str>,
captured: bool,
event_fn: impl Fn(&str) -> OutgoingEvent,
) -> Task<Message> {
coalesce_subscription_into(
&self.core,
&mut self.emitter,
key,
window_id,
captured,
event_fn,
)
}
pub fn dispatch_widget_subscription(
&mut self,
kind: &str,
window_id: Option<&str>,
msg: &Message,
) -> Task<Message> {
dispatch_widget_subscription_into(
&mut self.registry,
&mut self.emitter,
kind,
window_id,
msg,
)
}
}
pub(crate) fn dispatch_widget_subscription_into(
registry: &mut WidgetRegistry,
emitter: &mut EventEmitter,
kind: &str,
window_id: Option<&str>,
msg: &Message,
) -> Task<Message> {
if !registry.has_widget_subscription(kind) {
return Task::none();
}
let events = registry.dispatch_widget_subscription(kind, window_id, msg);
if events.is_empty() {
return Task::none();
}
let tasks: Vec<_> = events
.into_iter()
.map(|event| emitter.emit_immediate(event))
.collect();
Task::batch(tasks)
}
pub(crate) fn coalesce_subscription_into(
core: &plushie_renderer_engine::Core,
emitter: &mut EventEmitter,
key: &str,
window_id: Option<&str>,
captured: bool,
event_fn: impl Fn(&str) -> OutgoingEvent,
) -> Task<Message> {
let entries = core.matching_entries_with_catchall(key, SUB_EVENT, window_id);
match entries.len() {
0 => Task::none(),
1 => {
let entry = &entries[0];
let event = event_fn(entry.tag.as_str()).with_captured(captured);
emitter.coalesce(CoalesceKey::Subscription(entry.tag.clone()), event)
}
_ => {
let tasks: Vec<_> = entries
.into_iter()
.map(|entry| {
let event = event_fn(entry.tag.as_str()).with_captured(captured);
emitter.coalesce(CoalesceKey::Subscription(entry.tag.clone()), event)
})
.collect();
Task::batch(tasks)
}
}
}