use std::collections::HashMap;
pub struct ActionItem {
pub key: String,
pub label: String,
pub tooltip: String,
pub enabled: bool,
}
impl ActionItem {
pub fn new(key: impl Into<String>, label: impl Into<String>, tooltip: impl Into<String>) -> Self {
Self {
key: key.into(),
label: label.into(),
tooltip: tooltip.into(),
enabled: true,
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
pub fn action_rail(
ui: &mut egui::Ui,
title: Option<&str>,
subtitle: Option<&str>,
items: &[ActionItem],
hits: &mut HashMap<String, egui::Rect>,
) -> Option<String> {
if crate::viewport::touch::compact(ui.ctx()) {
return mobile_chooser(ui, title, items, hits);
}
if items.is_empty() {
return None;
}
let font = egui::TextStyle::Button.resolve(ui.style());
let measure = |ui: &egui::Ui, text: &str| -> f32 {
ui.ctx().fonts_mut(|f| {
f.layout_no_wrap(text.to_owned(), font.clone(), egui::Color32::PLACEHOLDER)
.size()
.x
})
};
let icon_w = ui.text_style_height(&egui::TextStyle::Body);
let mut widest = 0.0f32;
for item in items {
let (icon, rest) = crate::icon_text::split_caption(&item.label);
let text_w = measure(ui, rest);
widest = widest.max(if icon.is_some() {
text_w + icon_w + ui.spacing().item_spacing.x
} else {
text_w
});
}
let mut col_w = widest + 2.0 * ui.spacing().button_padding.x;
if let Some(title) = title {
let body = egui::TextStyle::Body.resolve(ui.style());
let title_w = ui.ctx().fonts_mut(|f| {
f.layout_no_wrap(title.to_owned(), body, egui::Color32::PLACEHOLDER).size().x
});
col_w = col_w.max(title_w);
}
ui.set_width(col_w.max(1.0));
if let Some(title) = title {
ui.label(egui::RichText::new(title).strong());
}
if let Some(subtitle) = subtitle {
ui.label(egui::RichText::new(subtitle).weak().small());
}
ui.add_space(2.0);
let full = ui.available_width();
let h = ui.spacing().interact_size.y;
let mut clicked = None;
for item in items {
let resp = ui
.add_enabled_ui(item.enabled, |ui| {
let button = crate::icon_text::icon_button(ui, &item.label);
ui.add_sized([full, h], button)
})
.inner
.on_hover_text(&item.tooltip)
.on_disabled_hover_text(&item.tooltip);
hits.insert(item.key.clone(), resp.rect);
if resp.clicked() {
clicked = Some(item.key.clone());
}
}
clicked
}
const MOBILE_PALETTE: &str = "brep-mobile-actions-palette";
const MOBILE_REQUEST: &str = "brep-mobile-actions-request";
const MOBILE_CHOICE: &str = "brep-mobile-actions-choice";
pub(crate) fn open_mobile_actions(ctx: &egui::Context) {
ctx.data_mut(|d| d.insert_temp(egui::Id::new(MOBILE_REQUEST), true));
}
pub(crate) fn close_mobile_actions(ctx: &egui::Context) {
ctx.data_mut(|d| {
let mut palette = d.get_temp::<crate::palette::Palette>(egui::Id::new(MOBILE_PALETTE)).unwrap_or_default();
palette.close();
d.remove::<String>(egui::Id::new(MOBILE_CHOICE));
d.insert_temp(egui::Id::new(MOBILE_PALETTE), palette);
d.insert_temp(egui::Id::new(MOBILE_REQUEST), false);
});
}
fn mobile_chooser(ui: &mut egui::Ui, title: Option<&str>, items: &[ActionItem], hits: &mut HashMap<String, egui::Rect>) -> Option<String> {
let ctx = ui.ctx().clone();
let (mut palette, requested) = ctx.data_mut(|d| {
let palette = d.get_temp::<crate::palette::Palette>(egui::Id::new(MOBILE_PALETTE)).unwrap_or_default();
let requested = d.get_temp::<bool>(egui::Id::new(MOBILE_REQUEST)).unwrap_or(false);
d.insert_temp(egui::Id::new(MOBILE_REQUEST), false);
(palette, requested)
});
let choices = || items.iter().map(|item| crate::palette::PaletteItem::new(&item.key, &item.label, vec![item.tooltip.clone()])).collect::<Vec<_>>();
if requested { palette.open(choices(), title.unwrap_or("Selection actions"), "Search actions"); }
let chosen = ctx.data_mut(|d| d.remove_temp::<String>(egui::Id::new(MOBILE_CHOICE)))
.filter(|key| items.iter().any(|item| item.enabled && item.key == *key));
if items.is_empty() { palette.close(); }
else if palette.is_open() {
let disabled = items.iter().filter(|item| !item.enabled).map(|item| (item.key.clone(), item.tooltip.clone())).collect();
palette.refresh_items(choices(), disabled);
for (key, rect) in palette.hits() {
if let Some(key) = key.strip_prefix("item:") { hits.insert(key.to_owned(), *rect); }
}
}
ctx.data_mut(|d| d.insert_temp(egui::Id::new(MOBILE_PALETTE), palette));
chosen
}
pub(crate) fn show_mobile_actions(ctx: &egui::Context) {
let mut palette = ctx.data(|d| d.get_temp::<crate::palette::Palette>(egui::Id::new(MOBILE_PALETTE))).unwrap_or_default();
let chosen = palette.show_with_id(ctx, egui::Id::new("brep-mobile-actions-modal"));
ctx.data_mut(|d| {
d.insert_temp(egui::Id::new(MOBILE_PALETTE), palette);
if let Some(chosen) = chosen { d.insert_temp(egui::Id::new(MOBILE_CHOICE), chosen); }
});
}
#[cfg(all(feature = "automation", target_arch = "wasm32"))]
pub(crate) fn mobile_actions_json(ctx: &egui::Context, entry: egui::Rect) -> String {
let palette = ctx.data(|d| d.get_temp::<crate::palette::Palette>(egui::Id::new(MOBILE_PALETTE))).unwrap_or_default();
serde_json::json!({"open": [entry.left(), entry.top(), entry.width(), entry.height()], "visible": palette.is_open(),
"rects": serde_json::from_str::<serde_json::Value>(&crate::automation::hit_rects::hits_json(palette.hits().iter())).unwrap_or_default()}).to_string()
}