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