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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
//! The patch editor: a window with the test screen, the inspector and the
//! layout stack on the left, and the canvas the selected layout is edited on.
mod canvas;
mod chains;
mod clipboard;
mod commands;
mod draft;
mod drag;
mod fixture_ui;
mod fixtures;
mod inspector;
mod layout_stack;
mod member_preview;
mod membership;
mod node_response;
mod port_inspector;
mod selection_handles;
mod state;
mod toolbar;
#[cfg(test)]
mod window_tests;
use crate::canvas::test_screen;
use crate::fixture_editor::membership::ListDrag;
use crate::fixture_editor::state::EditorState;
use crate::ui::file_pick::FilePick;
use crate::ui::window_common::{
dialog_viewport_builder, embedded_close_clicked, gled_window_frame, raise, tool_window_builder,
};
use draft::{Draft, DraftStatus};
use egui::{Color32, Context, Id, Rangef, ScrollArea, Sense, Ui, Vec2, ViewportId};
use gled_model::asset::project::Project;
use gled_model::asset_id::AssetId;
use gled_model::collection::Collections;
use gled_model::patching::Patch;
use gled_model::patching::definition::FixtureDefinition;
use gled_model::patching::layout::LayoutSelection;
use state::PatchEdit;
/// Longest a layout or mode name can be typed.
const NAME_CHAR_LIMIT: usize = 24;
/// The ⚠ beside something the output cannot do as it is set.
const WARNING: Color32 = Color32::from_rgb(230, 160, 70);
/// Apply's fill while there is something to apply.
const APPLY_FILL: Color32 = Color32::from_rgb(58, 104, 68);
/// Hands the patch to the show. Green because it is the one button here that
/// reaches beyond the window, grey while there is nothing to hand over.
fn apply_button(enabled: bool) -> egui::Button<'static> {
let fill = if enabled {
APPLY_FILL
} else {
Color32::from_gray(45)
};
egui::Button::new("Apply").fill(fill)
}
#[derive(Default)]
pub(crate) struct FixtureWindow {
open: bool,
/// Set while the window still has to be brought forward.
raise: bool,
/// Set while the user is being asked what to do with unapplied changes.
closing: bool,
state: EditorState,
/// A run of list rows being dragged between the membership lists.
list_drag: Option<ListDrag>,
draft: Option<Draft>,
/// Set by Apply until `project_edits` has looked at the patch.
patch_applied: bool,
/// Set when the interface adopted the engine's project, until the draft
/// was compared with its patch.
show_patch_replaced: bool,
/// The fixture definition editor was asked for, on this definition if
/// any. The windows owning both editors hand it over.
definitions_request: Option<Option<AssetId<FixtureDefinition>>>,
}
impl std::fmt::Debug for FixtureWindow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FixtureWindow")
.field("open", &self.open)
.field("unapplied", &self.unapplied())
.finish_non_exhaustive()
}
}
impl FixtureWindow {
/// The window itself and the two dialogs that belong to it.
fn viewport() -> ViewportId {
ViewportId(Id::new("fixture editor window"))
}
fn picker_viewport() -> ViewportId {
ViewportId(Id::new("fixture member picker"))
}
fn closing_viewport() -> ViewportId {
ViewportId(Id::new("fixture editor closing"))
}
const TITLE: &str = "Gleditor";
const CLOSING_TITLE: &str = "Unapplied changes";
const PICKER_TITLE: &str = "Fixtures in this layout";
/// What the test screen, the inspector and the layout stack each need.
const TEST_SCREEN_MIN: f32 = 120.0;
const INSPECTOR_MIN: f32 = 160.0;
const LAYOUT_STACK_MIN: f32 = 160.0;
/// Whether the fixture definition editor was asked for since the last
/// call, and on which definition.
pub(crate) fn take_definitions_request(
&mut self,
) -> Option<Option<AssetId<FixtureDefinition>>> {
self.definitions_request.take()
}
pub(crate) fn open(&mut self) {
self.open = true;
self.raise = true;
}
#[cfg_attr(feature = "profiling", profiling::function)]
pub(crate) fn update(
&mut self,
ctx: &Context,
project: &mut Option<Project>,
collections: &Collections,
) {
if !self.open {
return;
}
ctx.show_viewport_immediate(
Self::viewport(),
tool_window_builder(Self::TITLE)
.with_inner_size(Vec2::new(1200.0, 800.0))
.with_min_inner_size(Vec2::new(660.0, 500.0)),
|ctx, _viewport_class| {
raise(ctx, &mut self.raise);
self.state.caches.begin_frame();
// what the toolbar is drawn with this frame
let mut shown = None;
gled_window_frame(ctx, Self::TITLE, |ui| {
if self.dialog().is_some() {
ui.disable();
}
let Some(project) = project.as_mut() else {
ui.centered_and_justified(|ui| ui.label("No project loaded"));
self.draft = None;
return;
};
// A draft taken from a patch the show has since replaced —
// another project was loaded — is not about this patch any
// more and is started over. Only the interface adopting
// the engine's project replaces the patch besides Apply,
// so it is compared only after that.
let replaced = std::mem::take(&mut self.show_patch_replaced);
let mut draft = match self
.draft
.take()
.filter(|draft| !replaced || draft.applied == project.patch)
{
Some(draft) => draft,
None => {
// an image chosen for the draft before is not
// about this one
self.state.background_pick = FilePick::default();
Draft::taken_from(&project.patch)
}
};
self.state.clamp(&draft.patch);
let status = draft.status();
shown = Some(status);
let apply = draft.edit(|edit| self.draw_window(ui, edit, collections, status));
if apply {
self.apply(&mut draft, project);
}
self.draft = Some(draft);
});
// Closing drops the draft, so changes nobody applied have to be
// asked about first.
if ctx.input(|input| input.viewport().close_requested())
|| embedded_close_clicked(Self::TITLE)
{
if self.unapplied() {
self.closing = true;
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
} else {
self.close();
}
}
// The dialogs are windows of their own, so they are shown
// beside this one rather than inside it.
if self.state.picking_members
&& let Some(mut draft) = self.draft.take()
{
draft.edit(|edit| self.member_picker(ctx, edit, collections));
self.draft = Some(draft);
}
if self.closing {
self.ask_before_closing(ctx, project);
}
self.keep_dialogs_in_front(ctx);
// Apply, undo and redo are drawn before the canvas and the
// dialogs change the draft. A frame showing them as they were
// stays on screen until the next one, which may not come
// before the next input, so it is drawn again instead.
if self.draft.as_ref().map(Draft::status) != shown {
ctx.request_discard("the patch editor's draft changed after its toolbar");
}
},
);
}
/// The window's panels and canvas on `edit`. Returns whether the draft is
/// to be handed to the show.
fn draw_window(
&mut self,
ui: &mut Ui,
edit: &mut PatchEdit<'_>,
collections: &Collections,
status: DraftStatus,
) -> bool {
egui::Panel::left("editor left panel")
.resizable(true)
.default_size(250.0)
.min_size(250.0)
.max_size(600.0)
.show(ui, |ui| {
// Each part keeps its floor, so a divider can only
// take room that is actually free and nothing is
// pushed out of the window.
let (test, inspector, stack) = Self::left_floors(ui.available_height());
let room = ui.available_height();
egui::Panel::top("layout test screen")
.resizable(true)
.default_size(200.0)
.size_range(Rangef::new(test, (room - inspector - stack).max(test)))
.show(ui, |ui| self.show_test_screen(ui, edit.value, collections));
let room = ui.available_height();
egui::Panel::top("fixture inspector")
.resizable(true)
.default_size(200.0)
.size_range(Rangef::new(inspector, (room - stack).max(inspector)))
.show(ui, |ui| self.show_inspector(ui, edit, collections));
self.layout_stack(ui, edit, collections);
});
self.draw_editor(ui, edit, collections, status)
}
/// Hands the draft to the show: the interface's project takes it, and the
/// edit goes to the engine with the next sync (`project_edits`), which
/// derives the positions and universes from it.
fn apply(&mut self, draft: &mut Draft, project: &mut Project) {
draft.apply_to(project);
self.patch_applied = true;
}
/// Whether Apply handed a patch to the interface's project since the last
/// call, which `project_edits` then compares with the show's.
pub(crate) fn take_patch_applied(&mut self) -> bool {
std::mem::take(&mut self.patch_applied)
}
/// Notes that the interface adopted the engine's project, which may hold
/// another patch than the one the draft was taken from.
pub(crate) fn project_replaced(&mut self) {
self.show_patch_replaced = true;
}
fn unapplied(&self) -> bool {
self.draft.as_ref().is_some_and(Draft::unapplied)
}
/// Closes the window and forgets the draft with it.
fn close(&mut self) {
self.open = false;
self.closing = false;
self.state.pick_members(false);
self.state.background_pick = FilePick::default();
self.draft = None;
}
/// Which dialog of this window is open, if any.
fn dialog(&self) -> Option<ViewportId> {
if self.closing {
Some(Self::closing_viewport())
} else if self.state.picking_members {
Some(Self::picker_viewport())
} else {
None
}
}
/// A dialog blocks the window it belongs to: bringing that window forward
/// hands the focus straight back to the dialog.
fn keep_dialogs_in_front(&self, ctx: &Context) {
if let Some(dialog) = self.dialog()
&& ctx.input(|input| input.viewport().focused.unwrap_or(false))
{
ctx.send_viewport_cmd_to(dialog, egui::ViewportCommand::Focus);
}
}
/// The three ways out of a window with unapplied changes.
fn ask_before_closing(&mut self, ctx: &Context, project: &mut Option<Project>) {
ctx.show_viewport_immediate(
Self::closing_viewport(),
dialog_viewport_builder(Self::CLOSING_TITLE).with_inner_size(Vec2::new(420.0, 150.0)),
|ctx, _viewport_class| {
// the window's own close button is the same as cancelling
if ctx.input(|input| input.viewport().close_requested())
|| embedded_close_clicked(Self::CLOSING_TITLE)
{
self.closing = false;
}
gled_window_frame(ctx, Self::CLOSING_TITLE, |ui| {
ui.add_space(6.0);
ui.label("The show is still running the patch as it was.");
ui.add_space(12.0);
ui.horizontal(|ui| {
if ui.button("Cancel").clicked() {
self.closing = false;
}
if ui.button("Discard").clicked() {
self.close();
}
if ui.add(apply_button(true)).clicked() {
if let (Some(mut draft), Some(project)) =
(self.draft.take(), project.as_mut())
{
self.apply(&mut draft, project);
}
self.close();
}
});
});
},
);
}
/// How much of `room` each part of the left panel keeps. A window too
/// small for all three shrinks them together — they scroll inside rather
/// than push one another out of it.
fn left_floors(room: f32) -> (f32, f32, f32) {
let wanted = Self::TEST_SCREEN_MIN + Self::INSPECTOR_MIN + Self::LAYOUT_STACK_MIN;
let share = (room / wanted).clamp(0.0, 1.0);
(
Self::TEST_SCREEN_MIN * share,
Self::INSPECTOR_MIN * share,
Self::LAYOUT_STACK_MIN * share,
)
}
/// What a test tile would look like on the selected layout and mode, so the
/// choice can be judged here rather than by patching a tile in the main
/// window.
fn show_test_screen(&mut self, ui: &mut Ui, patch: &Patch, collections: &Collections) {
ui.label(
egui::RichText::new("Test screen")
.small()
.color(Color32::from_gray(150)),
);
// It is square and has a size below which it says nothing, so a panel
// too short for it scrolls rather than shrinking it away.
let side = ui.available_size().min_elem().max(Self::TEST_SCREEN_MIN);
ScrollArea::both()
.id_salt("test screen")
.auto_shrink([false; 2])
.show(ui, |ui| {
// as wide as there is room for, so the square sits in the middle
let width = ui.available_width().max(side);
let (rect, _) = ui.allocate_exact_size(Vec2::new(width, side), Sense::hover());
test_screen::show(
ui,
rect,
patch,
LayoutSelection::new(self.state.selection, self.state.selected_mode),
collections,
);
});
}
}