use crate::dev::overlay::{DevOverlayHandle, OVERLAY_PREFIX, RebuildingOverlay, Status};
use crate::event::{Event, EventType};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OverlayAction {
Toggle,
Dismiss,
Unknown,
}
fn parse_action(id: &str) -> OverlayAction {
let suffix = id.strip_prefix(&format!("{OVERLAY_PREFIX}/")).unwrap_or("");
match suffix {
"toggle" => OverlayAction::Toggle,
"dismiss" => OverlayAction::Dismiss,
_ => OverlayAction::Unknown,
}
}
pub(crate) fn maybe_handle_event(handle: &DevOverlayHandle, event: &Event) -> bool {
let Some(widget_id) = overlay_id(event) else {
return false;
};
if !widget_id.starts_with(&format!("{OVERLAY_PREFIX}/")) {
return false;
}
let action = parse_action(widget_id);
if !matches!(event_type_of(event), Some(EventType::Click)) {
return true;
}
apply_action(handle, action);
true
}
fn overlay_id(event: &Event) -> Option<&str> {
let widget = event.as_widget()?;
if widget.scoped_id.full.starts_with(OVERLAY_PREFIX) {
Some(widget.scoped_id.full.as_str())
} else {
None
}
}
fn event_type_of(event: &Event) -> Option<EventType> {
event.as_widget().map(|w| w.event_type.clone())
}
fn apply_action(handle: &DevOverlayHandle, action: OverlayAction) {
let current = handle.snapshot();
match action {
OverlayAction::Toggle => {
let Some(mut overlay) = current else {
return;
};
if matches!(overlay.status, Status::Frozen) {
return;
}
overlay.expanded = !overlay.expanded;
handle.set(Some(overlay));
}
OverlayAction::Dismiss => {
handle.set(None);
}
OverlayAction::Unknown => {
log::debug!("dev overlay: ignoring unknown action in {:?}", action);
}
}
}
pub(crate) fn schedule_dismiss(handle: DevOverlayHandle) {
std::thread::Builder::new()
.name("plushie-dev-overlay-dismiss".to_string())
.spawn(move || {
std::thread::sleep(super::overlay::DISMISS_DELAY);
let Some(current) = handle.snapshot() else {
return;
};
if !matches!(current.status, Status::Success) || current.expanded {
return;
}
handle.set(None);
})
.expect("failed to spawn dev-overlay dismiss thread");
}
pub(crate) fn handle_overlay_message(handle: &DevOverlayHandle, overlay: RebuildingOverlay) {
let status = overlay.status;
handle.set(Some(overlay));
if matches!(status, Status::Success) {
schedule_dismiss(handle.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::WidgetEvent;
use plushie_core::ScopedId;
use serde_json::Value;
fn click_event(id: &str) -> Event {
Event::Widget(WidgetEvent {
event_type: EventType::Click,
scoped_id: ScopedId::parse(id),
value: Value::Null,
})
}
#[test]
fn non_overlay_events_pass_through() {
let handle = DevOverlayHandle::new();
let event = click_event("app/button");
assert!(!maybe_handle_event(&handle, &event));
}
#[test]
fn toggle_expands_and_collapses() {
let handle = DevOverlayHandle::new();
handle.publish(Status::Rebuilding, "building");
let event = click_event(&format!("{OVERLAY_PREFIX}/toggle"));
assert!(maybe_handle_event(&handle, &event));
assert!(handle.snapshot().unwrap().expanded);
assert!(maybe_handle_event(&handle, &event));
assert!(!handle.snapshot().unwrap().expanded);
}
#[test]
fn toggle_on_frozen_is_noop() {
let handle = DevOverlayHandle::new();
handle.set(Some(RebuildingOverlay {
status: Status::Frozen,
detail: String::new(),
expanded: false,
success_at: None,
}));
let event = click_event(&format!("{OVERLAY_PREFIX}/toggle"));
assert!(maybe_handle_event(&handle, &event));
assert!(!handle.snapshot().unwrap().expanded);
}
#[test]
fn dismiss_removes_overlay() {
let handle = DevOverlayHandle::new();
handle.publish(Status::Failed, "boom");
let event = click_event(&format!("{OVERLAY_PREFIX}/dismiss"));
assert!(maybe_handle_event(&handle, &event));
assert!(handle.snapshot().is_none());
}
#[test]
fn auto_dismiss_fires_for_collapsed_success() {
let handle = DevOverlayHandle::new();
handle.set(Some(RebuildingOverlay {
status: Status::Success,
detail: "built".to_string(),
expanded: false,
success_at: Some(
std::time::Instant::now() - (super::super::overlay::DISMISS_DELAY * 2),
),
}));
assert!(handle.snapshot().is_none());
}
#[test]
fn auto_dismiss_skipped_when_user_expanded_drawer() {
let handle = DevOverlayHandle::new();
handle.set(Some(RebuildingOverlay {
status: Status::Success,
detail: String::new(),
expanded: true,
success_at: Some(std::time::Instant::now()),
}));
let current = handle.snapshot().expect("overlay should be present");
let should_clear = matches!(current.status, Status::Success) && !current.expanded;
assert!(!should_clear);
}
#[test]
fn maybe_handle_event_consumes_overlay_ids_without_state() {
let handle = DevOverlayHandle::new();
let event = click_event(&format!("{OVERLAY_PREFIX}/toggle"));
assert!(maybe_handle_event(&handle, &event));
assert!(handle.snapshot().is_none());
}
#[test]
fn handle_overlay_message_installs_and_schedules() {
let handle = DevOverlayHandle::new();
handle_overlay_message(
&handle,
RebuildingOverlay {
status: Status::Rebuilding,
detail: "x".to_string(),
expanded: false,
success_at: None,
},
);
assert_eq!(handle.snapshot().unwrap().status, Status::Rebuilding);
}
}