egui-elements 1.3.0

Themes, widgets, and components for egui.
Documentation
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
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
//! [`Window`] — [`egui::Window`] with a title fill that stays put.
//!
//! Stock egui `title_ui` overwrites [`egui::Window::title_frame`] fill
//! with `visuals.widgets.open.weak_bg_fill` whenever the window is the top
//! layer. Opening a second window (Theme Editor on top of the demo window,
//! for example) flips that slot and the title bar changes color even if you
//! passed an explicit title frame.
//!
//! This wrapper forwards the stock builder API. On [`Window::show`] it pins
//! `widgets.open.weak_bg_fill` to the requested title fill so focused and
//! unfocused titles stay the same. The style is restored when `show` returns.

use egui::{
   Align2, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Order, Pos2, Rect, Resize, Ui,
   Vec2, Vec2b, ViewportBuilder, ViewportId, WindowDrag,
   scroll_area::{DragScroll, ScrollBarVisibility},
};

/// Builder for a floating window. Same API as [`egui::Window`].
///
/// The only behavior change: [`Self::title_frame`] fill is honored even when
/// this window is the top layer. Stock egui replaces that fill from
/// `widgets.open`.
///
///
/// ### This misbehavior is fixed in egui 0.36.2 - https://github.com/emilk/egui/pull/8400
///
///
/// ```
/// # use egui::__run_test_ctx;
/// # use egui_elements::widgets::Window;
/// # __run_test_ctx(|ctx| {
/// let frame = egui::Frame::window(&ctx.global_style());
/// Window::new("Demo")
///     .frame(frame)
///     .title_frame(frame.stroke(egui::Stroke::NONE))
///     .show(ctx, |ui| {
///         ui.label("hello");
///     });
/// # });
/// ```
#[must_use = "You should call .show()"]
pub struct Window<'a> {
   inner: egui::Window<'a>,
   /// Fill taken from [`Self::title_frame`]. `None` means stock default
   /// (`visuals.window_fill`).
   title_fill: Option<Color32>,
}

impl<'a> Window<'a> {
   /// The window title is used as a unique [`Id`] and must be unique, and
   /// should not change. See [`egui::Window::new`].
   pub fn new(title: impl IntoAtoms<'a>) -> Self {
      Self {
         inner: egui::Window::new(title),
         title_fill: None,
      }
   }

   /// Construct a [`Window`] that follows the given viewport.
   pub fn from_viewport(id: ViewportId, viewport: ViewportBuilder) -> Self {
      Self {
         inner: egui::Window::from_viewport(id, viewport),
         title_fill: None,
      }
   }

   /// Assign a unique id. Required if the title changes or is shared.
   #[inline]
   pub fn id(mut self, id: Id) -> Self {
      self.inner = self.inner.id(id);
      self
   }

   /// Add a close button. See [`egui::Window::open`].
   #[inline]
   pub fn open(mut self, open: &'a mut bool) -> Self {
      self.inner = self.inner.open(open);
      self
   }

   /// If `false` the window is grayed out and non-interactive.
   #[inline]
   pub fn enabled(mut self, enabled: bool) -> Self {
      self.inner = self.inner.enabled(enabled);
      self
   }

   /// If `false`, clicks go through to what is behind us. Default: `true`.
   #[inline]
   pub fn interactable(mut self, interactable: bool) -> Self {
      self.inner = self.inner.interactable(interactable);
      self
   }

   /// If `false` the window cannot be moved by dragging.
   #[inline]
   pub fn movable(mut self, movable: bool) -> Self {
      self.inner = self.inner.movable(movable);
      self
   }

   /// Where the user can grab the window to move it.
   #[inline]
   pub fn drag_area(mut self, drag_area: WindowDrag) -> Self {
      self.inner = self.inner.drag_area(drag_area);
      self
   }

   /// `order(Order::Foreground)` for a window that should always be on top.
   #[inline]
   pub fn order(mut self, order: Order) -> Self {
      self.inner = self.inner.order(order);
      self
   }

   /// Fade in when the window first appears. Default: `true`.
   #[inline]
   pub fn fade_in(mut self, fade_in: bool) -> Self {
      self.inner = self.inner.fade_in(fade_in);
      self
   }

