1use blockworx_geom::{Pos2, vec2};
7use blockworx_paint::Cursor;
8
9use crate::canvas::View;
10use crate::canvas::convert::{IntoEgui as _, IntoGeom};
11use crate::io_pin_picker::{self, PinTypePick};
12use crate::kernel::{Overlay, View as Shown};
13use crate::panels::overlay::{OpenPicker, RightClick, selection_overlay};
14use crate::panels::palette::{Palette, PaletteOutcome};
15use crate::preferences::Preferences;
16use crate::theme::Theme;
17use crate::tools::commands::CommandSet;
18use crate::tools::commands::{Act, CommandId, Effect};
19use crate::tools::names::ToolName;
20use crate::tools::tool::{Action, RoleTarget};
21use blockworx_doc::id::PinId;
22
23const WORKSPACES: &str = "workspaces";
26
27#[derive(Default)]
28pub(crate) struct Surface {
29 pub(crate) canvas: View,
32 popup: Option<Popup>,
37 overlay_top_right: Option<Pos2>,
41 palette: Option<Palette>,
43 pub(crate) workspace: crate::shell::workspace::Workspace,
46 workspaces: std::collections::BTreeMap<String, crate::shell::workspace::Workspace>,
50 pub(crate) safe: crate::shell::SafeArea,
54 history_search: String,
58}
59
60pub(crate) enum Popup {
63 Role(RoleTarget),
65 PinType(Vec<PinId>),
68}
69
70impl Popup {
71 fn picker(&self) -> OpenPicker {
72 match self {
73 Popup::Role(_) => OpenPicker::Role,
74 Popup::PinType(_) => OpenPicker::PinType,
75 }
76 }
77}
78
79pub(crate) struct BarState<'a> {
83 pub theme: &'a Theme,
84 pub preferences: &'a mut Preferences,
85 #[cfg(not(target_arch = "wasm32"))]
86 pub recent: &'a [std::path::PathBuf],
87 #[cfg(not(target_arch = "wasm32"))]
88 pub rename_draft: &'a mut String,
89}
90
91pub(crate) struct OverCanvas {
95 pub picker: Option<OpenPicker>,
96 pub act: Option<Act>,
97}
98
99impl Surface {
100 pub(crate) fn show_popups(
108 &mut self,
109 ctx: &egui::Context,
110 bar: Option<&Overlay>,
111 theme: &Theme,
112 ) -> (Option<OpenPicker>, Option<Action>) {
113 let was_open = self.popup.as_ref().map(Popup::picker);
114 let Some(corner) = self.overlay_top_right else {
117 return (was_open, None);
118 };
119 let at = corner - vec2(0.0, crate::grid::GRID_SIZE);
120 let (still_open, picked) = match self.popup.take() {
121 Some(Popup::Role(target)) => show_role_picker(ctx, bar, theme, at, target),
122 Some(Popup::PinType(pins)) => show_pin_type_picker(ctx, bar, at, pins),
123 None => (None, None),
124 };
125 self.popup = still_open;
126 (was_open, picked)
127 }
128
129 pub(crate) fn intercept_object_paste(&self, ctx: &egui::Context) -> Option<String> {
136 if !ctx.egui_wants_keyboard_input() {
137 return None;
138 }
139 let text = latest_paste(ctx)?;
140 if !crate::widget::clipboard::is_object_clipboard(&text) {
141 return None;
142 }
143 ctx.input_mut(|i| {
144 i.events.retain(|e| !matches!(e, egui::Event::Paste(_)));
145 i.raw.events.retain(|e| !matches!(e, egui::Event::Paste(_)));
146 });
147 let focused = ctx
148 .memory(egui::Memory::focused)
149 .or_else(|| self.canvas.focused_edit_id());
150 if let Some(id) = focused {
151 ctx.memory_mut(|m| m.surrender_focus(id));
152 }
153 Some(text)
154 }
155
156 pub(crate) fn show_canvas_chrome(
161 &mut self,
162 ui: &mut egui::Ui,
163 shown: &mut Shown,
164 over: OverCanvas,
165 ) -> Option<Act> {
166 let ctx_owned = ui.ctx().clone();
167 let ctx = &ctx_owned;
168 let OverCanvas {
169 picker: open_picker,
170 act: mut action,
171 } = over;
172 let Shown {
173 commands,
174 overlay: bar,
175 history,
176 nav_tree,
177 viewport,
178 ..
179 } = shown;
180 let viewport = *viewport;
181 let right_click = RightClick::from(
186 self.canvas.canvas_hovered() && ctx.input(|i| i.pointer.secondary_clicked()),
187 );
188 let (overlay_action, overlay_corner) = selection_overlay(
189 ui,
190 crate::panels::overlay::Overlay {
191 commands,
192 bar: bar.as_ref(),
193 open_picker,
194 safe: self.safe,
195 camera: self.canvas.camera(),
196 right_click,
197 },
198 );
199 if let Some(overlay_action) = overlay_action {
200 action = Some(overlay_action);
201 }
202 self.overlay_top_right = overlay_corner.map(IntoGeom::geom);
203 if overlay_corner.is_none() {
205 self.popup = None;
206 }
207 let scope = crate::panels::palette::PaletteScope {
208 tree: nav_tree,
209 revs: history,
210 };
211 let outcome = self
212 .palette
213 .as_mut()
214 .map(|palette| palette.show(ctx, commands, scope, viewport.egui()));
215 match outcome {
216 None | Some(PaletteOutcome::Open) => {}
217 Some(PaletteOutcome::Close) => self.palette = None,
218 Some(PaletteOutcome::Dispatch(palette_action)) => {
219 action = Some(*palette_action);
220 self.palette = None;
221 }
222 }
223 action
224 }
225
226 pub(crate) fn toggles_the_palette(&mut self) {
230 self.palette = match self.palette {
231 None => Some(Palette::new()),
232 Some(_) => None,
233 };
234 }
235
236 pub(crate) fn show_top_bar(
239 &mut self,
240 chrome: &mut crate::shell::Chrome,
241 model: &crate::kernel::TopBar,
242 commands: &mut CommandSet,
243 bar: BarState<'_>,
244 ) -> Option<Act> {
245 let BarState {
246 theme,
247 preferences,
248 #[cfg(not(target_arch = "wasm32"))]
249 recent,
250 #[cfg(not(target_arch = "wasm32"))]
251 rename_draft,
252 } = bar;
253 let clicked = crate::shell::top_bar::top_bar(
254 chrome,
255 commands,
256 crate::shell::top_bar::Docked {
257 model,
258 navigator: self.workspace.open().into(),
259 theme,
260 prefs: preferences,
261 #[cfg(not(target_arch = "wasm32"))]
262 recent,
263 #[cfg(not(target_arch = "wasm32"))]
264 document: crate::shell::top_bar::Document {
265 draft: rename_draft,
266 renaming: model.renaming,
267 },
268 },
269 );
270 if clicked.browse {
271 self.workspace.toggle();
272 }
273 clicked.action
274 }
275
276 pub(crate) fn dismiss_navigator(&mut self, ctx: &egui::Context) {
281 if !self.workspace.open() {
282 return;
283 }
284 self.workspace.close();
285 crate::panels::nav_tree::clear_filter(ctx);
286 }
287
288 pub(crate) fn show_navigator_body(
290 &mut self,
291 ui: &mut egui::Ui,
292 shown: &Shown,
293 theme: &Theme,
294 ) -> Option<Act> {
295 let view = self.workspace.view;
296 match view {
297 crate::shell::workspace::PanelView::History => crate::panels::history_panel::body(
298 ui,
299 crate::panels::history_panel::HistoryPanel {
300 scene: crate::panels::history_panel::HistoryScene {
301 rows: &shown.history,
302 viewing: shown.top_bar.lens.viewing,
303 head: shown.top_bar.lens.head,
304 now: blockworx_store::history::now(),
305 theme,
306 },
307 search: &mut self.history_search,
308 },
309 ),
310 crate::shell::workspace::PanelView::Hierarchy => crate::panels::nav_tree::body(
311 ui,
312 crate::panels::nav_tree::NavScene {
313 tree: &shown.nav_tree,
314 theme,
315 },
316 )
317 .map(Act::from),
318 }
319 }
320
321 pub(crate) fn dropped(
330 ctx: &egui::Context,
331 shown: &Shown,
332 carried: crate::shell::tool_cluster::DragOut,
333 ) -> Option<Act> {
334 let onto_the_canvas = shown.viewport.contains(carried.at.geom())
335 && !crate::shell::over_the_chrome(ctx, carried.at);
336 if !onto_the_canvas {
337 return None;
338 }
339 let at = shown
340 .vantage
341 .screen_to_world(shown.viewport.min, carried.at.geom());
342 Some(match carried.tool {
343 ToolName::NewImage => Effect::AddImage.into(),
344 tool => Action::StampTool { tool, at }.into(),
345 })
346 }
347
348 #[cfg(not(target_arch = "wasm32"))]
350 pub(crate) fn forget_popup(&mut self) {
351 self.popup = None;
352 }
353
354 pub(crate) fn open_popup(&mut self, ctx: &egui::Context, popup: Popup) {
358 self.popup = Some(popup);
359 ctx.request_repaint();
360 }
361
362 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
367 pub(crate) fn settle_workspace(&mut self, was: &str, now: &str) {
368 self.workspaces.insert(was.to_owned(), self.workspace);
369 self.workspace = self.workspaces.get(now).copied().unwrap_or_default();
370 }
371
372 pub(crate) fn restore(&mut self, storage: &dyn eframe::Storage, key: &str) {
375 if let Some(s) = storage.get_string(WORKSPACES)
376 && let Ok(workspaces) = serde_json::from_str(&s)
377 {
378 self.workspaces = workspaces;
379 self.workspace = self.workspaces.get(key).copied().unwrap_or_default();
380 }
381 }
382
383 pub(crate) fn save(&mut self, storage: &mut dyn eframe::Storage, key: &str) {
384 self.workspaces.insert(key.to_owned(), self.workspace);
385 match serde_json::to_string(&self.workspaces) {
386 Ok(s) => storage.set_string(WORKSPACES, s),
387 Err(e) => tracing::error!("Failed to serialize workspaces: {e}"),
388 }
389 }
390}
391
392pub(crate) fn handle_keyboard(ctx: &egui::Context, shown: &mut Shown) -> Option<Act> {
398 if ctx.egui_wants_keyboard_input() {
399 return None;
400 }
401 let paste_txt = latest_paste(ctx);
402 let writable = shown.writable == blockworx_store::doc::Writability::Writable;
407 if let Some(text) = paste_txt {
408 writable.then_some(Action::Paste(text).into())
409 } else if ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Copy))) {
410 shown.commands.take(CommandId::Copy)
413 } else if ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Cut))) {
414 shown.commands.take(CommandId::Cut)
415 } else {
416 None
417 }
418}
419
420fn show_role_picker(
423 ctx: &egui::Context,
424 bar: Option<&Overlay>,
425 theme: &Theme,
426 at: Pos2,
427 target: RoleTarget,
428) -> (Option<Popup>, Option<Action>) {
429 let current = bar.and_then(|bar| bar.accent);
430 let picked = match crate::role_picker::show(ctx, at.egui(), theme, current, target) {
431 crate::role_picker::RolePick::Set(role) => Some(Action::SetRole { target, role }),
432 crate::role_picker::RolePick::Dismiss => return (None, None),
433 crate::role_picker::RolePick::None => None,
434 };
435 (Some(Popup::Role(target)), picked)
436}
437
438fn show_pin_type_picker(
442 ctx: &egui::Context,
443 bar: Option<&Overlay>,
444 at: Pos2,
445 pins: Vec<PinId>,
446) -> (Option<Popup>, Option<Action>) {
447 let current = bar.and_then(|bar| bar.pin_dir);
448 let picked = match io_pin_picker::show(ctx, at.egui(), current) {
449 PinTypePick::Set(kind) => Some(Action::SetPinsKind {
450 pins: pins.clone(),
451 kind,
452 }),
453 PinTypePick::Dismiss => return (None, None),
454 PinTypePick::None => None,
455 };
456 (Some(Popup::PinType(pins)), picked)
457}
458
459pub(crate) fn effective_cursor(
463 tool_cursor: Option<Cursor>,
464 pointer: PointerOver,
465) -> Option<Cursor> {
466 match pointer {
467 PointerOver::Canvas => tool_cursor,
468 PointerOver::Elsewhere => None,
469 }
470}
471
472#[derive(Clone, Copy, PartialEq, Eq)]
475pub(crate) enum PointerOver {
476 Canvas,
477 Elsewhere,
478}
479
480fn latest_paste(ctx: &egui::Context) -> Option<String> {
482 ctx.input(|i| {
483 i.events.iter().rev().find_map(|e| match e {
484 egui::Event::Paste(s) => Some(s.clone()),
485 _ => None,
486 })
487 })
488}