responsive_menu/
responsive_menu.rs1mod utils;
8use bevy_ui::widget::NodeImageMode;
9use utils::*;
10
11use std::sync::OnceLock;
12
13use bevy::{prelude::*, window::WindowResized};
14use futures_signals::signal::Mutable;
15use haalka::prelude::*;
16
17fn main() {
18 App::new()
19 .add_plugins(examples_plugin)
20 .add_systems(
21 Startup,
22 (setup, |world: &mut World| {
23 ui_root().spawn(world);
24 })
25 .chain(),
26 )
27 .add_systems(Update, on_resize)
28 .run();
29}
30
31const BASE_SIZE: f32 = 600.;
32const GAP: f32 = 10.;
33const FONT_SIZE: f32 = 33.33;
34
35static NINE_SLICE_TEXTURE: OnceLock<Handle<Image>> = OnceLock::new();
36
37fn nine_slice_texture() -> &'static Handle<Image> {
38 NINE_SLICE_TEXTURE
39 .get()
40 .expect("expected NINE_SLICE_TEXTURE_ATLAS to be initialized")
41}
42
43static NINE_SLICE_TEXTURE_ATLAS_LAYOUT: OnceLock<Handle<TextureAtlasLayout>> = OnceLock::new();
44
45fn nine_slice_texture_atlas_layout() -> &'static Handle<TextureAtlasLayout> {
46 NINE_SLICE_TEXTURE_ATLAS_LAYOUT
47 .get()
48 .expect("expected NINE_SLICE_TEXTURE_ATLAS_LAYOUT to be initialized")
49}
50
51static IMAGE: OnceLock<Handle<Image>> = OnceLock::new();
52
53fn image() -> &'static Handle<Image> {
54 IMAGE.get().expect("expected IMAGE to be initialized")
55}
56
57fn nine_slice_el(frame_signal: impl Signal<Item = usize> + Send + 'static) -> El<ImageNode> {
58 El::<ImageNode>::new()
59 .image_node(
60 ImageNode::from_atlas_image(
61 nine_slice_texture().clone(),
62 TextureAtlas {
63 layout: nine_slice_texture_atlas_layout().clone(),
64 index: 0,
65 },
66 )
67 .with_mode(NodeImageMode::Sliced(TextureSlicer {
68 border: BorderRect::all(24.0),
69 center_scale_mode: SliceScaleMode::Stretch,
70 sides_scale_mode: SliceScaleMode::Stretch,
71 max_corner_scale: 1.0,
72 })),
73 )
74 .on_signal_with_image_node(frame_signal, move |mut image, frame| {
75 if let Some(atlas) = &mut image.texture_atlas {
76 atlas.index = frame;
77 }
78 })
79}
80
81fn nine_slice_button() -> impl Element {
82 let hovered = Mutable::new(false);
83 let pressed = Mutable::new(false);
84 nine_slice_el(map_ref! {
85 let hovered = hovered.signal(),
86 let pressed = pressed.signal() => {
87 if *pressed {
88 2
89 } else if *hovered {
90 1
91 } else {
92 0
93 }
94 }
95 })
96 .with_node(|mut node| {
97 node.width = Val::Px(100.);
98 node.height = Val::Px(50.);
99 })
100 .hovered_sync(hovered)
101 .pressed_sync(pressed)
102 .cursor(CursorIcon::System(SystemCursorIcon::Pointer))
103}
104
105static WIDTH: LazyLock<Mutable<f32>> = LazyLock::new(default);
106
107fn horizontal() -> impl Element {
108 Row::<Node>::new()
109 .with_node(|mut node| {
110 node.width = Val::Percent(100.);
111 node.height = Val::Percent(100.);
112 node.column_gap = Val::Px(GAP);
113 })
114 .item(
115 Column::<Node>::new()
116 .with_node(|mut node| {
117 node.width = Val::Percent(50.);
118 node.height = Val::Percent(100.);
119 node.row_gap = Val::Px(GAP);
120 })
121 .align_content(Align::center())
122 .items((0..8).map(|_| nine_slice_button())),
123 )
124 .item(El::<ImageNode>::new().image_node(ImageNode::new(image().clone())))
125}
126
127fn vertical() -> impl Element {
128 Column::<Node>::new()
129 .with_node(|mut node| {
130 node.width = Val::Percent(100.);
131 node.height = Val::Percent(100.);
132 node.row_gap = Val::Px(GAP);
133 })
134 .item(El::<ImageNode>::new().image_node(ImageNode::new(image().clone())))
135 .item(
136 Row::<Node>::new()
137 .multiline()
138 .align_content(Align::center())
139 .with_node(|mut node| {
140 node.width = Val::Percent(100.);
141 node.height = Val::Percent(50.);
142 node.column_gap = Val::Px(GAP);
143 })
144 .items((0..8).map(|_| nine_slice_button())),
145 )
146}
147
148fn menu() -> impl Element {
149 nine_slice_el(always(3))
150 .with_node(|mut node| {
151 node.height = Val::Px(BASE_SIZE);
152 node.padding = UiRect::all(Val::Px(GAP));
153 })
154 .on_signal_with_node(
155 WIDTH.signal().map(|width| BASE_SIZE.min(width)).dedupe().map(Val::Px),
156 |mut node, width| node.width = width,
157 )
158 .child_signal(
159 WIDTH
160 .signal()
161 .map(|width| width > 400.)
162 .dedupe()
163 .map_bool(|| horizontal().type_erase(), || vertical().type_erase()),
164 )
165}
166
167fn ui_root() -> impl Element {
168 El::<Node>::new()
169 .with_node(|mut node| {
170 node.width = Val::Percent(100.);
171 node.height = Val::Percent(100.);
172 })
173 .align_content(Align::center())
174 .cursor(CursorIcon::default())
175 .child(
176 Column::<Node>::new()
177 .with_node(|mut node| node.row_gap = Val::Px(GAP))
178 .item(
179 Row::<Node>::new()
180 .with_node(|mut node| node.padding.left = Val::Px(GAP))
181 .item(
182 El::<Text>::new()
183 .text_font(TextFont::from_font_size(FONT_SIZE))
184 .text(Text::new("width: ")),
185 )
186 .item(
187 El::<Text>::new()
188 .text_font(TextFont::from_font_size(FONT_SIZE))
189 .text_signal(WIDTH.signal_ref(ToString::to_string).map(Text)),
190 ),
191 )
192 .item(menu()),
193 )
194}
195
196fn setup(
197 mut commands: Commands,
198 asset_server: Res<AssetServer>,
199 mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
200) {
201 NINE_SLICE_TEXTURE
202 .set(asset_server.load("panels.png"))
203 .expect("failed to initialize NINE_SLICE_TEXTURE");
204 NINE_SLICE_TEXTURE_ATLAS_LAYOUT
205 .set(texture_atlases.add(TextureAtlasLayout::from_grid(UVec2::new(32, 32), 4, 1, None, None)))
206 .expect("failed to initialize NINE_SLICE_TEXTURE_ATLAS_LAYOUT");
207 IMAGE
208 .set(asset_server.load("icon.png"))
209 .expect("failed to initialize IMAGE");
210 commands.spawn(Camera2d);
211}
212
213fn on_resize(mut resize_events: EventReader<WindowResized>) {
214 for event in resize_events.read() {
215 WIDTH.set(event.width)
216 }
217}