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 let Some(path) = self.build_lyon_path(size, border_offset) else {
142 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 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 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}