use crate::core::{Color, Font, Point, Rect};
use crate::event::{Event, EventHandler};
use crate::render::{RenderCommand, RenderContext};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
const ARROW_SIZE: i32 = 10;
const CORNER_RADIUS: u32 = 8;
pub struct Popover {
base: BaseWidget,
content: Option<Box<dyn Widget>>,
anchor_rect: Rect,
visible: bool,
body_rect: Rect,
}
impl Popover {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Popover, geometry, "Popover"),
content: None,
anchor_rect: Rect::default(),
visible: false,
body_rect: Rect::default(),
}
}
pub fn show(&mut self, anchor: Rect) {
self.anchor_rect = anchor;
self.visible = true;
self.base.request_redraw();
}
pub fn hide(&mut self) {
self.visible = false;
self.base.request_redraw();
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn set_content(&mut self, widget: Box<dyn Widget>) {
self.content = Some(widget);
self.base.request_redraw();
}
pub fn content(&self) -> Option<&dyn Widget> {
self.content.as_deref()
}
pub fn content_mut(&mut self) -> Option<&mut dyn Widget> {
self.content.as_deref_mut()
}
pub fn anchor_rect(&self) -> Rect {
self.anchor_rect
}
pub fn set_anchor_rect(&mut self, rect: Rect) {
self.anchor_rect = rect;
self.base.request_redraw();
}
fn compute_layout(&self) -> (Rect, Point, ArrowDirection) {
let geom = self.geometry();
let body_width = geom.width.max(100).min(400);
let body_height = geom.height.max(60).min(400);
let below_space =
geom.y + geom.height as i32 - (self.anchor_rect.y + self.anchor_rect.height as i32);
let above_space = self.anchor_rect.y - geom.y;
let (body_y, arrow_dir) = if below_space >= body_height as i32 + ARROW_SIZE {
(self.anchor_rect.y + self.anchor_rect.height as i32 + ARROW_SIZE, ArrowDirection::Up)
} else if above_space >= body_height as i32 + ARROW_SIZE {
(self.anchor_rect.y - body_height as i32 - ARROW_SIZE, ArrowDirection::Down)
} else {
(self.anchor_rect.y + self.anchor_rect.height as i32 + ARROW_SIZE, ArrowDirection::Up)
};
let anchor_center_x = self.anchor_rect.x + self.anchor_rect.width as i32 / 2;
let body_x = (anchor_center_x - body_width as i32 / 2).max(geom.x);
let body_rect = Rect::new(body_x, body_y, body_width, body_height);
let arrow_tip = Point::new(
anchor_center_x,
match arrow_dir {
ArrowDirection::Up => body_y - ARROW_SIZE,
ArrowDirection::Down => body_y + body_height as i32 + ARROW_SIZE,
},
);
(body_rect, arrow_tip, arrow_dir)
}
}
impl Widget for Popover {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
}
impl Draw for Popover {
fn draw(&mut self, context: &mut RenderContext) {
if !self.visible {
return;
}
let (body_rect, arrow_tip, arrow_dir) = self.compute_layout();
self.body_rect = body_rect;
let shadow_offset = 2i32;
let shadow_rect = Rect::new(
body_rect.x + shadow_offset,
body_rect.y + shadow_offset,
body_rect.width,
body_rect.height,
);
context.fill_rounded_rect(shadow_rect, CORNER_RADIUS, Color::rgba(0, 0, 0, 40));
context.fill_rounded_rect(body_rect, CORNER_RADIUS, Color::WHITE);
context.draw_rounded_rect_stroke(
body_rect,
CORNER_RADIUS,
Color::rgba(200, 200, 200, 200),
1,
);
self.draw_arrow(context, arrow_tip, arrow_dir);
let content_padding = 8i32;
let content_rect = Rect::new(
body_rect.x + content_padding,
body_rect.y + content_padding,
body_rect.width - (content_padding as u32) * 2,
body_rect.height - (content_padding as u32) * 2,
);
let font = Font::simple("sans-serif", 13.0);
let label = if self.content.is_some() { "Popover" } else { "Popover (empty)" };
let metrics = context.measure_text(label, &font);
let text_x = content_rect.x + (content_rect.width as i32 - metrics.width as i32) / 2;
let text_y = content_rect.y
+ (content_rect.height as i32 - metrics.height as i32) / 2
+ metrics.ascent as i32;
context.draw_text(
Point::new(text_x.max(content_rect.x), text_y.max(content_rect.y)),
label,
&font,
Color::rgba(150, 150, 150, 200),
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ArrowDirection {
Up,
Down,
}
impl Popover {
fn draw_arrow(&self, context: &mut RenderContext, tip: Point, dir: ArrowDirection) {
let half_base = ARROW_SIZE / 2;
let (_base_center, base_left, base_right) = match dir {
ArrowDirection::Up => {
let base_center = Point::new(tip.x, tip.y + ARROW_SIZE);
let base_left = Point::new(tip.x - half_base, tip.y + ARROW_SIZE);
let base_right = Point::new(tip.x + half_base, tip.y + ARROW_SIZE);
(base_center, base_left, base_right)
}
ArrowDirection::Down => {
let base_center = Point::new(tip.x, tip.y - ARROW_SIZE);
let base_left = Point::new(tip.x - half_base, tip.y - ARROW_SIZE);
let base_right = Point::new(tip.x + half_base, tip.y - ARROW_SIZE);
(base_center, base_left, base_right)
}
};
let points = vec![tip, base_left, base_right];
context.execute_command(RenderCommand::DrawPath {
points: points.clone(),
closed: true,
color: Color::WHITE,
filled: true,
width: 1,
});
context.execute_command(RenderCommand::DrawPath {
points,
closed: true,
color: Color::rgba(200, 200, 200, 200),
filled: false,
width: 1,
});
}
}
impl EventHandler for Popover {
fn handle_event(&mut self, event: &Event) {
if !self.visible {
self.base.handle_event(event);
return;
}
match event {
Event::MousePress { pos, button } => {
if *button == 1 {
if !self.body_rect.contains_point(*pos) {
self.hide();
}
}
}
Event::KeyPress { key, modifiers: _ } => {
if *key == 27 {
self.hide();
}
}
_ => {
self.base.handle_event(event);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::svg::render_to_svg;
struct TestContent {
base: BaseWidget,
text: String,
}
impl TestContent {
fn new(text: &str, geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Label, geometry, "TestContent"),
text: text.to_string(),
}
}
}
impl Widget for TestContent {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
}
impl Draw for TestContent {
fn draw(&mut self, context: &mut RenderContext) {
let font = Font::simple("sans-serif", 12.0);
context.draw_text(
Point::new(self.geometry().x, self.geometry().y),
&self.text,
&font,
Color::BLACK,
);
}
}
impl EventHandler for TestContent {
fn handle_event(&mut self, _event: &Event) {}
}
#[test]
fn popover_default_creation() {
let popover = Popover::new(Rect::new(0, 0, 300, 200));
assert_eq!(popover.kind(), WidgetKind::Popover);
assert!(!popover.is_visible());
assert!(popover.content().is_none());
assert_eq!(popover.geometry(), Rect::new(0, 0, 300, 200));
}
#[test]
fn popover_show_hide() {
let mut popover = Popover::new(Rect::new(0, 0, 300, 200));
assert!(!popover.is_visible());
popover.show(Rect::new(100, 100, 50, 20));
assert!(popover.is_visible());
popover.hide();
assert!(!popover.is_visible());
}
#[test]
fn popover_anchor_rect() {
let mut popover = Popover::new(Rect::new(0, 0, 300, 400));
let anchor = Rect::new(100, 100, 50, 20);
popover.show(anchor);
assert_eq!(popover.anchor_rect(), anchor);
let new_anchor = Rect::new(50, 50, 80, 30);
popover.set_anchor_rect(new_anchor);
assert_eq!(popover.anchor_rect(), new_anchor);
}
#[test]
fn popover_set_content() {
let mut popover = Popover::new(Rect::new(0, 0, 300, 200));
assert!(popover.content().is_none());
let content = TestContent::new("Hello", Rect::new(0, 0, 100, 30));
popover.set_content(Box::new(content));
assert!(popover.content().is_some());
}
#[test]
fn popover_auto_dismiss_on_click_outside() {
let mut popover = Popover::new(Rect::new(0, 0, 400, 400));
let anchor = Rect::new(150, 100, 50, 20);
popover.show(anchor);
assert!(popover.is_visible());
popover.handle_event(&Event::MousePress { pos: Point::new(5, 5), button: 1 });
assert!(!popover.is_visible());
}
#[test]
fn popover_escape_key_dismisses() {
let mut popover = Popover::new(Rect::new(0, 0, 400, 400));
popover.show(Rect::new(150, 100, 50, 20));
assert!(popover.is_visible());
popover.handle_event(&Event::KeyPress { key: 27, modifiers: 0 });
assert!(!popover.is_visible());
}
#[test]
fn popover_svg_output_visible() {
let mut popover = Popover::new(Rect::new(0, 0, 300, 200));
popover.show(Rect::new(100, 100, 50, 20));
let svg = render_to_svg(&mut popover);
assert!(svg.starts_with("<svg"), "SVG should start with <svg, got: {svg:.60}");
assert!(svg.ends_with("</svg>"), "SVG should end with </svg>");
}
#[test]
fn popover_svg_output_hidden() {
let mut popover = Popover::new(Rect::new(0, 0, 300, 200));
let svg = render_to_svg(&mut popover);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
let fill_count = svg.matches("fill=").count();
assert_eq!(fill_count, 1, "expected only background fill, got {fill_count}: {svg}");
}
}