1use teksilo_canvas::{Canvas, EdgeInsets, Path, Point, Rect, Size, SizeProposal};
13use teksilo_core::accessibility::AccessNodeBuilder;
14use teksilo_core::build_context::BuildContext;
15use teksilo_core::overlay::OverlayPlacement;
16use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
17use teksilo_core::widget_id::WidgetId;
18use teksilo_tokens::{CornerRadius, SurfaceRole};
19
20pub struct PopoverSurface {
21 content_id: Option<WidgetId>,
22 pending_content: Option<PendingChild>,
23 placement: OverlayPlacement,
24 show_caret: bool,
25 caret_size: f32,
26 name: String,
28 content_padding: EdgeInsets,
32 background: SurfaceRole,
34 corner_radius: f32,
36 presentational: bool,
41}
42
43impl PopoverSurface {
44 #[allow(clippy::too_many_arguments)]
45 pub fn new(
46 content: PendingChild,
47 placement: OverlayPlacement,
48 show_caret: bool,
49 caret_size: f32,
50 name: String,
51 content_padding: EdgeInsets,
52 background: SurfaceRole,
53 corner_radius: f32,
54 presentational: bool,
55 ) -> Self {
56 Self {
57 content_id: None,
58 pending_content: Some(content),
59 placement,
60 show_caret,
61 caret_size,
62 name,
63 content_padding,
64 background,
65 corner_radius,
66 presentational,
67 }
68 }
69
70 fn attached_shadow_side(
80 &self,
81 layout_direction: teksilo_core::environment::LayoutDirection,
82 ) -> Option<crate::shadow::AttachedSide> {
83 use teksilo_core::environment::LayoutDirection;
84 match self.placement {
85 OverlayPlacement::Below
86 | OverlayPlacement::BelowPreferred
87 | OverlayPlacement::NearAnchor { .. } => Some(crate::shadow::AttachedSide::Top),
88 OverlayPlacement::Above => Some(crate::shadow::AttachedSide::Bottom),
89 OverlayPlacement::TrailingEdge => match layout_direction {
90 LayoutDirection::LeftToRight => Some(crate::shadow::AttachedSide::Left),
91 LayoutDirection::RightToLeft => Some(crate::shadow::AttachedSide::Right),
92 },
93 _ => None,
94 }
95 }
96
97 fn caret_insets(&self) -> (f32, f32) {
98 if !self.show_caret {
99 return (0.0, 0.0);
100 }
101
102 match self.placement {
103 OverlayPlacement::Below
104 | OverlayPlacement::BelowPreferred
105 | OverlayPlacement::NearAnchor { .. } => (self.caret_size, 0.0),
106 OverlayPlacement::Above => (0.0, self.caret_size),
107 _ => (0.0, 0.0),
108 }
109 }
110
111 fn panel_bounds(&self, bounds: Rect) -> Rect {
112 let (top, bottom) = self.caret_insets();
113 Rect::new(
114 bounds.x,
115 bounds.y + top,
116 bounds.width,
117 (bounds.height - top - bottom).max(0.0),
118 )
119 }
120
121 fn caret_path(&self, bounds: Rect) -> Option<Path> {
122 if !self.show_caret {
123 return None;
124 }
125
126 let panel = self.panel_bounds(bounds);
127 let center_x = panel.x + panel.width.min(56.0) / 2.0 + 18.0;
128 let half = self.caret_size;
129 let mut path = Path::new();
130
131 match self.placement {
132 OverlayPlacement::Below
133 | OverlayPlacement::BelowPreferred
134 | OverlayPlacement::NearAnchor { .. } => {
135 path.move_to(Point::new(center_x - half, panel.y));
136 path.line_to(Point::new(center_x, bounds.y));
137 path.line_to(Point::new(center_x + half, panel.y));
138 path.close();
139 Some(path)
140 }
141 OverlayPlacement::Above => {
142 let bottom = panel.bottom();
143 path.move_to(Point::new(center_x - half, bottom));
144 path.line_to(Point::new(center_x, bottom + self.caret_size));
145 path.line_to(Point::new(center_x + half, bottom));
146 path.close();
147 Some(path)
148 }
149 _ => None,
150 }
151 }
152}
153
154impl std::fmt::Debug for PopoverSurface {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.debug_struct("PopoverSurface").finish()
157 }
158}
159
160impl Widget for PopoverSurface {
161 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
162 if let Some(pending) = self.pending_content.take() {
163 self.content_id = Some(match pending {
164 PendingChild::Id(id) => id,
165 PendingChild::Deferred(w) => ctx.add_boxed(w),
166 });
167 }
168 self.children()
169 }
170
171 fn layout_response(
172 &self,
173 proposal: SizeProposal,
174 ctx: &LayoutContext,
175 ) -> teksilo_core::widget::LayoutResponse {
176 let inset_w = self.content_padding.leading + self.content_padding.trailing;
177 let inset_h = self.content_padding.top + self.content_padding.bottom;
178 let (caret_top, caret_bottom) = self.caret_insets();
179 self.content_id
180 .and_then(|id| {
181 ctx.child_size(
182 id,
183 SizeProposal {
184 width: proposal.width.map(|width| (width - inset_w).max(0.0)),
185 height: proposal
186 .height
187 .map(|height| (height - inset_h - caret_top - caret_bottom).max(0.0)),
188 },
189 )
190 })
191 .map(|size| {
192 Size::new(
193 size.width + inset_w,
194 size.height + inset_h + caret_top + caret_bottom,
195 )
196 })
197 .unwrap_or_else(|| proposal.resolve(200.0, 80.0))
198 .into()
199 }
200
201 fn place_children(
202 &self,
203 bounds: Rect,
204 _proposal: SizeProposal,
205 children: &mut [WidgetPlacement],
206 _ctx: &LayoutContext,
207 ) {
208 let panel = self.panel_bounds(bounds);
209 let pad = self.content_padding;
210 for child in children.iter_mut() {
211 child.origin = teksilo_canvas::Point::new(panel.x + pad.leading, panel.y + pad.top);
212 child.size = Size::new(
213 (panel.width - pad.leading - pad.trailing).max(0.0),
214 (panel.height - pad.top - pad.bottom).max(0.0),
215 );
216 }
217 }
218
219 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
220 let panel = self.panel_bounds(bounds);
221 let radius = CornerRadius::uniform(self.corner_radius);
222 let fill = self.background.resolve(&ctx.theme.colors);
223 crate::shadow::paint_layered_shadow(
224 canvas,
225 panel,
226 radius,
227 &ctx.theme.shape.shadow_sm,
228 &ctx.theme.shape.shadow_inner_sm,
229 crate::styles::recipe_popover_style::POPOVER_SHADOW_DENSITY,
230 self.attached_shadow_side(ctx.layout_direction),
231 );
232 canvas.fill_rounded_rect(panel, radius, fill);
237 canvas.stroke_rounded_rect(
238 panel,
239 radius,
240 ctx.theme.colors.border,
241 ctx.theme.shape.border_width,
242 );
243 if let Some(path) = self.caret_path(bounds) {
244 canvas.fill_path(&path, fill);
245 canvas.stroke_path(&path, ctx.theme.colors.border, ctx.theme.shape.border_width);
246 }
247 }
248
249 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
250 if self.presentational {
251 builder.set_hidden();
255 return;
256 }
257 builder.set_role(teksilo_core::accesskit::Role::Dialog);
263 builder.set_name(&self.name);
264 }
265
266 fn children(&self) -> Vec<WidgetId> {
267 self.content_id.into_iter().collect()
268 }
269}