gpui_squircle/
lib.rs

1use figma_squircle::{FigmaSquircleParams, get_svg_path};
2use gpui::{
3    App, Background, Bounds, Element, ElementId, GlobalElementId, InspectorElementId,
4    InteractiveElement, Interactivity, IntoElement, LayoutId, PathBuilder, Pixels, Refineable,
5    Size, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window, point, px,
6};
7use lyon::{
8    extra::parser::{ParserOptions, PathParser, Source},
9    path::Path as LyonPath,
10};
11
12mod style;
13pub use style::{SquircleStyleRefinement, Styled as SquircleStyled};
14
15struct BuildAndPaintOptions {
16    builder: PathBuilder,
17    background: Background,
18}
19
20impl BuildAndPaintOptions {
21    fn fill(background: Background) -> Self {
22        Self {
23            builder: PathBuilder::fill(),
24            background,
25        }
26    }
27
28    fn stroke(background: Background, border_width: f32) -> Self {
29        Self {
30            builder: PathBuilder::stroke(px(border_width)),
31            background,
32        }
33    }
34}
35
36#[derive(Default, Clone, Copy)]
37pub enum BorderMode {
38    #[default]
39    Center,
40    Outside,
41    Inside,
42}
43
44pub struct Squircle {
45    pub style: SquircleStyleRefinement,
46    interactivity: Interactivity,
47}
48
49impl SquircleStyled for Squircle {
50    fn style(&mut self) -> &mut StyleRefinement {
51        &mut self.style.inner
52    }
53
54    fn outer_style(&mut self) -> &mut SquircleStyleRefinement {
55        &mut self.style
56    }
57}
58
59impl Squircle {
60    fn new() -> Self {
61        Self {
62            style: SquircleStyleRefinement::default(),
63            interactivity: Interactivity::new(),
64        }
65    }
66
67    pub fn absolute_expand(mut self) -> Self {
68        self.style.inner = self
69            .style
70            .inner
71            .size_full()
72            .absolute()
73            .top_0()
74            .bottom_0()
75            .left_0()
76            .right_0();
77
78        self
79    }
80
81    fn to_params(&self, width: f32, height: f32, border_offset: f32) -> FigmaSquircleParams {
82        let style = &self.style;
83        let corner_radii = &self.style.corner_radii;
84
85        FigmaSquircleParams {
86            width,
87            height,
88            corner_radius: None,
89            top_left_corner_radius: Some(
90                corner_radii.top_left.unwrap_or_default().to_f64() as f32 + border_offset,
91            ),
92            top_right_corner_radius: Some(
93                corner_radii.top_right.unwrap_or_default().to_f64() as f32 + border_offset,
94            ),
95            bottom_left_corner_radius: Some(
96                corner_radii.bottom_left.unwrap_or_default().to_f64() as f32 + border_offset,
97            ),
98            bottom_right_corner_radius: Some(
99                corner_radii.bottom_right.unwrap_or_default().to_f64() as f32 + border_offset,
100            ),
101            corner_smoothing: style.corner_smoothing.unwrap_or(px(1.)).to_f64() as f32,
102            preserve_smoothing: style.preserve_smoothing.unwrap_or(true),
103        }
104    }
105
106    fn build_lyon_path(&self, size: Size<Pixels>, border_offset: f32) -> Option<LyonPath> {
107        let svg_path = get_svg_path(self.to_params(
108            size.width.to_f64() as f32,
109            size.height.to_f64() as f32,
110            border_offset,
111        ));
112
113        let mut lyon_builder = LyonPath::builder();
114
115        let parsed = PathParser::new().parse(
116            &ParserOptions::DEFAULT,
117            &mut Source::new(svg_path.chars()),
118            &mut lyon_builder,
119        );
120
121        if parsed.is_err() {
122            return None;
123        }
124
125        Some(lyon_builder.build())
126    }
127
128    fn build_and_paint_paths<'a, const N: usize>(
129        &self,
130        window: &mut Window,
131        Bounds { origin, size }: Bounds<Pixels>,
132        size_offset: f32,
133        border_offset: f32,
134        mut options: [BuildAndPaintOptions; N],
135    ) {
136        let size_offset_px = px(size_offset);
137        let size = size - gpui::size(size_offset_px, size_offset_px);
138
139        // If the path doesn't exist then the svg is malformed.
140        // TODO: fallback to regular rounded rectangle if this case is met.
141        let Some(path) = self.build_lyon_path(size, border_offset) else {
142            //println!("malformed svg");
143            return;
144        };
145
146        let (origin_x, origin_y) = (
147            (origin.x + size_offset_px / 2.).to_f64() as f32,
148            (origin.y + size_offset_px / 2.).to_f64() as f32,
149        );
150
151        for event in path.iter() {
152            match event {
153                lyon::path::Event::Begin { at } => {
154                    let at = point(px(origin_x + at.x), px(origin_y + at.y));
155
156                    for BuildAndPaintOptions { builder, .. } in options.as_mut() {
157                        builder.move_to(at)
158                    }
159                }
160
161                lyon::path::Event::Line { from: _, to } => {
162                    let to = point(px(origin_x + to.x), px(origin_y + to.y));
163
164                    for BuildAndPaintOptions { builder, .. } in options.as_mut() {
165                        builder.line_to(to)
166                    }
167                }
168
169                lyon::path::Event::Quadratic { from: _, ctrl, to } => {
170                    let to = point(px(origin_x + to.x), px(origin_y + to.y));
171                    let ctrl = point(px(origin_x + ctrl.x), px(origin_y + ctrl.y));
172
173                    for BuildAndPaintOptions { builder, .. } in options.as_mut() {
174                        builder.curve_to(to, ctrl);
175                    }
176                }
177
178                lyon::path::Event::Cubic {
179                    from: _,
180                    ctrl1,
181                    ctrl2,
182                    to,
183                } => {
184                    let to = point(px(origin_x + to.x), px(origin_y + to.y));
185                    let ctrl1 = point(px(origin_x + ctrl1.x), px(origin_y + ctrl1.y));
186                    let ctrl2 = point(px(origin_x + ctrl2.x), px(origin_y + ctrl2.y));
187
188                    for BuildAndPaintOptions { builder, .. } in options.as_mut() {
189                        builder.cubic_bezier_to(to, ctrl1, ctrl2)
190                    }
191                }
192
193                lyon::path::Event::End { close, .. } => {
194                    if close {
195                        for BuildAndPaintOptions { builder, .. } in options.as_mut() {
196                            builder.close()
197                        }
198                    }
199                }
200            }
201        }
202
203        for BuildAndPaintOptions {
204            builder,
205            background,
206            ..
207        } in options
208        {
209            let Ok(path) = builder.build() else { continue };
210            window.paint_path(path, background);
211        }
212    }
213
214    #[inline(always)]
215    fn get_size_and_border_offsets(&self, border_width: f32) -> (f32, f32) {
216        match self.style.border_mode.unwrap_or_default() {
217            BorderMode::Outside => (-border_width, border_width - 2.),
218            BorderMode::Inside => (border_width, -(border_width - 1.)),
219            BorderMode::Center => (0., 0.),
220        }
221    }
222}
223
224impl Element for Squircle {
225    type RequestLayoutState = Style;
226    type PrepaintState = Option<()>;
227
228    fn id(&self) -> Option<ElementId> {
229        self.interactivity.element_id.clone()
230    }
231
232    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
233        self.interactivity.source_location()
234    }
235
236    fn request_layout(
237        &mut self,
238        _id: Option<&GlobalElementId>,
239        _inspector_id: Option<&InspectorElementId>,
240        window: &mut Window,
241        cx: &mut App,
242    ) -> (LayoutId, Self::RequestLayoutState) {
243        let mut style = Style::default();
244        style.refine(&self.style.inner);
245
246        let layout_id = window.request_layout(style.clone(), [], cx);
247        (layout_id, style)
248    }
249
250    fn prepaint(
251        &mut self,
252        _id: Option<&GlobalElementId>,
253        _inspector_id: Option<&InspectorElementId>,
254        _bounds: Bounds<Pixels>,
255        _request_layout: &mut Style,
256        _window: &mut Window,
257        _cx: &mut App,
258    ) -> Option<()> {
259        None
260    }
261
262    fn paint(
263        &mut self,
264        _id: Option<&GlobalElementId>,
265        _inspector_id: Option<&InspectorElementId>,
266        bounds: Bounds<Pixels>,
267        style: &mut Style,
268        _prepaint: &mut Self::PrepaintState,
269        window: &mut Window,
270        cx: &mut App,
271    ) {
272        let style_refinement = &self.style;
273
274        style.refine(&style_refinement.inner);
275
276        style.paint(bounds, window, cx, |window, _cx| {
277            match (style_refinement.background, style_refinement.border_color) {
278                (Some(bg), None) => {
279                    self.build_and_paint_paths(
280                        window,
281                        bounds,
282                        0.,
283                        0.,
284                        [BuildAndPaintOptions::fill(bg)],
285                    );
286                }
287
288                (Some(bg), Some(border_color)) => {
289                    let border_width =
290                        style_refinement.border_width.unwrap_or_default().to_f64() as f32;
291                    let (size_offset, border_offset) =
292                        self.get_size_and_border_offsets(border_width);
293
294                    if size_offset == 0. {
295                        // We can generate the same path for both the fill and the stroke.
296
297                        self.build_and_paint_paths(
298                            window,
299                            bounds,
300                            0.,
301                            border_offset,
302                            [
303                                BuildAndPaintOptions::fill(bg),
304                                BuildAndPaintOptions::stroke(border_color, border_width),
305                            ],
306                        );
307                    } else {
308                        // We need to generate differen't paths for the fill and
309                        // stroke as they have different corner radius's and size's.
310
311                        self.build_and_paint_paths(
312                            window,
313                            bounds,
314                            0.,
315                            0.,
316                            [BuildAndPaintOptions::fill(bg)],
317                        );
318
319                        self.build_and_paint_paths(
320                            window,
321                            bounds,
322                            size_offset,
323                            border_offset,
324                            [BuildAndPaintOptions::stroke(border_color, border_width)],
325                        );
326                    }
327                }
328
329                (None, None) => (),
330
331                (None, Some(border_color)) => {
332                    let border_width =
333                        style_refinement.border_width.unwrap_or_default().to_f64() as f32;
334
335                    let (size_offset, border_offset) =
336                        self.get_size_and_border_offsets(border_width);
337
338                    self.build_and_paint_paths(
339                        window,
340                        bounds,
341                        size_offset,
342                        border_offset,
343                        [BuildAndPaintOptions::stroke(border_color, border_width)],
344                    );
345                }
346            }
347        });
348    }
349}
350
351impl IntoElement for Squircle {
352    type Element = Self;
353
354    fn into_element(self) -> Self::Element {
355        self
356    }
357}
358
359impl InteractiveElement for Squircle {
360    fn interactivity(&mut self) -> &mut Interactivity {
361        &mut self.interactivity
362    }
363}
364
365impl StatefulInteractiveElement for Squircle {}
366
367pub fn squircle() -> Squircle {
368    Squircle::new()
369}