Skip to main content

egui_elements/widgets/
window.rs

1//! [`Window`] — [`egui::Window`] with a title fill that stays put.
2//!
3//! Stock egui `title_ui` overwrites [`egui::Window::title_frame`] fill
4//! with `visuals.widgets.open.weak_bg_fill` whenever the window is the top
5//! layer. Opening a second window (Theme Editor on top of the demo window,
6//! for example) flips that slot and the title bar changes color even if you
7//! passed an explicit title frame.
8//!
9//! This wrapper forwards the stock builder API. On [`Window::show`] it pins
10//! `widgets.open.weak_bg_fill` to the requested title fill so focused and
11//! unfocused titles stay the same. The style is restored when `show` returns.
12
13use egui::{
14   Align2, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Order, Pos2, Rect, Resize, Ui,
15   Vec2, Vec2b, ViewportBuilder, ViewportId, WindowDrag,
16   scroll_area::{DragScroll, ScrollBarVisibility},
17};
18
19/// Builder for a floating window. Same API as [`egui::Window`].
20///
21/// The only behavior change: [`Self::title_frame`] fill is honored even when
22/// this window is the top layer. Stock egui replaces that fill from
23/// `widgets.open`.
24///
25/// ```
26/// # use egui::__run_test_ctx;
27/// # use egui_elements::widgets::Window;
28/// # __run_test_ctx(|ctx| {
29/// let frame = egui::Frame::window(&ctx.global_style());
30/// Window::new("Demo")
31///     .frame(frame)
32///     .title_frame(frame.stroke(egui::Stroke::NONE))
33///     .show(ctx, |ui| {
34///         ui.label("hello");
35///     });
36/// # });
37/// ```
38#[must_use = "You should call .show()"]
39pub struct Window<'a> {
40   inner: egui::Window<'a>,
41   /// Fill taken from [`Self::title_frame`]. `None` means stock default
42   /// (`visuals.window_fill`).
43   title_fill: Option<Color32>,
44}
45
46impl<'a> Window<'a> {
47   /// The window title is used as a unique [`Id`] and must be unique, and
48   /// should not change. See [`egui::Window::new`].
49   pub fn new(title: impl IntoAtoms<'a>) -> Self {
50      Self {
51         inner: egui::Window::new(title),
52         title_fill: None,
53      }
54   }
55
56   /// Construct a [`Window`] that follows the given viewport.
57   pub fn from_viewport(id: ViewportId, viewport: ViewportBuilder) -> Self {
58      Self {
59         inner: egui::Window::from_viewport(id, viewport),
60         title_fill: None,
61      }
62   }
63
64   /// Assign a unique id. Required if the title changes or is shared.
65   #[inline]
66   pub fn id(mut self, id: Id) -> Self {
67      self.inner = self.inner.id(id);
68      self
69   }
70
71   /// Add a close button. See [`egui::Window::open`].
72   #[inline]
73   pub fn open(mut self, open: &'a mut bool) -> Self {
74      self.inner = self.inner.open(open);
75      self
76   }
77
78   /// If `false` the window is grayed out and non-interactive.
79   #[inline]
80   pub fn enabled(mut self, enabled: bool) -> Self {
81      self.inner = self.inner.enabled(enabled);
82      self
83   }
84
85   /// If `false`, clicks go through to what is behind us. Default: `true`.
86   #[inline]
87   pub fn interactable(mut self, interactable: bool) -> Self {
88      self.inner = self.inner.interactable(interactable);
89      self
90   }
91
92   /// If `false` the window cannot be moved by dragging.
93   #[inline]
94   pub fn movable(mut self, movable: bool) -> Self {
95      self.inner = self.inner.movable(movable);
96      self
97   }
98
99   /// Where the user can grab the window to move it.
100   #[inline]
101   pub fn drag_area(mut self, drag_area: WindowDrag) -> Self {
102      self.inner = self.inner.drag_area(drag_area);
103      self
104   }
105
106   /// `order(Order::Foreground)` for a window that should always be on top.
107   #[inline]
108   pub fn order(mut self, order: Order) -> Self {
109      self.inner = self.inner.order(order);
110      self
111   }
112
113   /// Fade in when the window first appears. Default: `true`.
114   #[inline]
115   pub fn fade_in(mut self, fade_in: bool) -> Self {
116      self.inner = self.inner.fade_in(fade_in);
117      self
118   }
119
120   /// Fade out when the window closes via [`Self::open`]. Default: `true`.
121   #[inline]
122   pub fn fade_out(mut self, fade_out: bool) -> Self {
123      self.inner = self.inner.fade_out(fade_out);
124      self
125   }
126
127   /// Mutate the builder in place.
128   #[inline]
129   pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
130      mutate(&mut self);
131      self
132   }
133
134   /// Mutate the inner [`Resize`].
135   #[inline]
136   pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
137      self.inner = self.inner.resize(mutate);
138      self
139   }
140
141   /// Change the background color, margins, etc. of the window body.
142   #[inline]
143   pub fn frame(mut self, frame: Frame) -> Self {
144      self.inner = self.inner.frame(frame);
145      self
146   }
147
148   /// Change the background color, margins, etc. of the title bar.
149   ///
150   /// Unlike [`egui::Window::title_frame`], this fill is kept even when the
151   /// window is the top layer.
152   #[inline]
153   pub fn title_frame(mut self, frame: Frame) -> Self {
154      self.title_fill = Some(frame.fill);
155      self.inner = self.inner.title_frame(frame);
156      self
157   }
158
159   /// Set minimum width of the window.
160   #[inline]
161   pub fn min_width(mut self, min_width: f32) -> Self {
162      self.inner = self.inner.min_width(min_width);
163      self
164   }
165
166   /// Set minimum height of the window.
167   #[inline]
168   pub fn min_height(mut self, min_height: f32) -> Self {
169      self.inner = self.inner.min_height(min_height);
170      self
171   }
172
173   /// Set minimum outer size of the window.
174   #[inline]
175   pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
176      self.inner = self.inner.min_size(min_size);
177      self
178   }
179
180   /// Set maximum width of the window.
181   #[inline]
182   pub fn max_width(mut self, max_width: f32) -> Self {
183      self.inner = self.inner.max_width(max_width);
184      self
185   }
186
187   /// Set maximum height of the window.
188   #[inline]
189   pub fn max_height(mut self, max_height: f32) -> Self {
190      self.inner = self.inner.max_height(max_height);
191      self
192   }
193
194   /// Set maximum outer size of the window.
195   #[inline]
196   pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
197      self.inner = self.inner.max_size(max_size);
198      self
199   }
200
201   /// Set current position of the window.
202   #[inline]
203   pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
204      self.inner = self.inner.current_pos(current_pos);
205      self
206   }
207
208   /// Set initial position of the window.
209   #[inline]
210   pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
211      self.inner = self.inner.default_pos(default_pos);
212      self
213   }
214
215   /// Sets the window position and prevents it from being dragged.
216   #[inline]
217   pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
218      self.inner = self.inner.fixed_pos(pos);
219      self
220   }
221
222   /// Constrains this window to [`Context::content_rect`]. Default: `true`.
223   #[inline]
224   pub fn constrain(mut self, constrain: bool) -> Self {
225      self.inner = self.inner.constrain(constrain);
226      self
227   }
228
229   /// Constrain movement of the window to the given rectangle.
230   #[inline]
231   pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
232      self.inner = self.inner.constrain_to(constrain_rect);
233      self
234   }
235
236   /// Where the "root" of the window is. Default: [`Align2::LEFT_TOP`].
237   #[inline]
238   pub fn pivot(mut self, pivot: Align2) -> Self {
239      self.inner = self.inner.pivot(pivot);
240      self
241   }
242
243   /// Set anchor and distance. Also makes the window immovable.
244   #[inline]
245   pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
246      self.inner = self.inner.anchor(align, offset);
247      self
248   }
249
250   /// Set initial collapsed state of the window.
251   #[inline]
252   pub fn default_open(mut self, default_open: bool) -> Self {
253      self.inner = self.inner.default_open(default_open);
254      self
255   }
256
257   /// Set initial outer size of the window.
258   #[inline]
259   pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
260      self.inner = self.inner.default_size(default_size);
261      self
262   }
263
264   /// Set initial width of the window.
265   #[inline]
266   pub fn default_width(mut self, default_width: f32) -> Self {
267      self.inner = self.inner.default_width(default_width);
268      self
269   }
270
271   /// Set initial height of the window.
272   #[inline]
273   pub fn default_height(mut self, default_height: f32) -> Self {
274      self.inner = self.inner.default_height(default_height);
275      self
276   }
277
278   /// Sets the window size and prevents resizing by dragging edges.
279   #[inline]
280   pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
281      self.inner = self.inner.fixed_size(size);
282      self
283   }
284
285   /// Set initial position and size of the window.
286   #[inline]
287   pub fn default_rect(self, rect: Rect) -> Self {
288      self.default_pos(rect.min).default_size(rect.size())
289   }
290
291   /// Sets pos and size and prevents moving and resizing by dragging.
292   #[inline]
293   pub fn fixed_rect(self, rect: Rect) -> Self {
294      self.fixed_pos(rect.min).fixed_size(rect.size())
295   }
296
297   /// Can the user resize the window by dragging its edges? Default: `true`.
298   #[inline]
299   pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
300      self.inner = self.inner.resizable(resizable);
301      self
302   }
303
304   /// Can the window be collapsed by clicking on its title?
305   #[inline]
306   pub fn collapsible(mut self, collapsible: bool) -> Self {
307      self.inner = self.inner.collapsible(collapsible);
308      self
309   }
310
311   /// Show a title bar? If `false`, the window is not collapsible and has no
312   /// close button.
313   #[inline]
314   pub fn title_bar(mut self, title_bar: bool) -> Self {
315      self.inner = self.inner.title_bar(title_bar);
316      self
317   }
318
319   /// Not resizable, just takes the size of its contents. Also disables
320   /// scrolling.
321   #[inline]
322   pub fn auto_sized(mut self) -> Self {
323      self.inner = self.inner.auto_sized();
324      self
325   }
326
327   /// Enable/disable horizontal/vertical scrolling. `false` by default.
328   #[inline]
329   pub fn scroll(mut self, scroll: impl Into<Vec2b>) -> Self {
330      self.inner = self.inner.scroll(scroll);
331      self
332   }
333
334   /// Enable/disable horizontal scrolling. `false` by default.
335   #[inline]
336   pub fn hscroll(mut self, hscroll: bool) -> Self {
337      self.inner = self.inner.hscroll(hscroll);
338      self
339   }
340
341   /// Enable/disable vertical scrolling. `false` by default.
342   #[inline]
343   pub fn vscroll(mut self, vscroll: bool) -> Self {
344      self.inner = self.inner.vscroll(vscroll);
345      self
346   }
347
348   /// Scroll by dragging the contents with the pointer.
349   #[inline]
350   pub fn drag_to_scroll(mut self, drag_to_scroll: DragScroll) -> Self {
351      self.inner = self.inner.drag_to_scroll(drag_to_scroll);
352      self
353   }
354
355   /// Sets the [`ScrollBarVisibility`] of the window.
356   #[inline]
357   pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self {
358      self.inner = self.inner.scroll_bar_visibility(visibility);
359      self
360   }
361
362   /// Show the window.
363   ///
364   /// Returns `None` if the window is not open (if [`Self::open`] was called
365   /// with `&mut false`). Returns `Some(InnerResponse { inner: None })` if
366   /// the window is collapsed.
367   #[inline]
368   pub fn show<R>(
369      self,
370      ctx: &Context,
371      add_contents: impl FnOnce(&mut Ui) -> R,
372   ) -> Option<InnerResponse<Option<R>>> {
373      let fill = self.title_fill.unwrap_or_else(|| ctx.global_style().visuals.window_fill);
374      let _guard = PinOpenWeakFill::new(ctx, fill);
375      self.inner.show(ctx, add_contents)
376   }
377}
378
379/// Pins `widgets.open.weak_bg_fill` for the duration of [`Window::show`].
380///
381/// Stock `title_ui` reads that slot when the window is on top. Restoring on
382/// drop keeps Theme Editor color pickers / stock combos from inheriting the
383/// title fill after the window returns.
384struct PinOpenWeakFill {
385   ctx: Context,
386   previous: Color32,
387}
388
389impl PinOpenWeakFill {
390   fn new(ctx: &Context, fill: Color32) -> Self {
391      let previous = ctx.global_style().visuals.widgets.open.weak_bg_fill;
392      ctx.global_style_mut(|style| {
393         style.visuals.widgets.open.weak_bg_fill = fill;
394      });
395      Self {
396         ctx: ctx.clone(),
397         previous,
398      }
399   }
400}
401
402impl Drop for PinOpenWeakFill {
403   fn drop(&mut self) {
404      self.ctx.global_style_mut(|style| {
405         style.visuals.widgets.open.weak_bg_fill = self.previous;
406      });
407   }
408}
409
410#[cfg(test)]
411mod tests {
412   use super::*;
413
414   #[test]
415   fn title_frame_fill_is_pinned_during_show_then_restored() {
416      egui::__run_test_ctx(|ctx| {
417         let original = Color32::from_rgb(9, 9, 9);
418         ctx.global_style_mut(|style| {
419            style.visuals.widgets.open.weak_bg_fill = original;
420         });
421
422         let title_fill = Color32::from_rgb(1, 2, 3);
423         Window::new("title").title_frame(Frame::NONE.fill(title_fill)).show(ctx, |ui| {
424            assert_eq!(
425               ui.ctx().global_style().visuals.widgets.open.weak_bg_fill,
426               title_fill
427            );
428         });
429
430         assert_eq!(
431            ctx.global_style().visuals.widgets.open.weak_bg_fill,
432            original
433         );
434      });
435   }
436}