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