use bevy::ecs::{
bundle::Bundle,
children,
component::Component,
resource::Resource,
system::{ResMut, SystemParam},
};
use bevy::picking::hover::Hovered;
use bevy::platform::collections::{HashMap, HashSet, hash_set::Entry};
use bevy::ui::{
AlignItems, BackgroundColor, BorderColor, FlexDirection, JustifyContent, Node, Overflow,
percent, px, vh, vw, widget::Text,
};
use bevy::utils::default;
use bevy::{
color::{
Srgba,
palettes::css::{BLACK, LIGHT_GRAY},
},
ui_widgets::Button,
};
use bevy_immediate::{
Imm, ImmCtx, ImmEntity,
attach::{BevyImmediateAttachPlugin, ImmediateAttach},
ui::{
CapsUi,
activated::ImmUiActivated,
floating_window_plugin::{
self, FloatingWindow, FloatingWindowPlugin, FloatingWindowStoreLocationId,
},
selected::ImmUiSelected,
text::ImmUiText,
},
utils::ImmLocalHashMemoryHelper,
};
use strum::IntoEnumIterator;
use crate::{
anchored::AnchoredUiExampleRoot,
bevy_scrollarea::BevyScrollareaExampleRoot,
bevy_widgets::BevyWidgetExampleRoot,
extension_use::ExtensionUseExampleRoot,
feathers_gallery::FeathersGalleryExampleRoot,
hello_world::HelloWorldRoot,
hot_patching::HotPatchingRoot,
main_menu::CurrentExample,
power_user::PowerUserExampleRoot,
styles::{self, MyStyle, button_bundle, node_container},
text_edit::TextEditExampleRoot,
tooltip::TooltipExampleRoot,
widget_use::WidgetUseExampleRoot,
};
pub struct FloatingWindowExamplePlugin;
impl bevy::app::Plugin for FloatingWindowExamplePlugin {
fn build(&self, app: &mut bevy::app::App) {
app.add_plugins(FloatingWindowPlugin);
app.add_plugins(BevyImmediateAttachPlugin::<CapsUi, FloatingWindowRoot>::new());
app.add_systems(bevy::app::Update, spawn_windows_from_system);
app.insert_resource(FloatingWindowState::default());
}
}
#[derive(Resource, Default)]
pub struct FloatingWindowState {
windows: HashMap<CurrentExample, Vec<i32>>,
unique_window_id_counter: i32,
windows_with_memory: HashSet<CurrentExample>,
}
#[derive(Component)]
pub struct FloatingWindowRoot;
#[derive(SystemParam)]
pub struct Params<'w> {
state: ResMut<'w, FloatingWindowState>,
}
impl ImmediateAttach<CapsUi> for FloatingWindowRoot {
type Params = Params<'static>;
fn construct(ui: &mut Imm<CapsUi>, params: &mut Params) {
ui.ch()
.on_spawn_text("Toggle floating windows for examples (will remember their location)");
ui.ch()
.on_spawn_insert(|| Node {
flex_wrap: bevy::ui::FlexWrap::Wrap,
..styles::row_node_container()
})
.add(|ui| {
for example in CurrentExample::iter() {
let mut button = ui
.ch()
.on_spawn_insert(styles::button_bundle)
.add(|ui| {
ui.ch().on_spawn_text_fn(|| example.title().to_string());
})
.selected_set(params.state.windows_with_memory.contains(&example));
if button.activated() {
match params.state.windows_with_memory.entry(example) {
Entry::Occupied(entry) => {
entry.remove();
}
Entry::Vacant(entry) => {
entry.insert();
}
}
}
}
});
ui.ch().on_spawn_text("Add floating windows for examples");
ui.ch()
.on_spawn_insert(|| Node {
flex_wrap: bevy::ui::FlexWrap::Wrap,
..styles::row_node_container()
})
.add(|ui| {
for example in CurrentExample::iter() {
let mut button = ui.ch().on_spawn_insert(styles::button_bundle).add(|ui| {
ui.ch().on_spawn_text_fn(|| example.title().to_string());
});
if button.activated() {
let unique_id = params.state.unique_window_id_counter;
params.state.unique_window_id_counter += 1;
params
.state
.windows
.entry(example)
.or_default()
.push(unique_id);
}
}
});
ui.ch()
.on_spawn_text("Spawn floating windows as \"popup\" directly:");
ui.ch()
.on_spawn_insert(styles::row_node_container)
.add(|ui| {
let mut button = ui.ch().on_spawn_insert(button_bundle).add(|ui| {
ui.ch().on_spawn_text("Popup");
});
let mut is_popup_open_local =
ImmLocalHashMemoryHelper::new(&mut button, "is_popup_open", &false);
if button.activated() {
is_popup_open_local.store(&true);
}
let is_open = is_popup_open_local.is_stored(&true);
button = button.selected_set(is_open);
if is_open {
button = button.unrooted("my_ui", |ui| {
my_window(
ui.ch(),
true,
|fw| fw,
|| "Popup".into(),
|| {
is_popup_open_local.store(&false);
},
|ui| {
ui.ch().on_spawn_insert(node_container).add(|ui| {
ui.ch().on_spawn_text("Popup text");
});
},
);
});
}
is_popup_open_local.finalize(&mut button);
});
}
}
fn spawn_windows_from_system(ctx: ImmCtx<CapsUi>, mut state: ResMut<FloatingWindowState>) {
let mut ui_root = ctx.build_immediate_root("example_windows");
for (example, ids) in state.windows.iter_mut() {
ids.retain(|id| {
let mut open = true;
show_example_window(
ui_root.ch_id(("non_unique", example, id)),
false,
example,
&mut open,
);
open
});
}
state.windows_with_memory.retain(|example| {
let mut open = true;
show_example_window(ui_root.ch_id(("memory", example)), true, example, &mut open);
open
});
}
fn show_example_window(
imm_entity: ImmEntity<CapsUi>,
memorize_location: bool,
example: &CurrentExample,
open: &mut bool,
) {
my_window(
imm_entity,
memorize_location,
|fw| fw,
|| example.title().into(),
|| *open = false,
|ui| {
let content = ui.ch().on_spawn_insert(|| {
(Node {
flex_direction: FlexDirection::Column,
..node_container()
},)
});
match *example {
CurrentExample::WidgetUse => {
content.on_spawn_insert(|| WidgetUseExampleRoot);
}
CurrentExample::HelloWorld => {
content.on_spawn_insert(|| HelloWorldRoot);
}
CurrentExample::ExtensionUse => {
content.on_spawn_insert(|| ExtensionUseExampleRoot);
}
CurrentExample::PowerUser => {
content.on_spawn_insert(|| PowerUserExampleRoot);
}
CurrentExample::BevyWidgets => {
content.on_spawn_insert(|| BevyWidgetExampleRoot);
}
CurrentExample::BevyScrollbar => {
content.on_spawn_insert(|| BevyScrollareaExampleRoot);
}
CurrentExample::HotPatching => {
content.on_spawn_insert(|| HotPatchingRoot);
}
CurrentExample::Tooltip => {
content.on_spawn_insert(|| TooltipExampleRoot);
}
CurrentExample::FloatingWindows => {
content.on_spawn_insert(|| FloatingWindowRoot);
}
CurrentExample::Anchored => {
content.on_spawn_insert(|| AnchoredUiExampleRoot);
}
CurrentExample::TextEdit => {
content.on_spawn_insert(|| TextEditExampleRoot);
}
CurrentExample::FeathersGallery => {
content.on_spawn_insert(|| FeathersGalleryExampleRoot);
}
}
},
);
}
fn my_window(
mut ui_root: ImmEntity<CapsUi>,
memorize_location: bool,
window_params: impl FnOnce(FloatingWindow) -> FloatingWindow,
title: impl FnOnce() -> String,
on_close: impl FnOnce(),
content: impl FnOnce(&mut Imm<CapsUi>),
) {
ui_root = ui_root.on_spawn_insert(|| {
(
Node {
border: px(2.).into(),
..default()
},
window_params(FloatingWindow {
max_width: vw(95.),
max_height: vh(95.),
min_width: px(100.),
min_height: px(100.),
..Default::default()
}),
BackgroundColor(BLACK.into()),
BorderColor::all(LIGHT_GRAY),
floating_window_plugin::resizable_borders(8., ()),
)
});
if memorize_location {
let id = ui_root.imm_id();
ui_root = ui_root.on_spawn_insert(|| FloatingWindowStoreLocationId(id.raw()));
}
ui_root.add(|ui| {
ui.ch()
.on_spawn_insert(|| Node {
flex_direction: FlexDirection::Column,
width: percent(100.),
height: percent(100.),
max_width: percent(100.),
max_height: percent(100.),
overflow: Overflow::clip(),
..default()
})
.add(|ui| {
ui.ch()
.on_spawn_insert(|| {
(
Node {
flex_direction: FlexDirection::RowReverse,
align_items: AlignItems::Stretch,
..default()
},
BackgroundColor(Srgba::new(0.363, 0.363, 0.363, 1.0).into()),
)
})
.add(|ui| {
let mut close = ui.ch().on_spawn_insert(close_button_bundle);
if close.activated() {
on_close();
}
ui.ch()
.on_spawn_insert(|| Node {
flex_grow: 1.,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
})
.add(|ui| {
ui.ch().on_spawn_text_fn(title);
});
});
content(ui);
});
});
}
fn close_button_bundle() -> impl Bundle {
(
Node {
flex_grow: 0.,
aspect_ratio: Some(1.),
padding: px(4.).into(),
..default()
},
Button,
Hovered::default(),
MyStyle,
children![Text("X".into())],
)
}