use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::Mutex;
use plushie_widget_sdk::iced::{Element, Task, Theme};
use plushie_widget_sdk::protocol::TreeNode;
use plushie_widget_sdk::render_ctx::RenderCtx;
use plushie_widget_sdk::runtime::Message;
use plushie_widget_sdk::runtime::iced_widget_set;
use crate::App;
use crate::command::Command;
use crate::event::{EffectEvent, EffectResult, Event, WidgetEvent};
use crate::runtime::subscriptions::{SubOp, SubscriptionManager};
use crate::runtime::view_errors::{UpdateOutcome, ViewErrors, ViewOutcome};
use crate::widget::{EventResult as WidgetEventResult, Interception, WidgetStateStore};
use super::effect_tracker::{self, EffectTracker};
use super::queue_sink::{QueueSink, SinkEvent};
const DIRECT_ROOT_WINDOW_ID: &str = "main";
struct DirectApp<A: App> {
model: A::Model,
renderer: plushie_renderer_lib::App,
event_queue: Arc<Mutex<Vec<SinkEvent>>>,
current_tree: Option<TreeNode>,
window_sync: crate::runtime::windows::WindowSync,
widget_store: WidgetStateStore,
memo_cache: crate::runtime::MemoCache,
widget_view_cache: crate::runtime::WidgetViewCache,
running_tasks: HashMap<String, plushie_widget_sdk::iced::task::Handle>,
sub_manager: SubscriptionManager,
active_timers: HashMap<String, std::time::Duration>,
effect_tracker: EffectTracker,
view_errors: ViewErrors,
}
impl<A: App> DirectApp<A> {
fn init() -> (Self, Task<Message>) {
let (model, init_cmd) = A::init();
let builder =
plushie_widget_sdk::app::PlushieAppBuilder::<plushie_widget_sdk::iced::Renderer>::new()
.widget_set(&iced_widget_set());
let registry = builder.build();
let (sink, event_queue) = QueueSink::new();
plushie_renderer_lib::emitters::init_sink(Box::new(sink));
let sink_arc = plushie_renderer_lib::emitters::sink_arc();
let effect_handler = Box::new(plushie_renderer_lib::NativeEffectHandler);
let renderer = plushie_renderer_lib::App::new(registry, effect_handler, sink_arc);
let mut app = Self {
model,
renderer,
event_queue,
current_tree: None,
window_sync: crate::runtime::windows::WindowSync::new(),
widget_store: WidgetStateStore::new(),
memo_cache: crate::runtime::MemoCache::new(),
widget_view_cache: crate::runtime::WidgetViewCache::new(),
running_tasks: HashMap::new(),
sub_manager: SubscriptionManager::new(),
active_timers: HashMap::new(),
effect_tracker: EffectTracker::new(),
view_errors: ViewErrors::default(),
};
apply_settings::<A>(&mut app.renderer);
app.refresh_view();
let base_settings = build_direct_base_settings::<A>();
let win_task = app.sync_windows(&base_settings);
let mut init_tasks = vec![win_task];
let initial_subs = A::subscribe(&app.model);
for op in app.sub_manager.sync(initial_subs) {
init_tasks.push(app.apply_sub_op(op));
}
init_tasks.push(app.execute_command(init_cmd));
(app, Task::batch(init_tasks))
}
fn update(&mut self, msg: Message) -> Task<Message> {
if let Message::TimerTick(tag) = &msg {
self.handle_timer_tick(tag.clone());
}
let renderer_task = self.renderer.update(msg);
let app_task = self.drain_event_queue().unwrap_or_else(Task::none);
Task::batch([renderer_task, app_task])
}
fn view_window(
&self,
_window_id: plushie_widget_sdk::iced::window::Id,
) -> Element<'_, Message, Theme, plushie_widget_sdk::iced::Renderer> {
if let Some(tree) = &self.current_tree {
let ctx = RenderCtx {
caches: &self.renderer.core.caches,
images: &self.renderer.image_registry,
theme: &self.renderer.theme,
theme_chrome: self.renderer.theme_chrome,
registry: &self.renderer.registry,
default_text_size: self.renderer.core.default_text_size,
default_font: None,
window_id: DIRECT_ROOT_WINDOW_ID,
scale_factor: self.renderer.scale_factor,
validate_props: self.renderer.core.is_validate_props_enabled(),
};
plushie_widget_sdk::runtime::render(tree, ctx)
} else {
plushie_widget_sdk::iced::widget::text("No view").into()
}
}
fn title_for_window(&self, window_id: plushie_widget_sdk::iced::window::Id) -> String {
self.window_node_for(window_id)
.and_then(|node| node.props.get_str("title").map(str::to_string))
.unwrap_or_else(|| "Plushie".to_string())
}
fn theme_for_window(&self, window_id: plushie_widget_sdk::iced::window::Id) -> Theme {
if let Some(node) = self.window_node_for(window_id)
&& let Some(theme_val) = node.props.get_value("theme")
{
match plushie_widget_sdk::runtime::resolve_theme_resolution(&theme_val) {
plushie_widget_sdk::runtime::ThemeResolution::Theme(theme, _) => return theme,
plushie_widget_sdk::runtime::ThemeResolution::System => {
return self.renderer.system_theme.clone();
}
plushie_widget_sdk::runtime::ThemeResolution::Invalid => {}
}
}
if self.renderer.theme_follows_system {
self.renderer.system_theme.clone()
} else {
self.renderer.theme.clone()
}
}
fn scale_factor_for_window(&self, window_id: plushie_widget_sdk::iced::window::Id) -> f32 {
if let Some(node) = self.window_node_for(window_id)
&& let Some(sf) = node
.props
.get_value("scale_factor")
.and_then(|v| v.as_f64())
{
return plushie_renderer_lib::app::validate_scale_factor(sf as f32);
}
self.renderer.scale_factor
}
fn window_node_for(
&self,
window_id: plushie_widget_sdk::iced::window::Id,
) -> Option<&TreeNode> {
let tree = self.current_tree.as_ref()?;
let sdk_id = self.renderer.windows.get_window_id(&window_id)?;
find_window_node(tree, sdk_id)
}
fn drain_event_queue(&mut self) -> Option<Task<Message>> {
let events: Vec<SinkEvent> = {
let mut queue = self.event_queue.lock();
if queue.is_empty() {
return None;
}
std::mem::take(&mut *queue)
};
let mut tasks = Vec::new();
let mut delivered = false;
for sink_event in events {
if let SinkEvent::AsyncResult { tag, .. } = &sink_event {
self.running_tasks.remove(tag);
}
let sdk_event = match sink_event {
SinkEvent::EffectResponse(response) => self.resolve_effect_response(response),
other => super::event_bridge::sink_event_to_sdk(other),
};
if let Some(event) = sdk_event {
if let Some(task) = self.deliver_event(event) {
tasks.push(task);
}
delivered = true;
}
}
let timed_out = self.effect_tracker.check_timeouts();
for (tag, _kind) in timed_out {
let event = Event::Effect(EffectEvent {
tag,
result: EffectResult::Timeout,
});
if let Some(task) = self.deliver_event(event) {
tasks.push(task);
}
delivered = true;
}
if delivered {
self.refresh_view();
let base_settings = build_direct_base_settings::<A>();
let win_task = self.sync_windows(&base_settings);
tasks.push(win_task);
let new_subs = A::subscribe(&self.model);
let ops = self.sub_manager.sync(new_subs);
for op in ops {
tasks.push(self.apply_sub_op(op));
}
}
if tasks.is_empty() {
None
} else {
Some(Task::batch(tasks))
}
}
fn deliver_event(&mut self, event: Event) -> Option<Task<Message>> {
#[cfg(feature = "dev")]
{
if crate::dev::intercept_event(&event) {
return None;
}
}
match self.widget_store.intercept_event(&event) {
Some(Interception {
result: WidgetEventResult::Consumed,
..
}) => None,
Some(Interception {
result: WidgetEventResult::Emit { family, value },
widget_id,
outer_scope,
window_id,
}) => {
let new_event = Event::Widget(WidgetEvent {
event_type: crate::event::family_to_event_type(&family),
scoped_id: plushie_core::ScopedId::new(widget_id, outer_scope, Some(window_id)),
value,
});
#[cfg(feature = "dev")]
{
if crate::dev::intercept_event(&new_event) {
return None;
}
}
let cmd = self.guarded_update(new_event);
Some(self.execute_command(cmd))
}
Some(Interception {
result: WidgetEventResult::Ignored,
..
})
| None => {
let cmd = self.guarded_update(event);
Some(self.execute_command(cmd))
}
}
}
fn guarded_update(&mut self, event: Event) -> Command {
match crate::runtime::view_errors::run_guarded_update::<A>(
&mut self.view_errors,
&mut self.model,
event,
) {
UpdateOutcome::Ok(cmd) => cmd,
UpdateOutcome::Panicked { cmd, .. } => cmd,
}
}
fn resolve_effect_response(
&mut self,
response: plushie_widget_sdk::protocol::EffectResponse,
) -> Option<Event> {
let wire_id = &response.id;
match self.effect_tracker.resolve(wire_id) {
Some((tag, kind)) => {
let status = response.status;
log::debug!(
"direct effect response resolved: wire_id={wire_id} tag={tag} kind={kind} status={status}"
);
let error_as_value = response
.error
.as_ref()
.map(|e| serde_json::Value::String(e.clone()));
let value = response.result.as_ref().or(error_as_value.as_ref());
let result = EffectResult::parse(&kind, status, value);
Some(Event::Effect(EffectEvent { tag, result }))
}
None => {
log::warn!(
"effect response for unknown wire_id {wire_id}, \
falling back to bridge conversion"
);
Some(super::event_bridge::effect_response_to_sdk(response))
}
}
}
fn drain_effects_as_shutdown(&mut self) {
let pending = self.effect_tracker.pending_count();
if pending == 0 {
return;
}
log::info!("direct shutdown: flushing {pending} in-flight effect(s) as Shutdown");
for (tag, _kind) in self.effect_tracker.flush_all() {
let event = Event::Effect(crate::event::EffectEvent {
tag,
result: crate::event::EffectResult::Shutdown,
});
let _ = self.guarded_update(event);
}
}
fn sync_windows(&mut self, base_settings: &serde_json::Value) -> Task<Message> {
let Some(tree) = self.current_tree.as_ref() else {
return Task::none();
};
let ops = self.window_sync.sync(tree, base_settings);
let mut tasks = Vec::new();
for op in ops {
use crate::runtime::windows::WindowSyncOp;
let typed = match op {
WindowSyncOp::Open {
window_id,
settings,
} => plushie_core::ops::WindowOp::Open {
window_id,
settings,
},
WindowSyncOp::Close { window_id } => plushie_core::ops::WindowOp::Close(window_id),
WindowSyncOp::Update {
window_id,
settings,
} => plushie_core::ops::WindowOp::Update {
window_id,
settings,
},
};
tasks.push(self.renderer.dispatch_window_op(typed));
}
sync_direct_window_theme_state(&mut self.renderer.windows, tree);
if tasks.is_empty() {
Task::none()
} else {
Task::batch(tasks)
}
}
fn refresh_view(&mut self) {
let fallback = self.current_tree.clone().unwrap_or_else(placeholder_tree);
let outcome = crate::runtime::view_errors::run_guarded_view::<A>(
&mut self.view_errors,
&self.model,
&mut self.widget_store,
&mut self.memo_cache,
&mut self.widget_view_cache,
&fallback,
);
let mut tree = match outcome {
ViewOutcome::Ok(tree, warnings) => {
for warning in &warnings {
log::warn!("view normalization: {warning}");
}
tree
}
ViewOutcome::Panicked { last_good, .. } => last_good,
};
self.renderer.registry.prepare_walk_in_window(
&mut tree,
&mut self.renderer.core.caches,
&self.renderer.theme,
DIRECT_ROOT_WINDOW_ID,
);
self.current_tree = Some(tree);
}
fn execute_command(&mut self, cmd: Command) -> Task<Message> {
match cmd {
Command::None => Task::none(),
Command::Exit => {
self.drain_effects_as_shutdown();
plushie_widget_sdk::iced::exit()
}
Command::Batch(cmds) => {
let tasks: Vec<Task<Message>> =
cmds.into_iter().map(|c| self.execute_command(c)).collect();
Task::batch(tasks)
}
Command::Renderer(plushie_core::ops::RendererOp::Effect {
tag,
request,
timeout,
}) => {
let kind = request.kind();
let effective_timeout =
timeout.unwrap_or_else(|| effect_tracker::default_timeout(kind));
let (wire_id, replaced) =
self.effect_tracker
.track_with_replacement(&tag, kind, effective_timeout);
if let Some((prior_tag, _prior_kind)) = replaced {
self.event_queue
.lock()
.push(SinkEvent::DelayedEvent(Event::Effect(EffectEvent {
tag: prior_tag,
result: EffectResult::Cancelled,
})));
}
self.renderer
.execute(plushie_core::ops::RendererOp::Effect {
tag: wire_id,
request,
timeout: None,
})
}
Command::Renderer(op) => self.renderer.execute(op),
Command::Async { tag, task } => {
let queue = self.event_queue.clone();
let tag_clone = tag.clone();
let tag_for_guard = tag.clone();
let future = (task)();
let guarded =
async move { super::run_task_with_panic_guard(&tag_for_guard, future).await };
let (task, handle) = Task::perform(guarded, move |result| {
queue.lock().push(SinkEvent::AsyncResult {
tag: tag_clone,
result,
});
Message::NoOp
})
.abortable();
self.running_tasks.insert(tag, handle);
task
}
Command::Stream { tag, task } => {
let queue = self.event_queue.clone();
let emitter = crate::command::StreamEmitter::buffered(&tag);
let sink_queue = queue.clone();
let sink_tag = tag.clone();
emitter.attach_sink(Box::new(move |t, value| {
sink_queue
.lock()
.push(SinkEvent::StreamValue { tag: t, value });
let _ = sink_tag;
}));
let final_tag = tag.clone();
let tag_for_guard = tag.clone();
let future = (task)(emitter);
let guarded =
async move { super::run_task_with_panic_guard(&tag_for_guard, future).await };
let (task, handle) = Task::perform(guarded, move |result| {
queue.lock().push(SinkEvent::AsyncResult {
tag: final_tag,
result,
});
Message::NoOp
})
.abortable();
self.running_tasks.insert(tag, handle);
task
}
Command::Cancel { tag } => {
if let Some(handle) = self.running_tasks.remove(&tag) {
handle.abort();
}
Task::none()
}
Command::SendAfter { delay, event } => {
let queue = self.event_queue.clone();
Task::perform(
async move {
super::platform_sleep(delay).await;
},
move |_| {
queue.lock().push(SinkEvent::DelayedEvent(*event));
Message::NoOp
},
)
}
}
}
fn subscriptions(&self) -> plushie_widget_sdk::iced::Subscription<Message> {
let mut subs: Vec<plushie_widget_sdk::iced::Subscription<Message>> = self
.active_timers
.iter()
.map(|(tag, duration)| {
plushie_widget_sdk::iced::time::every(*duration)
.with(tag.clone()) .map(|(tag, _instant)| Message::TimerTick(tag))
})
.collect();
subs.push(self.renderer.renderer_subscriptions());
plushie_widget_sdk::iced::Subscription::batch(subs)
}
fn handle_timer_tick(&self, tag: String) {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
self.event_queue
.lock()
.push(SinkEvent::DelayedEvent(Event::Timer(
crate::event::TimerEvent { tag, timestamp },
)));
}
fn apply_sub_op(&mut self, op: SubOp) -> Task<Message> {
match op {
SubOp::Subscribe {
kind,
tag,
max_rate,
window_id,
} => self
.renderer
.execute(plushie_core::ops::RendererOp::Subscribe {
kind,
tag,
max_rate,
window_id,
}),
SubOp::Unsubscribe { kind, tag } => self
.renderer
.execute(plushie_core::ops::RendererOp::Unsubscribe { kind, tag }),
SubOp::StartTimer { tag, interval } => {
self.active_timers.insert(tag, interval);
Task::none()
}
SubOp::StopTimer { tag } => {
self.active_timers.remove(&tag);
Task::none()
}
}
}
}
fn apply_settings<A: App>(renderer: &mut plushie_renderer_lib::App) {
use plushie_renderer_engine::{CoreEffect, StateChange};
use plushie_widget_sdk::protocol::IncomingMessage;
let settings = A::settings();
let wire_json = settings.to_wire_json();
let effects = renderer.core.apply(IncomingMessage::Settings {
settings: wire_json,
});
for effect in effects {
match effect {
CoreEffect::StateChange(StateChange::WidgetConfig(config)) => {
let ctx = plushie_widget_sdk::registry::InitCtx {
config: &config,
theme: &renderer.theme,
default_text_size: renderer.core.default_text_size,
default_font: renderer.core.default_font,
};
renderer.registry.init_all(&ctx);
}
other => {
log::warn!("unexpected effect from initial Settings: {other:?}");
}
}
}
renderer
.emitter
.set_default_rate(renderer.core.default_event_rate);
if let Some(sf) = settings.scale_factor {
renderer.scale_factor = plushie_renderer_lib::app::validate_scale_factor(sf);
}
if let Some(theme) = settings.theme {
use plushie_core::types::{PlushieType, Theme};
match &theme {
Theme::System => {
renderer.theme_follows_system = true;
}
_ => {
let wire_val = serde_json::Value::from(theme.wire_encode());
renderer.theme = plushie_widget_sdk::runtime::resolve_theme(&wire_val);
}
}
}
}
pub fn run<A: App>() -> crate::Result {
let canonical = A::settings().to_wire_json();
let iced_settings = plushie_renderer_lib::settings::parse_iced_settings(&canonical);
plushie_widget_sdk::iced::daemon(
DirectApp::<A>::init,
DirectApp::<A>::update,
DirectApp::<A>::view_window,
)
.settings(iced_settings)
.subscription(DirectApp::<A>::subscriptions)
.title(DirectApp::<A>::title_for_window)
.theme(DirectApp::<A>::theme_for_window)
.scale_factor(DirectApp::<A>::scale_factor_for_window)
.run()
.map_err(|e| crate::Error::Iced(e.to_string()))
}
fn find_window_node<'a>(tree: &'a TreeNode, window_id: &str) -> Option<&'a TreeNode> {
if tree.type_name == "window" && tree.id == window_id {
return Some(tree);
}
for child in &tree.children {
if let Some(n) = find_window_node(child, window_id) {
return Some(n);
}
}
None
}
fn sync_direct_window_theme_state(
windows: &mut plushie_renderer_lib::window_map::WindowMap,
tree: &TreeNode,
) {
windows.clear_theme_cache();
sync_direct_window_theme_node(windows, tree);
}
fn sync_direct_window_theme_node(
windows: &mut plushie_renderer_lib::window_map::WindowMap,
node: &TreeNode,
) {
if node.type_name == "window"
&& let Some(theme_val) = node.props.get_value("theme")
{
match plushie_widget_sdk::runtime::resolve_theme_resolution(&theme_val) {
plushie_widget_sdk::runtime::ThemeResolution::Theme(theme, chrome) => {
windows.set_theme(&node.id, theme, chrome);
}
plushie_widget_sdk::runtime::ThemeResolution::System => {
windows.set_theme_follows_system(&node.id);
}
plushie_widget_sdk::runtime::ThemeResolution::Invalid => {}
}
}
for child in &node.children {
sync_direct_window_theme_node(windows, child);
}
}
fn build_direct_base_settings<A: App>() -> serde_json::Value {
let settings = A::settings();
let mut obj = serde_json::Map::new();
if let Some(sf) = settings.scale_factor {
obj.insert("scale_factor".into(), serde_json::json!(sf));
}
if let Some(theme) = settings.theme {
use plushie_core::types::PlushieType;
obj.insert("theme".into(), serde_json::Value::from(theme.wire_encode()));
}
serde_json::Value::Object(obj)
}
fn placeholder_tree() -> TreeNode {
TreeNode {
id: String::new(),
type_name: "container".to_string(),
props: plushie_widget_sdk::protocol::Props::from(
plushie_widget_sdk::protocol::PropMap::new(),
),
children: vec![],
}
}
#[cfg(test)]
mod tests {
use super::*;
use plushie_widget_sdk::protocol::{PropMap, Props};
use serde_json::json;
fn window(id: &str, props: serde_json::Value) -> TreeNode {
TreeNode {
id: id.to_string(),
type_name: "window".to_string(),
props: Props::from_json(props),
children: vec![],
}
}
fn container(children: Vec<TreeNode>) -> TreeNode {
TreeNode {
id: "root".to_string(),
type_name: "container".to_string(),
props: Props::from(PropMap::new()),
children,
}
}
#[test]
fn direct_window_system_theme_updates_renderer_subscription_state() {
let mut windows = plushie_renderer_lib::window_map::WindowMap::new();
windows.insert(
"main".to_string(),
plushie_widget_sdk::iced::window::Id::unique(),
);
let tree = container(vec![window("main", json!({"theme": "system"}))]);
sync_direct_window_theme_state(&mut windows, &tree);
assert!(windows.theme_follows_system("main"));
assert!(windows.any_theme_follows_system());
}
#[test]
fn direct_window_theme_state_clears_when_tree_no_longer_follows_system() {
let mut windows = plushie_renderer_lib::window_map::WindowMap::new();
windows.insert(
"main".to_string(),
plushie_widget_sdk::iced::window::Id::unique(),
);
let tree = container(vec![window("main", json!({"theme": "system"}))]);
sync_direct_window_theme_state(&mut windows, &tree);
let tree = container(vec![window("main", json!({"theme": "dark"}))]);
sync_direct_window_theme_state(&mut windows, &tree);
assert!(!windows.theme_follows_system("main"));
assert!(!windows.any_theme_follows_system());
}
}