   /// Fade out when the window closes via [`Self::open`]. Default: `true`.
   #[inline]
   pub fn fade_out(mut self, fade_out: bool) -> Self {
      self.inner = self.inner.fade_out(fade_out);
      self
   }

   /// Mutate the builder in place.
   #[inline]
   pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
      mutate(&mut self);
      self
   }

   /// Mutate the inner [`Resize`].
   #[inline]
   pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
      self.inner = self.inner.resize(mutate);
      self
   }

   /// Change the background color, margins, etc. of the window body.
   #[inline]
   pub fn frame(mut self, frame: Frame) -> Self {
      self.inner = self.inner.frame(frame);
      self
   }

   /// Change the background color, margins, etc. of the title bar.
   ///
   /// Unlike [`egui::Window::title_frame`], this fill is kept even when the
   /// window is the top layer.
   #[inline]
   pub fn title_frame(mut self, frame: Frame) -> Self {
      self.title_fill = Some(frame.fill);
      self.inner = self.inner.title_frame(frame);
      self
   }

   /// Set minimum width of the window.
   #[inline]
   pub fn min_width(mut self, min_width: f32) -> Self {
      self.inner = self.inner.min_width(min_width);
      self
   }

   /// Set minimum height of the window.
   #[inline]
   pub fn min_height(mut self, min_height: f32) -> Self {
      self.inner = self.inner.min_height(min_height);
      self
   }

   /// Set minimum outer size of the window.
   #[inline]
   pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
      self.inner = self.inner.min_size(min_size);
      self
   }

   /// Set maximum width of the window.
   #[inline]
   pub fn max_width(mut self, max_width: f32) -> Self {
      self.inner = self.inner.max_width(max_width);
      self
   }

   /// Set maximum height of the window.
   #[inline]
   pub fn max_height(mut self, max_height: f32) -> Self {
      self.inner = self.inner.max_height(max_height);
      self
   }

   /// Set maximum outer size of the window.
   #[inline]
   pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
      self.inner = self.inner.max_size(max_size);
      self
   }

   /// Set current position of the window.
   #[inline]
   pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
      self.inner = self.inner.current_pos(current_pos);
      self
   }

   /// Set initial position of the window.
   #[inline]
   pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
      self.inner = self.inner.default_pos(default_pos);
      self
   }

   /// Sets the window position and prevents it from being dragged.
   #[inline]
   pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
      self.inner = self.inner.fixed_pos(pos);
      self
   }

   /// Constrains this window to [`Context::content_rect`]. Default: `true`.
   #[inline]
   pub fn constrain(mut self, constrain: bool) -> Self {
      self.inner = self.inner.constrain(constrain);
      self
   }

   /// Constrain movement of the window to the given rectangle.
   #[inline]
   pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
      self.inner = self.inner.constrain_to(constrain_rect);
      self
   }

   /// Where the "root" of the window is. Default: [`Align2::LEFT_TOP`].
   #[inline]
   pub fn pivot(mut self, pivot: Align2) -> Self {
      self.inner = self.inner.pivot(pivot);
      self
   }

   /// Set anchor and distance. Also makes the window immovable.
   #[inline]
   pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
      self.inner = self.inner.anchor(align, offset);
      self
   }

   /// Set initial collapsed state of the window.
   #[inline]
   pub fn default_open(mut self, default_open: bool) -> Self {
      self.inner = self.inner.default_open(default_open);
      self
   }

   /// Set initial outer size of the window.
   #[inline]
   pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
      self.inner = self.inner.default_size(default_size);
      self
   }

   /// Set initial width of the window.
   #[inline]
   pub fn default_width(mut self, default_width: f32) -> Self {
      self.inner = self.inner.default_width(default_width);
      self
   }

   /// Set initial height of the window.
   #[inline]
   pub fn default_height(mut self, default_height: f32) -> Self {
      self.inner = self.inner.default_height(default_height);
      self
   }

   /// Sets the window size and prevents resizing by dragging edges.
   #[inline]
   pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
      self.inner = self.inner.fixed_size(size);
      self
   }

   /// Set initial position and size of the window.
   #[inline]
   pub fn default_rect(self, rect: Rect) -> Self {
      self.default_pos(rect.min).default_size(rect.size())
   }

   /// Sets pos and size and prevents moving and resizing by dragging.
   #[inline]
   pub fn fixed_rect(self, rect: Rect) -> Self {
      self.fixed_pos(rect.min).fixed_size(rect.size())
   }

   /// Can the user resize the window by dragging its edges? Default: `true`.
   #[inline]
   pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
      self.inner = self.inner.resizable(resizable);
      self
   }

   /// Can the window be collapsed by clicking on its title?
   #[inline]
   pub fn collapsible(mut self, collapsible: bool) -> Self {
      self.inner = self.inner.collapsible(collapsible);
      self
   }

   /// Show a title bar? If `false`, the window is not collapsible and has no
   /// close button.
   #[inline]
   pub fn title_bar(mut self, title_bar: bool) -> Self {
      self.inner = self.inner.title_bar(title_bar);
      self
   }

   /// Not resizable, just takes the size of its contents. Also disables
   /// scrolling.
   #[inline]
   pub fn auto_sized(mut self) -> Self {
      self.inner = self.inner.auto_sized();
      self
   }

   /// Enable/disable horizontal/vertical scrolling. `false` by default.
   #[inline]
   pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
      self.inner = self.inner.scroll(scroll);
      self
   }

   /// Enable/disable horizontal scrolling. `false` by default.
   #[inline]
   pub fn hscroll(mut self, hscroll: bool) -> Self {
      self.inner = self.inner.hscroll(hscroll);
      self
   }

   /// Enable/disable vertical scrolling. `false` by default.
   #[inline]
   pub fn vscroll(mut self, vscroll: bool) -> Self {
      self.inner = self.inner.vscroll(vscroll);
      self
   }

   /// Scroll by dragging the contents with the pointer.
   #[inline]
   pub fn drag_to_scroll(mut self, drag_to_scroll: DragScroll) -> Self {
      self.inner = self.inner.drag_to_scroll(drag_to_scroll);
      self
   }

   /// Sets the [`ScrollBarVisibility`] of the window.
   #[inline]
   pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
      self.inner = self.inner.scroll_bar_visibility(visibility);
      self
   }

   /// Show the window.
   ///
   /// Returns `None` if the window is not open (if [`Self::open`] was called
   /// with `&mut false`). Returns `Some(InnerResponse { inner: None })` if
   /// the window is collapsed.
   #[inline]
   pub fn show<R>(
      self,
      ctx: &Context,
      add_contents: impl FnOnce(&mut Ui) -> R,
   ) -> Option<InnerResponse<Option<R>>> {
      let fill = self.title_fill.unwrap_or_else(|| ctx.global_style().visuals.window_fill);
      let _guard = PinOpenWeakFill::new(ctx, fill);
      self.inner.show(ctx, add_contents)
   }
}

