1use serde_json::{Value as Json, json};
12
13use crate::engine::{DocState, EngineError, Result, Selection, Session};
14
15#[derive(Clone, Debug, Default, PartialEq)]
17pub struct WindowView {
18 pub selection: Selection,
19 pub sheet: usize,
20 pub sheet_name: String,
23 pub sheet_selections: Vec<Selection>,
24}
25
26#[derive(Clone, Debug)]
28pub struct WindowState {
29 pub id: u64,
31 pub doc_uid: u64,
33 pub hidden: bool,
35 view: Option<WindowView>,
38 last_active: u64,
40}
41
42impl WindowView {
43 fn capture(d: &DocState) -> WindowView {
44 WindowView {
45 selection: d.selection.clone(),
46 sheet: d.wb.active_sheet(),
47 sheet_name: d.wb.sheets.get(d.wb.active_sheet()).map(|s| s.name.clone()).unwrap_or_default(),
48 sheet_selections: d.sheet_selections.clone(),
49 }
50 }
51
52 fn apply(self, d: &mut DocState) {
53 let sheets = &d.wb.sheets;
54 let at = if sheets.get(self.sheet).is_some_and(|s| s.name == self.sheet_name) {
55 self.sheet
56 } else {
57 sheets.iter().position(|s| s.name == self.sheet_name).unwrap_or(self.sheet)
58 };
59 d.wb.set_active_sheet(at.min(sheets.len().saturating_sub(1)));
60 d.selection = self.selection;
61 d.sheet_selections = self.sheet_selections;
62 }
63}
64
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
67pub enum Target {
68 #[default]
70 Active,
71 Window(u64),
73 Doc(u64),
75}
76
77impl Target {
78 pub fn from_json(p: &Json) -> Result<Target> {
81 let id = |k: &str| -> Result<Option<u64>> {
82 match p.get(k) {
83 None | Some(Json::Null) => Ok(None),
84 Some(v) => v.as_u64().map(Some).ok_or_else(|| EngineError::Other(format!("`{k}` must be a window or workbook id number"))),
85 }
86 };
87 Ok(match (id("window")?, id("doc")?) {
88 (Some(w), _) => Target::Window(w),
89 (None, Some(d)) => Target::Doc(d),
90 (None, None) => Target::Active,
91 })
92 }
93}
94
95#[derive(Debug)]
97#[must_use]
98pub struct TargetGuard {
99 id: u64,
100 back: Option<u64>,
101}
102
103impl Session {
104 pub fn windows(&self) -> &[WindowState] {
106 &self.windows
107 }
108
109 pub fn active_window(&self) -> Option<u64> {
111 self.active_window
112 }
113
114 pub fn window(&self, id: u64) -> Option<&WindowState> {
115 self.windows.iter().find(|w| w.id == id)
116 }
117
118 fn windows_of(&self, doc: u64) -> impl Iterator<Item = &WindowState> {
120 self.windows.iter().filter(move |w| w.doc_uid == doc)
121 }
122
123 pub fn window_title(&self, id: u64) -> Option<String> {
126 let w = self.window(id)?;
127 let d = self.docs.iter().find(|d| d.uid == w.doc_uid)?;
128 let name = d.display_title();
129 let mut n = 0;
130 let mut at = 0;
131 for x in self.windows_of(w.doc_uid) {
132 n += 1;
133 if x.id == id {
134 at = n;
135 }
136 }
137 Some(if n > 1 { format!("{name}:{at}") } else { name })
138 }
139
140 pub fn doc_window(&self, doc: u64) -> Option<u64> {
142 self.windows_of(doc).max_by_key(|w| (!w.hidden, w.last_active)).map(|w| w.id)
143 }
144
145 pub fn target_window(&self, t: Target) -> Result<u64> {
147 match t {
148 Target::Active => self.active_window.ok_or(EngineError::NoDocument),
149 Target::Window(id) => self.window(id).map(|w| w.id).ok_or_else(|| EngineError::Other(format!("no open window has id {id}"))),
150 Target::Doc(uid) => self.doc_window(uid).ok_or_else(|| EngineError::Other(format!("no open workbook has id {uid}"))),
151 }
152 }
153
154 pub(crate) fn push_window(&mut self, doc_uid: u64) -> u64 {
155 self.next_window = self.next_window.saturating_add(1);
156 let id = self.next_window;
157 self.windows.push(WindowState { id, doc_uid, hidden: false, view: None, last_active: 0 });
158 id
159 }
160
161 pub fn activate_window(&mut self, id: u64) {
163 self.enter_window(id, true);
164 }
165
166 pub fn show_window(&mut self, id: u64) {
169 self.enter_window(id, false);
170 }
171
172 fn enter_window(&mut self, id: u64, recent: bool) {
173 let Some(wi) = self.windows.iter().position(|w| w.id == id) else { return };
174 let Some(w) = self.windows.get(wi) else { return };
175 let doc = w.doc_uid;
176 if w.view.is_some()
177 && let Some(di) = self.document_index(doc)
178 {
179 let live = self.windows.iter().position(|x| x.doc_uid == doc && x.view.is_none());
180 let incoming = self.windows.get_mut(wi).and_then(|w| w.view.take());
181 if let Some(d) = self.docs.get_mut(di) {
182 if let Some(li) = live
183 && let Some(lw) = self.windows.get_mut(li)
184 {
185 lw.view = Some(WindowView::capture(d));
186 }
187 if let Some(v) = incoming {
188 v.apply(d);
189 }
190 }
191 }
192 if recent {
193 self.activation = self.activation.saturating_add(1);
194 let stamp = self.activation;
195 if let Some(w) = self.windows.get_mut(wi) {
196 w.last_active = stamp;
197 }
198 }
199 self.active_window = Some(id);
200 if let Some(di) = self.document_index(doc) {
201 self.active = di;
202 }
203 }
204
205 pub(crate) fn activate_recent(&mut self) {
207 let next = self.windows.iter().max_by_key(|w| (!w.hidden, w.last_active)).map(|w| w.id);
208 match next {
209 Some(id) => self.activate_window(id),
210 None => {
211 self.active_window = None;
212 self.active = 0;
213 }
214 }
215 }
216
217 pub fn new_window(&mut self) -> Result<u64> {
219 let current = self.active_window.ok_or(EngineError::NoDocument)?;
220 let doc = self.window(current).map(|w| w.doc_uid).ok_or(EngineError::NoDocument)?;
221 let view = self.doc().map(WindowView::capture)?;
222 if let Some(w) = self.windows.iter_mut().find(|w| w.doc_uid == doc && w.view.is_none()) {
224 w.view = Some(view);
225 }
226 let id = self.push_window(doc);
227 self.activate_window(id);
228 Ok(id)
229 }
230
231 pub fn close_window(&mut self, id: u64) {
234 let Some(wi) = self.windows.iter().position(|w| w.id == id) else { return };
235 let doc = self.windows.get(wi).map(|w| w.doc_uid).unwrap_or_default();
236 if self.windows_of(doc).count() <= 1 {
237 if let Some(di) = self.document_index(doc) {
238 self.close_document(di);
239 }
240 return;
241 }
242 let closed = self.windows.remove(wi);
243 if closed.view.is_none() {
244 if let Some(next) = self.windows.iter().filter(|w| w.doc_uid == doc).max_by_key(|w| w.last_active).map(|w| w.id) {
246 let incoming = self.windows.iter_mut().find(|w| w.id == next).and_then(|w| w.view.take());
247 if let Some(v) = incoming
248 && let Some(d) = self.docs.iter_mut().find(|d| d.uid == doc)
249 {
250 v.apply(d);
251 }
252 }
253 }
254 if self.active_window == Some(id) {
255 self.activate_recent();
256 }
257 }
258
259 pub fn hide_window(&mut self, id: u64) {
261 let Some(w) = self.windows.iter_mut().find(|w| w.id == id) else { return };
262 w.hidden = true;
263 if self.active_window == Some(id) && self.windows.iter().any(|w| !w.hidden) {
264 self.activate_recent();
265 }
266 }
267
268 pub fn unhide_window(&mut self, id: u64) {
270 let Some(w) = self.windows.iter_mut().find(|w| w.id == id) else { return };
271 w.hidden = false;
272 self.activate_window(id);
273 }
274
275 pub fn last_hidden_window(&self) -> Option<u64> {
277 self.windows.iter().filter(|w| w.hidden).max_by_key(|w| w.last_active).map(|w| w.id)
278 }
279
280 pub fn window_list(&self) -> Json {
284 Json::Array(
285 self.windows
286 .iter()
287 .filter_map(|w| {
288 let index = self.document_index(w.doc_uid)?;
289 let d = self.docs.get(index)?;
290 Some(json!({
291 "window": w.id,
292 "doc": w.doc_uid,
293 "index": index,
294 "title": self.window_title(w.id).unwrap_or_default(),
295 "dirty": d.is_dirty(),
296 "active": self.active_window == Some(w.id),
297 "hidden": w.hidden,
298 }))
299 })
300 .collect(),
301 )
302 }
303
304 pub fn with_target<T>(&mut self, t: Target, f: impl FnOnce(&mut Session) -> T) -> Result<T> {
308 let guard = self.enter_target(t)?;
309 let r = f(self);
310 self.leave_target(guard);
311 Ok(r)
312 }
313
314 pub fn enter_target(&mut self, t: Target) -> Result<Option<TargetGuard>> {
318 if t == Target::Active {
319 return Ok(None);
320 }
321 let id = self.target_window(t)?;
322 let back = self.active_window;
323 self.activate_window(id);
324 Ok(Some(TargetGuard { id, back }))
325 }
326
327 pub fn leave_target(&mut self, guard: Option<TargetGuard>) {
328 let Some(TargetGuard { id, back: Some(back) }) = guard else { return };
329 if self.active_window == Some(id) && back != id && self.window(back).is_some() {
330 self.activate_window(back);
331 }
332 }
333
334 pub fn execute_in(&mut self, t: Target, id: &str, params: Json) -> Result<Json> {
336 self.with_target(t, |s| s.execute(id, params))?
337 }
338
339 pub fn execute_headless_in(&mut self, t: Target, id: &str, params: Json) -> Result<Json> {
341 self.with_target(t, |s| s.execute_headless(id, params))?
342 }
343}
344
345pub fn is_window_command(id: &str) -> bool {
347 id.starts_with("window.") || matches!(id, "view.newWindow" | "view.hideWindow" | "view.unhideWindow" | "view.switchWindows")
348}