use crate::core::{Color, Rect};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::GenericSignal;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
pub struct BottomSheet {
base: BaseWidget,
open: bool,
content_height: u32,
pub dismissed: GenericSignal,
}
impl BottomSheet {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::BottomSheet, geometry, "BottomSheet"),
open: false,
content_height: geometry.height / 2,
dismissed: GenericSignal::new(),
}
}
pub fn open(&mut self) {
if !self.open {
self.open = true;
self.base.request_redraw();
}
}
pub fn dismiss(&mut self) {
if self.open {
self.open = false;
self.base.request_redraw();
}
}
pub fn set_content_height(&mut self, height: u32) {
self.content_height = height.min(self.base.geometry.height);
self.base.request_redraw();
}
pub fn is_open(&self) -> bool {
self.open
}
}
impl Widget for BottomSheet {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
}
impl Draw for BottomSheet {
fn draw(&mut self, context: &mut RenderContext) {
if !self.open {
return;
}
let rect = self.geometry();
let sheet_height = self.content_height.min(rect.height);
let sheet_y = rect.y + rect.height as i32 - sheet_height as i32;
let sheet_rect = Rect::new(rect.x, sheet_y, rect.width, sheet_height);
let overlay_height = rect.height - sheet_height;
if overlay_height > 0 {
let overlay_rect = Rect::new(rect.x, rect.y, rect.width, overlay_height);
let overlay_color = Color::rgba(0, 0, 0, 100);
context.fill_rect(overlay_rect, overlay_color);
}
let sheet_radius = 16;
let sheet_color = Color::rgba(248, 248, 248, 255);
context.fill_rounded_rect(sheet_rect, sheet_radius, sheet_color);
context.draw_rounded_rect_stroke(
sheet_rect,
sheet_radius,
Color::rgba(220, 220, 220, 255),
1,
);
let handle_width = 32;
let handle_height = 5;
let handle_x = rect.x + (rect.width as i32 - handle_width as i32) / 2;
let handle_y = sheet_y + 8;
let handle_rect = Rect::new(handle_x, handle_y, handle_width, handle_height);
let handle_color = Color::rgba(180, 180, 180, 200);
context.fill_rounded_rect(handle_rect, handle_height / 2, handle_color);
}
}
impl EventHandler for BottomSheet {
fn handle_event(&mut self, event: &Event) {
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos: _, button } => {
if *button == 1 && self.open {
self.open = false;
self.dismissed.emit();
self.base.request_redraw();
}
}
_ => {
self.base.handle_event(event);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
use crate::widget::svg::render_to_svg;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
fn make_sheet() -> BottomSheet {
BottomSheet::new(Rect::new(0, 0, 400, 600))
}
#[test]
fn bottom_sheet_default_is_closed() {
let sheet = make_sheet();
assert!(!sheet.is_open());
assert_eq!(sheet.kind(), WidgetKind::BottomSheet);
assert_eq!(sheet.base.geometry, Rect::new(0, 0, 400, 600));
}
#[test]
fn bottom_sheet_open_and_close() {
let mut sheet = make_sheet();
assert!(!sheet.is_open());
sheet.open();
assert!(sheet.is_open());
sheet.dismiss();
assert!(!sheet.is_open());
}
#[test]
fn bottom_sheet_set_content_height() {
let mut sheet = make_sheet();
assert_eq!(sheet.content_height, 300);
sheet.set_content_height(200);
assert_eq!(sheet.content_height, 200);
sheet.set_content_height(999);
assert_eq!(sheet.content_height, 600);
}
#[test]
fn bottom_sheet_dismiss_signal_emits() {
let mut sheet = make_sheet();
sheet.open();
assert!(sheet.is_open());
let dismissed = Arc::new(AtomicBool::new(false));
let d = dismissed.clone();
sheet.dismissed.connect(move || {
d.store(true, Ordering::SeqCst);
});
sheet.handle_event(&Event::MousePress { pos: Point::new(200, 300), button: 1 });
assert!(!sheet.is_open());
assert!(dismissed.load(Ordering::SeqCst));
}
#[test]
fn bottom_sheet_mouse_press_dismisses_when_open() {
let mut sheet = make_sheet();
sheet.open();
assert!(sheet.is_open());
sheet.handle_event(&Event::MousePress { pos: Point::new(50, 50), button: 1 });
assert!(!sheet.is_open());
}
#[test]
fn bottom_sheet_mouse_press_noop_when_closed() {
let mut sheet = make_sheet();
assert!(!sheet.is_open());
let dismissed = Arc::new(AtomicBool::new(false));
let d = dismissed.clone();
sheet.dismissed.connect(move || {
d.store(true, Ordering::SeqCst);
});
sheet.handle_event(&Event::MousePress { pos: Point::new(50, 50), button: 1 });
assert!(!dismissed.load(Ordering::SeqCst));
}
#[test]
fn bottom_sheet_other_button_noop() {
let mut sheet = make_sheet();
sheet.open();
sheet.handle_event(&Event::MousePress { pos: Point::new(50, 50), button: 2 });
assert!(sheet.is_open());
}
#[test]
fn bottom_sheet_disabled_blocks_events() {
let mut sheet = make_sheet();
sheet.set_enabled(false);
sheet.open();
assert!(sheet.is_open());
let dismissed = Arc::new(AtomicBool::new(false));
let d = dismissed.clone();
sheet.dismissed.connect(move || {
d.store(true, Ordering::SeqCst);
});
sheet.handle_event(&Event::MousePress { pos: Point::new(50, 50), button: 1 });
assert!(sheet.is_open());
assert!(!dismissed.load(Ordering::SeqCst));
}
#[test]
fn bottom_sheet_open_twice_noop() {
let mut sheet = make_sheet();
sheet.open();
sheet.open(); assert!(sheet.is_open());
}
#[test]
fn bottom_sheet_dismiss_twice_noop() {
let mut sheet = make_sheet();
sheet.dismiss(); assert!(!sheet.is_open());
sheet.open();
sheet.dismiss();
sheet.dismiss(); assert!(!sheet.is_open());
}
#[test]
fn bottom_sheet_svg_output_open() {
let mut sheet = make_sheet();
sheet.open();
let svg = render_to_svg(&mut sheet);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
assert!(svg.contains("width=\"400\""));
assert!(svg.contains("height=\"600\""));
}
#[test]
fn bottom_sheet_svg_output_closed() {
let mut sheet = make_sheet();
let svg = render_to_svg(&mut sheet);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
}
#[test]
fn bottom_sheet_content_height_default_is_half_geometry() {
let sheet = BottomSheet::new(Rect::new(0, 0, 400, 800));
assert_eq!(sheet.content_height, 400);
}
#[test]
fn bottom_sheet_dismiss_via_overlay_click() {
let mut sheet = make_sheet();
sheet.open();
let dismissed = Arc::new(AtomicBool::new(false));
let d = dismissed.clone();
sheet.dismissed.connect(move || {
d.store(true, Ordering::SeqCst);
});
sheet.handle_event(&Event::MousePress { pos: Point::new(100, 50), button: 1 });
assert!(!sheet.is_open());
assert!(dismissed.load(Ordering::SeqCst));
}
#[test]
fn bottom_sheet_dismiss_via_sheet_click() {
let mut sheet = make_sheet();
sheet.open();
let dismissed = Arc::new(AtomicBool::new(false));
let d = dismissed.clone();
sheet.dismissed.connect(move || {
d.store(true, Ordering::SeqCst);
});
sheet.handle_event(&Event::MousePress { pos: Point::new(200, 450), button: 1 });
assert!(!sheet.is_open());
assert!(dismissed.load(Ordering::SeqCst));
}
}