/// Pins `widgets.open.weak_bg_fill` for the duration of [`Window::show`].
///
/// Stock `title_ui` reads that slot when the window is on top. Restoring on
/// drop keeps Theme Editor color pickers / stock combos from inheriting the
/// title fill after the window returns.
struct PinOpenWeakFill {
   ctx: Context,
   previous: Color32,
}

impl PinOpenWeakFill {
   fn new(ctx: &Context, fill: Color32) -> Self {
      let previous = ctx.global_style().visuals.widgets.open.weak_bg_fill;
      ctx.global_style_mut(|style| {
         style.visuals.widgets.open.weak_bg_fill = fill;
      });
      Self {
         ctx: ctx.clone(),
         previous,
      }
   }
}

impl Drop for PinOpenWeakFill {
   fn drop(&mut self) {
      self.ctx.global_style_mut(|style| {
         style.visuals.widgets.open.weak_bg_fill = self.previous;
      });
   }
}

#[cfg(test)]
mod tests {
   use super::*;

   #[test]
   fn title_frame_fill_is_pinned_during_show_then_restored() {
      egui::__run_test_ctx(|ctx| {
         let original = Color32::from_rgb(9, 9, 9);
         ctx.global_style_mut(|style| {
            style.visuals.widgets.open.weak_bg_fill = original;
         });

         let title_fill = Color32::from_rgb(1, 2, 3);
         Window::new("title").title_frame(Frame::NONE.fill(title_fill)).show(ctx, |ui| {
            assert_eq!(
               ui.ctx().global_style().visuals.widgets.open.weak_bg_fill,
               title_fill
            );
         });

         assert_eq!(
            ctx.global_style().visuals.widgets.open.weak_bg_fill,
            original
         );
      });
   }
}