Skip to main content

blockworx/canvas/
egui_compat.rs

1//! The toolkit boundary's dictionary: geom and paint ↔ egui, one conversion per
2//! type.
3//!
4//! Two extension traits rather than `From` impls, because neither
5//! `blockworx-geom` nor `blockworx-paint` names an egui-family crate — normal,
6//! build or dev — so neither side of a pair can own the impl. A call site says
7//! `.egui()` when it is about to hand a value to an egui API and `.geom()` when
8//! it has just taken one back, which reads as the crossing it is.
9
10use blockworx_geom::{Align, Align2, Pos2, Rangef, Rect, Vec2};
11use blockworx_paint::{AnimKey, Base, Color, Cursor, EditId, Font, FontFamily, Palette, Waker};
12
13/// Geom → egui, at the point an egui API is called.
14pub trait IntoEgui {
15    type Out;
16
17    fn egui(self) -> Self::Out;
18}
19
20/// egui → geom, at the point a value comes back across the boundary.
21pub trait IntoGeom {
22    type Out;
23
24    fn geom(self) -> Self::Out;
25}
26
27/// One `x`/`y` pair to another.
28macro_rules! xy {
29    ($ours:ty, $theirs:ty) => {
30        impl IntoEgui for $ours {
31            type Out = $theirs;
32
33            #[inline]
34            fn egui(self) -> Self::Out {
35                <$theirs>::new(self.x, self.y)
36            }
37        }
38
39        impl IntoGeom for $theirs {
40            type Out = $ours;
41
42            #[inline]
43            fn geom(self) -> Self::Out {
44                <$ours>::new(self.x, self.y)
45            }
46        }
47    };
48}
49
50xy!(Pos2, egui::Pos2);
51xy!(Vec2, egui::Vec2);
52
53impl IntoEgui for Rect {
54    type Out = egui::Rect;
55
56    #[inline]
57    fn egui(self) -> Self::Out {
58        egui::Rect::from_min_max(self.min.egui(), self.max.egui())
59    }
60}
61
62impl IntoGeom for egui::Rect {
63    type Out = Rect;
64
65    #[inline]
66    fn geom(self) -> Self::Out {
67        Rect::from_min_max(self.min.geom(), self.max.geom())
68    }
69}
70
71impl IntoEgui for Rangef {
72    type Out = egui::Rangef;
73
74    #[inline]
75    fn egui(self) -> Self::Out {
76        egui::Rangef::new(self.min, self.max)
77    }
78}
79
80impl IntoGeom for egui::Rangef {
81    type Out = Rangef;
82
83    #[inline]
84    fn geom(self) -> Self::Out {
85        Rangef::new(self.min, self.max)
86    }
87}
88
89impl IntoEgui for Align {
90    type Out = egui::Align;
91
92    #[inline]
93    fn egui(self) -> Self::Out {
94        match self {
95            Align::Min => egui::Align::Min,
96            Align::Center => egui::Align::Center,
97            Align::Max => egui::Align::Max,
98        }
99    }
100}
101
102impl IntoGeom for egui::Align {
103    type Out = Align;
104
105    #[inline]
106    fn geom(self) -> Self::Out {
107        match self {
108            egui::Align::Min => Align::Min,
109            egui::Align::Center => Align::Center,
110            egui::Align::Max => Align::Max,
111        }
112    }
113}
114
115impl IntoEgui for Align2 {
116    type Out = egui::Align2;
117
118    #[inline]
119    fn egui(self) -> Self::Out {
120        egui::Align2([self.x().egui(), self.y().egui()])
121    }
122}
123
124impl IntoGeom for egui::Align2 {
125    type Out = Align2;
126
127    #[inline]
128    fn geom(self) -> Self::Out {
129        Align2([self.x().geom(), self.y().geom()])
130    }
131}
132
133impl IntoEgui for Color {
134    type Out = egui::Color32;
135
136    #[inline]
137    fn egui(self) -> Self::Out {
138        let [r, g, b, a] = self.to_array();
139        egui::Color32::from_rgba_premultiplied(r, g, b, a)
140    }
141}
142
143impl IntoGeom for egui::Color32 {
144    type Out = Color;
145
146    #[inline]
147    fn geom(self) -> Self::Out {
148        let [r, g, b, a] = self.to_array();
149        Color::from_rgba_premultiplied(r, g, b, a)
150    }
151}
152
153impl IntoEgui for FontFamily {
154    type Out = egui::FontFamily;
155
156    #[inline]
157    fn egui(self) -> Self::Out {
158        match self {
159            FontFamily::Proportional => egui::FontFamily::Proportional,
160            FontFamily::Monospace => egui::FontFamily::Monospace,
161            FontFamily::Named(name) => egui::FontFamily::Name(name),
162        }
163    }
164}
165
166impl IntoGeom for egui::FontFamily {
167    type Out = FontFamily;
168
169    #[inline]
170    fn geom(self) -> Self::Out {
171        match self {
172            egui::FontFamily::Proportional => FontFamily::Proportional,
173            egui::FontFamily::Monospace => FontFamily::Monospace,
174            egui::FontFamily::Name(name) => FontFamily::Named(name),
175        }
176    }
177}
178
179impl IntoEgui for Font {
180    type Out = egui::FontId;
181
182    #[inline]
183    fn egui(self) -> Self::Out {
184        egui::FontId::new(self.size, self.family.egui())
185    }
186}
187
188impl IntoEgui for &Font {
189    type Out = egui::FontId;
190
191    #[inline]
192    fn egui(self) -> Self::Out {
193        self.clone().egui()
194    }
195}
196
197impl IntoGeom for egui::FontId {
198    type Out = Font;
199
200    #[inline]
201    fn geom(self) -> Self::Out {
202        Font::new(self.size, self.family.geom())
203    }
204}
205
206impl IntoEgui for Cursor {
207    type Out = egui::CursorIcon;
208
209    #[inline]
210    fn egui(self) -> Self::Out {
211        match self {
212            Cursor::Default => egui::CursorIcon::Default,
213            Cursor::Crosshair => egui::CursorIcon::Crosshair,
214            Cursor::Text => egui::CursorIcon::Text,
215            Cursor::PointingHand => egui::CursorIcon::PointingHand,
216            Cursor::Move => egui::CursorIcon::Move,
217            Cursor::Grab => egui::CursorIcon::Grab,
218            Cursor::Grabbing => egui::CursorIcon::Grabbing,
219        }
220    }
221}
222
223impl IntoEgui for EditId {
224    type Out = egui::Id;
225
226    #[inline]
227    fn egui(self) -> Self::Out {
228        egui::Id::new(self.get())
229    }
230}
231
232impl IntoEgui for AnimKey {
233    type Out = egui::Id;
234
235    #[inline]
236    fn egui(self) -> Self::Out {
237        egui::Id::new(self.get())
238    }
239}
240
241/// A [`Waker`] that asks `ctx` for another frame. The one place a repaint
242/// request becomes shareable across threads, so the canvas and the shell hand
243/// out the same thing.
244pub fn waker(ctx: &egui::Context) -> Waker {
245    let ctx = ctx.clone();
246    Waker::new(move || ctx.request_repaint())
247}
248
249/// Build egui's [`Visuals`](egui::Visuals) from `palette` so the chrome egui
250/// draws itself — menus, buttons, checkboxes, separators and the in-place rename
251/// `TextEdit` — matches the canvas. Starts from the light or dark preset (per the
252/// scheme's own luminance) and overrides the colors that read against our
253/// background.
254pub fn visuals(palette: &Palette) -> egui::Visuals {
255    use egui::Stroke;
256    let c = |b| palette.get(b).egui();
257    let mut v = if palette.is_dark() {
258        egui::Visuals::dark()
259    } else {
260        egui::Visuals::light()
261    };
262
263    v.override_text_color = Some(c(Base::B05));
264    v.panel_fill = c(Base::B00);
265    v.window_fill = c(Base::B01);
266    v.window_stroke = Stroke::new(1.0, c(Base::B02));
267    v.extreme_bg_color = c(Base::B00); // text-edit background
268    v.faint_bg_color = c(Base::B01);
269    v.code_bg_color = c(Base::B01);
270    v.hyperlink_color = c(Base::B0D);
271    v.selection.bg_fill = c(Base::B0D).gamma_multiply(0.4);
272    v.selection.stroke = Stroke::new(1.0, c(Base::B05));
273
274    let w = &mut v.widgets;
275    w.noninteractive.bg_fill = c(Base::B01);
276    w.noninteractive.weak_bg_fill = c(Base::B01);
277    w.noninteractive.bg_stroke = Stroke::new(1.0, c(Base::B02));
278    w.noninteractive.fg_stroke = Stroke::new(1.0, c(Base::B05));
279
280    w.inactive.bg_fill = c(Base::B02);
281    w.inactive.weak_bg_fill = c(Base::B01);
282    w.inactive.bg_stroke = Stroke::new(1.0, c(Base::B02));
283    w.inactive.fg_stroke = Stroke::new(1.0, c(Base::B05));
284
285    w.hovered.bg_fill = c(Base::B03);
286    w.hovered.weak_bg_fill = c(Base::B02);
287    w.hovered.bg_stroke = Stroke::new(1.0, c(Base::B0D));
288    w.hovered.fg_stroke = Stroke::new(1.5, c(Base::B06));
289
290    w.active.bg_fill = c(Base::B0D);
291    w.active.weak_bg_fill = c(Base::B03);
292    w.active.bg_stroke = Stroke::new(1.0, c(Base::B0D));
293    w.active.fg_stroke = Stroke::new(2.0, c(Base::B00));
294
295    w.open.bg_fill = c(Base::B02);
296    w.open.weak_bg_fill = c(Base::B01);
297    w.open.bg_stroke = Stroke::new(1.0, c(Base::B02));
298    w.open.fg_stroke = Stroke::new(1.0, c(Base::B05));
299
300    v
301}
302
303#[cfg(test)]
304mod tests;