facett-app/src/popover.rs
//! **`Popover` — an anchored floating panel.** Generalises the dropdown popup
//! into a reusable overlay: attach arbitrary content to a trigger response. Like
//! the other overlays, its `open` state lives in the struct (FC-1), so it is
//! headless-drivable + observable.
use serde_json::{Value, json};
/// A floating panel anchored under a trigger widget.
#[derive(Default)]
pub struct Popover {
id_salt: String,
open: bool,
}
impl Popover {
pub fn new(id_salt: impl Into<String>) -> Self {
Self { id_salt: id_salt.into(), open: false }
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn open(&mut self) {
self.open = true;
}
pub fn close(&mut self) {
self.open = false;
}
pub fn toggle(&mut self) {
self.open = !self.open;
}
/// Draw the popover (if open) anchored below `trigger`, filling it with
/// `add_contents`. Esc closes. Call after drawing the trigger widget.
pub fn show(&mut self, ui: &mut egui::Ui, trigger: &egui::Response, add_contents: impl FnOnce(&mut egui::Ui)) {
if !self.open {
return;
}
let area_id = egui::Id::new((&self.id_salt, "popover"));
egui::Area::new(area_id)
.order(egui::Order::Foreground)
.fixed_pos(trigger.rect.left_bottom())
.show(ui.ctx(), |ui| {
egui::Frame::popup(ui.style()).inner_margin(8.0).show(ui, |ui| {
ui.set_max_width(280.0);
add_contents(ui);
});
});
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
self.open = false;
}
}
pub fn state_json(&self) -> Value {
json!({ "open": self.open })
}
}