use teksilo_canvas::{Canvas, EdgeInsets, Path, Point, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::build_context::BuildContext;
use teksilo_core::overlay::OverlayPlacement;
use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::{CornerRadius, SurfaceRole};
pub struct PopoverSurface {
content_id: Option<WidgetId>,
pending_content: Option<PendingChild>,
placement: OverlayPlacement,
show_caret: bool,
caret_size: f32,
name: String,
content_padding: EdgeInsets,
background: SurfaceRole,
corner_radius: f32,
presentational: bool,
}
impl PopoverSurface {
#[allow(clippy::too_many_arguments)]
pub fn new(
content: PendingChild,
placement: OverlayPlacement,
show_caret: bool,
caret_size: f32,
name: String,
content_padding: EdgeInsets,
background: SurfaceRole,
corner_radius: f32,
presentational: bool,
) -> Self {
Self {
content_id: None,
pending_content: Some(content),
placement,
show_caret,
caret_size,
name,
content_padding,
background,
corner_radius,
presentational,
}
}
fn attached_shadow_side(
&self,
layout_direction: teksilo_core::environment::LayoutDirection,
) -> Option<crate::shadow::AttachedSide> {
use teksilo_core::environment::LayoutDirection;
match self.placement {
OverlayPlacement::Below
| OverlayPlacement::BelowPreferred
| OverlayPlacement::NearAnchor { .. } => Some(crate::shadow::AttachedSide::Top),
OverlayPlacement::Above => Some(crate::shadow::AttachedSide::Bottom),
OverlayPlacement::TrailingEdge => match layout_direction {
LayoutDirection::LeftToRight => Some(crate::shadow::AttachedSide::Left),
LayoutDirection::RightToLeft => Some(crate::shadow::AttachedSide::Right),
},
_ => None,
}
}
fn caret_insets(&self) -> (f32, f32) {
if !self.show_caret {
return (0.0, 0.0);
}
match self.placement {
OverlayPlacement::Below
| OverlayPlacement::BelowPreferred
| OverlayPlacement::NearAnchor { .. } => (self.caret_size, 0.0),
OverlayPlacement::Above => (0.0, self.caret_size),
_ => (0.0, 0.0),
}
}
fn panel_bounds(&self, bounds: Rect) -> Rect {
let (top, bottom) = self.caret_insets();
Rect::new(
bounds.x,
bounds.y + top,
bounds.width,
(bounds.height - top - bottom).max(0.0),
)
}
fn caret_path(&self, bounds: Rect) -> Option<Path> {
if !self.show_caret {
return None;
}
let panel = self.panel_bounds(bounds);
let center_x = panel.x + panel.width.min(56.0) / 2.0 + 18.0;
let half = self.caret_size;
let mut path = Path::new();
match self.placement {
OverlayPlacement::Below
| OverlayPlacement::BelowPreferred
| OverlayPlacement::NearAnchor { .. } => {
path.move_to(Point::new(center_x - half, panel.y));
path.line_to(Point::new(center_x, bounds.y));
path.line_to(Point::new(center_x + half, panel.y));
path.close();
Some(path)
}
OverlayPlacement::Above => {
let bottom = panel.bottom();
path.move_to(Point::new(center_x - half, bottom));
path.line_to(Point::new(center_x, bottom + self.caret_size));
path.line_to(Point::new(center_x + half, bottom));
path.close();
Some(path)
}
_ => None,
}
}
}
impl std::fmt::Debug for PopoverSurface {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PopoverSurface").finish()
}
}
impl Widget for PopoverSurface {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
if let Some(pending) = self.pending_content.take() {
self.content_id = Some(match pending {
PendingChild::Id(id) => id,
PendingChild::Deferred(w) => ctx.add_boxed(w),
});
}
self.children()
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
let inset_w = self.content_padding.leading + self.content_padding.trailing;
let inset_h = self.content_padding.top + self.content_padding.bottom;
let (caret_top, caret_bottom) = self.caret_insets();
self.content_id
.and_then(|id| {
ctx.child_size(
id,
SizeProposal {
width: proposal.width.map(|width| (width - inset_w).max(0.0)),
height: proposal
.height
.map(|height| (height - inset_h - caret_top - caret_bottom).max(0.0)),
},
)
})
.map(|size| {
Size::new(
size.width + inset_w,
size.height + inset_h + caret_top + caret_bottom,
)
})
.unwrap_or_else(|| proposal.resolve(200.0, 80.0))
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
let panel = self.panel_bounds(bounds);
let pad = self.content_padding;
for child in children.iter_mut() {
child.origin = teksilo_canvas::Point::new(panel.x + pad.leading, panel.y + pad.top);
child.size = Size::new(
(panel.width - pad.leading - pad.trailing).max(0.0),
(panel.height - pad.top - pad.bottom).max(0.0),
);
}
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let panel = self.panel_bounds(bounds);
let radius = CornerRadius::uniform(self.corner_radius);
let fill = self.background.resolve(&ctx.theme.colors);
crate::shadow::paint_layered_shadow(
canvas,
panel,
radius,
&ctx.theme.shape.shadow_sm,
&ctx.theme.shape.shadow_inner_sm,
crate::styles::recipe_popover_style::POPOVER_SHADOW_DENSITY,
self.attached_shadow_side(ctx.layout_direction),
);
canvas.fill_rounded_rect(panel, radius, fill);
canvas.stroke_rounded_rect(
panel,
radius,
ctx.theme.colors.border,
ctx.theme.shape.border_width,
);
if let Some(path) = self.caret_path(bounds) {
canvas.fill_path(&path, fill);
canvas.stroke_path(&path, ctx.theme.colors.border, ctx.theme.shape.border_width);
}
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
if self.presentational {
builder.set_hidden();
return;
}
builder.set_role(teksilo_core::accesskit::Role::Dialog);
builder.set_name(&self.name);
}
fn children(&self) -> Vec<WidgetId> {
self.content_id.into_iter().collect()
}
}