1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Processes incoming protocol messages (snapshots, patches, settings,
//! extension commands) by delegating to Core and handling resulting effects.
use std::io;
use iced::Task;
use toddy_core::engine::CoreEffect;
use toddy_core::message::Message;
use toddy_core::protocol::IncomingMessage;
use super::App;
use super::emitters::{emit_effect_response, emit_event};
impl App {
pub(super) fn apply(&mut self, message: IncomingMessage) -> io::Result<()> {
// Extension commands bypass the normal tree update / diff / patch cycle.
match &message {
IncomingMessage::ExtensionCommand {
node_id,
op,
payload,
} => {
let events = self.dispatcher.handle_command(
node_id,
op,
payload,
&mut self.core.caches.extension,
);
for ev in events {
emit_event(ev)?;
}
return Ok(());
}
IncomingMessage::ExtensionCommands { commands } => {
for cmd in commands {
let events = self.dispatcher.handle_command(
&cmd.node_id,
&cmd.op,
&cmd.payload,
&mut self.core.caches.extension,
);
for ev in events {
emit_event(ev)?;
}
}
return Ok(());
}
_ => {}
}
let is_snapshot = matches!(message, IncomingMessage::Snapshot { .. });
let is_tree_change = matches!(
message,
IncomingMessage::Snapshot { .. } | IncomingMessage::Patch { .. }
);
let effects = self.core.apply(message);
for effect in effects {
match effect {
CoreEffect::SyncWindows => {
let task = self.sync_windows();
self.pending_tasks.push(task);
}
CoreEffect::EmitEvent(event) => emit_event(event)?,
CoreEffect::EmitEffectResponse(response) => {
emit_effect_response(response)?;
}
CoreEffect::WidgetOp { op, payload } => {
let task = self.handle_widget_op(&op, &payload);
self.pending_tasks.push(task);
}
CoreEffect::WindowOp {
op,
window_id,
settings,
} => {
let task = self.handle_window_op(&op, &window_id, &settings);
self.pending_tasks.push(task);
}
CoreEffect::ThemeChanged(theme) => {
self.theme = theme;
self.theme_follows_system = false;
}
CoreEffect::ThemeFollowsSystem => {
self.theme_follows_system = true;
}
CoreEffect::ImageOp {
op,
handle,
data,
pixels,
width,
height,
} => {
self.handle_image_op(&op, &handle, data, pixels, width, height);
}
CoreEffect::ExtensionConfig(config) => {
self.dispatcher.init_all(&config);
}
CoreEffect::SpawnAsyncEffect {
request_id,
effect_type,
params,
} => {
let task = Task::perform(
async move {
toddy_core::effects::handle_async_effect(
request_id,
&effect_type,
¶ms,
)
.await
},
|response| {
// Inside an async Task callback -- log and
// continue; the next synchronous write will
// detect the broken pipe and exit cleanly.
if let Err(e) = emit_effect_response(response) {
log::error!("write error in async effect: {e}");
}
Message::NoOp
},
);
self.pending_tasks.push(task);
}
}
}
// After tree changes, update per-window theme cache and notify extensions.
if is_tree_change {
// Rebuild per-window theme cache from current tree.
self.windows.clear_theme_cache();
for win_id in self.core.tree.window_ids() {
// When resolve_theme_only returns None it means "system" --
// no cache entry, falls through to the system_theme path
// in theme_for_window().
if let Some(node) = self.core.tree.find_window(&win_id)
&& let Some(theme_val) = node.props.get("theme")
&& let Some(theme) = toddy_core::theming::resolve_theme_only(theme_val)
{
self.windows.set_theme(&win_id, Some(theme));
}
}
if is_snapshot {
self.dispatcher.clear_poisoned();
}
if let Some(root) = self.core.tree.root() {
self.dispatcher
.prepare_all(root, &mut self.core.caches.extension, &self.theme);
}
}
Ok(())
}
}