use std::marker::PhantomData;
use crate::prelude::*;
use super::config::MosaicConfig;
use super::modal::Modals;
use super::widget::{Pane, Widget, WidgetContext};
pub struct TileBehavior<W: Widget<C, M>, C = (), M = ()> {
pub(crate) viewport_rects: std::collections::HashMap<egui_tiles::TileId, egui::Rect>,
pub(crate) viewport_textures: Vec<egui::TextureId>,
pub(crate) selected_viewport_tile: Option<egui_tiles::TileId>,
pub(crate) pending_add_widget: Option<(egui_tiles::TileId, usize)>,
pub(crate) layout_modified: bool,
pub(crate) config: MosaicConfig,
pub(crate) pending_removals: Vec<(egui_tiles::TileId, W)>,
pub(crate) window_index: Option<usize>,
pub(crate) is_active_window: bool,
pub(crate) cached_cameras: Vec<Entity>,
_phantom: PhantomData<(C, M)>,
}
impl<W: Widget<C, M>, C, M> Default for TileBehavior<W, C, M> {
fn default() -> Self {
Self {
viewport_rects: std::collections::HashMap::new(),
viewport_textures: Vec::new(),
selected_viewport_tile: None,
pending_add_widget: None,
layout_modified: false,
config: MosaicConfig::default(),
pending_removals: Vec::new(),
window_index: None,
is_active_window: true,
cached_cameras: Vec::new(),
_phantom: PhantomData,
}
}
}
pub(crate) struct TileBehaviorWithWorld<'a, W: Widget<C, M>, C = (), M = ()> {
pub(crate) behavior: &'a mut TileBehavior<W, C, M>,
pub(crate) world: &'a mut World,
pub(crate) app: &'a mut C,
pub(crate) modals: &'a mut Modals,
pub(crate) messages: &'a mut Vec<M>,
pub(crate) incoming_messages: &'a mut std::collections::HashMap<egui_tiles::TileId, Vec<M>>,
}
impl<W: Widget<C, M>, C, M> egui_tiles::Behavior<Pane<W>> for TileBehaviorWithWorld<'_, W, C, M> {
fn tab_title_for_pane(&mut self, pane: &Pane<W>) -> egui::WidgetText {
pane.widget.title().into()
}
fn pane_ui(
&mut self,
ui: &mut egui::Ui,
tile_id: egui_tiles::TileId,
pane: &mut Pane<W>,
) -> egui_tiles::UiResponse {
let rect = ui.available_rect_before_wrap();
self.behavior.viewport_rects.insert(tile_id, rect);
let mut context = WidgetContext {
world: &mut *self.world,
selected_viewport_tile: &mut self.behavior.selected_viewport_tile,
viewport_textures: &self.behavior.viewport_textures,
current_tile_id: tile_id,
modals: &mut *self.modals,
app: &mut *self.app,
window_index: self.behavior.window_index,
is_active_window: self.behavior.is_active_window,
cached_cameras: &self.behavior.cached_cameras,
messages: &mut *self.messages,
incoming_messages: &mut *self.incoming_messages,
};
pane.widget.ui(ui, &mut context);
egui_tiles::UiResponse::None
}
fn simplification_options(&self) -> egui_tiles::SimplificationOptions {
self.behavior.config.simplification_options
}
fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
self.behavior.config.tab_bar_height
}
fn close_button_outer_size(&self) -> f32 {
self.behavior.config.close_button_size
}
fn gap_width(&self, _style: &egui::Style) -> f32 {
self.behavior.config.gap_width
}
fn min_size(&self) -> f32 {
self.behavior.config.min_size
}
fn is_tab_closable(
&self,
tiles: &egui_tiles::Tiles<Pane<W>>,
tile_id: egui_tiles::TileId,
) -> bool {
if !self.behavior.config.all_closable {
return false;
}
if let Some(egui_tiles::Tile::Pane(pane)) = tiles.get(tile_id) {
pane.widget.closable()
} else {
true
}
}
fn on_tab_close(
&mut self,
tiles: &mut egui_tiles::Tiles<Pane<W>>,
tile_id: egui_tiles::TileId,
) -> bool {
if let Some(egui_tiles::Tile::Pane(pane)) = tiles.get(tile_id) {
self.behavior
.pending_removals
.push((tile_id, pane.widget.clone()));
}
self.behavior.layout_modified = true;
true
}
fn on_edit(&mut self, edit_action: egui_tiles::EditAction) {
match edit_action {
egui_tiles::EditAction::TileDropped | egui_tiles::EditAction::TileResized => {
self.behavior.layout_modified = true;
}
_ => {}
}
}
fn top_bar_right_ui(
&mut self,
_tiles: &egui_tiles::Tiles<Pane<W>>,
ui: &mut egui::Ui,
tile_id: egui_tiles::TileId,
_tabs: &egui_tiles::Tabs,
_scroll_offset: &mut f32,
) {
if !self.behavior.config.show_add_button {
return;
}
render_add_widget_popup::<W, C, M>(ui, tile_id, self.behavior);
}
}
fn render_add_widget_popup<W: Widget<C, M>, C, M>(
ui: &mut egui::Ui,
tile_id: egui_tiles::TileId,
behavior: &mut TileBehavior<W, C, M>,
) {
let button_response = ui.button("+").on_hover_text("Add widget");
let popup_id = egui::Id::new("mosaic_add_widget_popup").with(tile_id);
if button_response.clicked() {
egui::Popup::open_id(ui.ctx(), popup_id);
ui.memory_mut(|memory| {
memory
.data
.get_temp_mut_or_insert_with::<String>(popup_id, String::new);
memory
.data
.get_temp_mut_or_insert_with::<usize>(popup_id.with("selected_index"), || 0);
});
}
egui::Popup::from_response(&button_response)
.layout(egui::Layout::top_down_justified(egui::Align::LEFT))
.open_memory(None::<egui::SetOpenCommand>)
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
.id(popup_id)
.align(egui::RectAlign::BOTTOM_START)
.width(button_response.rect.width())
.show(|ui| {
render_add_widget_popup_content::<W, C, M>(ui, tile_id, popup_id, behavior);
});
}
fn render_add_widget_popup_content<W: Widget<C, M>, C, M>(
ui: &mut egui::Ui,
tile_id: egui_tiles::TileId,
popup_id: egui::Id,
behavior: &mut TileBehavior<W, C, M>,
) {
ui.set_min_width(200.0);
let search = render_widget_search_input(ui, popup_id);
ui.add_space(4.0);
ui.separator();
render_widget_catalog_list::<W, C, M>(ui, popup_id, tile_id, behavior, &search);
ui.separator();
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
ui.visuals_mut().widgets.noninteractive.fg_stroke.color = ui.visuals().weak_text_color();
ui.label("Up/Down to navigate");
ui.add_space(8.0);
ui.label("Enter to select");
});
}
fn render_widget_search_input(ui: &mut egui::Ui, popup_id: egui::Id) -> String {
let mut search = ui.memory_mut(|memory| {
memory
.data
.get_temp_mut_or_default::<String>(popup_id)
.clone()
});
let is_popup_open = egui::Popup::is_id_open(ui.ctx(), popup_id);
let popup_just_opened = ui.memory_mut(|memory| {
let popup_open_key = popup_id.with("was_just_opened");
if is_popup_open {
let was_open_before = memory
.data
.get_temp::<bool>(popup_open_key)
.unwrap_or(false);
if !was_open_before {
memory.data.insert_temp(popup_open_key, true);
true
} else {
false
}
} else {
memory.data.insert_temp(popup_open_key, false);
false
}
});
ui.spacing_mut().item_spacing.y = 6.0;
let search_frame = egui::Frame::NONE
.fill(ui.visuals().extreme_bg_color)
.inner_margin(egui::Margin::same(8))
.stroke(ui.visuals().widgets.noninteractive.bg_stroke);
search_frame.show(ui, |ui| {
ui.horizontal(|ui| {
let search_response = ui
.text_edit_singleline(&mut search)
.on_hover_text("Type to filter widgets");
let should_focus = popup_just_opened
|| (egui::Popup::is_id_open(ui.ctx(), popup_id)
&& !ui.memory(|memory| memory.has_focus(search_response.id)));
if should_focus {
ui.memory_mut(|memory| memory.request_focus(search_response.id));
ui.ctx().request_repaint();
}
if search_response.changed() {
ui.memory_mut(|memory| {
*memory.data.get_temp_mut_or_default::<String>(popup_id) = search.clone();
*memory
.data
.get_temp_mut_or_default::<usize>(popup_id.with("selected_index")) = 0;
});
}
if !search.is_empty() && ui.button("x").clicked() {
search.clear();
ui.memory_mut(|memory| {
*memory.data.get_temp_mut_or_default::<String>(popup_id) = String::new();
});
}
});
});
search
}
fn contains_case_insensitive_ascii(haystack: &str, needle: &str) -> bool {
if needle.len() > haystack.len() {
return false;
}
haystack
.as_bytes()
.windows(needle.len())
.any(|window| window.eq_ignore_ascii_case(needle.as_bytes()))
}
fn handle_catalog_keyboard_navigation(
ui: &mut egui::Ui,
popup_id: egui::Id,
item_count: usize,
) -> (usize, bool) {
let mut selected_index = ui.memory_mut(|memory| {
*memory
.data
.get_temp_mut_or_default::<usize>(popup_id.with("selected_index"))
});
if selected_index >= item_count {
selected_index = item_count - 1;
ui.memory_mut(|memory| {
*memory
.data
.get_temp_mut_or_default::<usize>(popup_id.with("selected_index")) = selected_index;
});
}
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown))
&& selected_index < item_count - 1
{
selected_index += 1;
ui.memory_mut(|memory| {
*memory
.data
.get_temp_mut_or_default::<usize>(popup_id.with("selected_index")) = selected_index;
});
}
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp))
&& selected_index > 0
{
selected_index -= 1;
ui.memory_mut(|memory| {
*memory
.data
.get_temp_mut_or_default::<usize>(popup_id.with("selected_index")) = selected_index;
});
}
let enter_pressed =
ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Enter));
(selected_index, enter_pressed)
}
fn render_widget_catalog_list<W: Widget<C, M>, C, M>(
ui: &mut egui::Ui,
popup_id: egui::Id,
tile_id: egui_tiles::TileId,
behavior: &mut TileBehavior<W, C, M>,
search: &str,
) {
let catalog = W::catalog();
let filtered: Vec<(usize, &super::widget::WidgetEntry<W>)> = catalog
.iter()
.enumerate()
.filter(|(_, entry)| {
search.is_empty() || contains_case_insensitive_ascii(&entry.name, search)
})
.collect();
if filtered.is_empty() {
ui.vertical_centered(|ui| {
ui.add_space(10.0);
ui.label("No matches found");
ui.add_space(10.0);
});
return;
}
let (selected_index, enter_pressed) =
handle_catalog_keyboard_navigation(ui, popup_id, filtered.len());
let item_height = 28.0;
let max_height = (filtered.len().min(8) as f32 * item_height) + 4.0;
egui::ScrollArea::vertical()
.id_salt(ui.next_auto_id())
.max_height(max_height)
.auto_shrink([false, false])
.show_viewport(ui, |ui, _viewport| {
ui.style_mut().visuals.button_frame = false;
ui.style_mut().spacing.item_spacing.y = 1.0;
if enter_pressed
|| ui.input(|input| {
input.key_pressed(egui::Key::ArrowUp) || input.key_pressed(egui::Key::ArrowDown)
})
{
ui.scroll_to_rect(
egui::Rect::from_min_size(
egui::pos2(0.0, selected_index as f32 * item_height),
egui::vec2(1.0, item_height),
),
None,
);
}
for (display_index, (catalog_index, entry)) in filtered.iter().enumerate() {
let is_selected = display_index == selected_index;
let button = egui::Button::new(egui::RichText::new(&entry.name).size(14.0).color(
if is_selected {
ui.visuals().selection.stroke.color
} else {
ui.visuals().text_color()
},
));
let button_response = ui.add_sized(
[ui.available_width(), item_height],
button.fill(if is_selected {
ui.visuals().selection.bg_fill
} else {
ui.visuals().extreme_bg_color
}),
);
if button_response.hovered() && !is_selected {
ui.memory_mut(|memory| {
*memory
.data
.get_temp_mut_or_default::<usize>(popup_id.with("selected_index")) =
display_index;
});
}
if button_response.clicked() || (enter_pressed && is_selected) {
behavior.pending_add_widget = Some((tile_id, *catalog_index));
behavior.layout_modified = true;
ui.memory_mut(|memory| {
*memory
.data
.get_temp_mut_or_default::<usize>(popup_id.with("selected_index")) = 0;
});
ui.ctx().request_repaint();
}
}
});
}