use crate::compat::Vec;
use crate::core::{Color, ObjectId, Point, Rect, Size};
use crate::event::{key_codes, Event, EventHandler};
use crate::impl_widget_property_hooks;
use crate::property_names_of;
use crate::render::RenderContext;
use crate::signal::{EventSignalRef, GenericSignal, Signal1};
use crate::widget::capability::coercion::expect_bool;
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
pub struct ModalHost {
base: BaseWidget,
stack: Vec<ObjectId>,
scrim_enabled: bool,
scrim_color: Option<Color>,
pub modal_changed: Signal1<usize>,
pub dismiss_requested: GenericSignal,
}
impl ModalHost {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Panel, geometry, "ModalHost"),
stack: Vec::new(),
scrim_enabled: true,
scrim_color: None,
modal_changed: Signal1::new(),
dismiss_requested: GenericSignal::new(),
}
}
pub fn push(&mut self, id: ObjectId) {
if let Some(position) = self.stack.iter().position(|existing| *existing == id) {
if position + 1 == self.stack.len() {
return;
}
self.stack.remove(position);
}
self.stack.push(id);
self.modal_changed.emit(self.stack.len());
self.base.request_redraw();
}
pub fn pop(&mut self) -> Option<ObjectId> {
let popped = self.stack.pop()?;
self.modal_changed.emit(self.stack.len());
self.base.request_redraw();
Some(popped)
}
pub fn remove(&mut self, id: ObjectId) -> bool {
let Some(position) = self.stack.iter().position(|existing| *existing == id) else {
return false;
};
self.stack.remove(position);
self.modal_changed.emit(self.stack.len());
self.base.request_redraw();
true
}
pub fn is_modal_top(&self, id: ObjectId) -> bool {
self.stack.last().copied() == Some(id)
}
pub fn top_modal(&self) -> Option<ObjectId> {
self.stack.last().copied()
}
pub fn stack(&self) -> &[ObjectId] {
&self.stack
}
pub fn depth(&self) -> usize {
self.stack.len()
}
pub fn scrim_enabled(&self) -> bool {
self.scrim_enabled
}
pub fn set_scrim_enabled(&mut self, enabled: bool) {
if self.scrim_enabled == enabled {
return;
}
self.scrim_enabled = enabled;
self.base.request_redraw();
}
pub fn scrim_color(&self) -> Option<Color> {
self.scrim_color
}
pub fn set_scrim_color(&mut self, color: Option<Color>) {
if self.scrim_color == color {
return;
}
self.scrim_color = color;
self.base.request_redraw();
}
}
impl Widget for ModalHost {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
Size::new(320, 240)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<EventSignalRef> {
match name {
"modal_changed" => {
Some(EventSignalRef::mapped("modal_changed", &self.modal_changed, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
"dismiss_requested" => {
Some(EventSignalRef::unit("dismiss_requested", &self.dismiss_requested))
}
_ => None,
}
}
}
impl WidgetProperties for ModalHost {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"modal_depth" => Ok(CapabilityValue::UInt(self.stack.len() as u64)),
"top_modal" => Ok(match self.top_modal() {
Some(id) => CapabilityValue::UInt(id),
None => CapabilityValue::Null,
}),
"scrim_enabled" => Ok(CapabilityValue::Bool(self.scrim_enabled)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"scrim_enabled" => {
self.set_scrim_enabled(expect_bool(value)?);
Ok(())
}
"modal_depth" | "top_modal" => Err(CapabilityAccessError::ReadOnlyProperty),
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["modal_depth", "top_modal", "scrim_enabled", BASE_PROPERTY_NAMES]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"pop" => {
self.pop();
Ok(())
}
"push" | "remove" => Err(CapabilityAccessError::OutOfRange),
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl EventHandler for ModalHost {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::KeyPress { key, .. } if *key == key_codes::ESCAPE && !self.stack.is_empty() => {
self.dismiss_requested.emit();
}
Event::MousePress { pos, button: 1, .. }
if !self.stack.is_empty() && !self.modal_area_contains(*pos) =>
{
self.dismiss_requested.emit();
}
_ => {}
}
}
}
impl ModalHost {
fn modal_area_contains(&self, pos: Point) -> bool {
self.base.geometry().contains_point(pos)
}
}
impl Draw for ModalHost {
fn draw(&mut self, context: &mut RenderContext) {
if self.stack.is_empty() || !self.scrim_enabled {
return;
}
let rect = self.base.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
let color = self.scrim_color.unwrap_or_else(|| {
crate::style::layer_color(crate::style::LayerColor::Scrim)
.unwrap_or(Color::rgba(0, 0, 0, 82))
});
context.fill_rect(rect, color);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_repeated_push_raises_one_entry_instead_of_duplicating_it() {
let mut host = ModalHost::new(Rect::new(0, 0, 400, 300));
host.push(7);
host.push(9);
host.push(7);
assert_eq!(host.stack(), &[9, 7]);
assert!(host.is_modal_top(7));
assert_eq!(host.top_modal(), Some(7));
assert_eq!(host.pop(), Some(7));
assert_eq!(host.depth(), 1);
assert_eq!(host.pop(), Some(9));
assert_eq!(host.pop(), None);
}
}