1use beuvy_runtime::button::ButtonClickMessage;
2use beuvy_runtime::input::{InputType, InputValueChangedMessage};
3use beuvy_runtime::text::FontResource;
4use beuvy_runtime::text::set_plain_text;
5use beuvy_runtime::{
6 AddButton, AddInput, AddSelect, AddSelectOption, AddText, SelectValueChangedMessage,
7 UiKitPlugin,
8};
9use bevy::prelude::*;
10use bevy::text::TextLayout;
11
12#[derive(Component)]
13struct EventLogText;
14
15#[derive(Resource, Default)]
16struct EventLog {
17 entries: Vec<String>,
18}
19
20fn main() {
21 App::new()
22 .init_resource::<EventLog>()
23 .add_plugins(DefaultPlugins.set(WindowPlugin {
24 primary_window: Some(Window {
25 title: "beuvy-runtime control events".to_string(),
26 resolution: (1180, 720).into(),
27 ..default()
28 }),
29 ..default()
30 }))
31 .add_plugins(UiKitPlugin)
32 .add_systems(Startup, setup)
33 .add_systems(
34 Update,
35 (
36 record_button_events,
37 record_input_events,
38 record_select_events,
39 ),
40 )
41 .run();
42}
43
44fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
45 commands.spawn(Camera2d);
46 commands.insert_resource(FontResource::from_handle(
47 asset_server.load("fonts/SarasaFixedSC-Regular.ttf"),
48 ));
49
50 commands
51 .spawn((
52 Node {
53 width: Val::Percent(100.0),
54 height: Val::Percent(100.0),
55 padding: UiRect::all(Val::Px(24.0)),
56 column_gap: Val::Px(20.0),
57 overflow: Overflow::visible(),
58 ..default()
59 },
60 BackgroundColor(Color::srgb_u8(248, 250, 252)),
61 ))
62 .with_children(|parent| {
63 let mut left = parent.spawn(Node {
64 width: Val::Px(420.0),
65 padding: UiRect::all(Val::Px(16.0)),
66 row_gap: Val::Px(12.0),
67 flex_direction: FlexDirection::Column,
68 overflow: Overflow::visible(),
69 border_radius: BorderRadius::all(Val::Px(12.0)),
70 ..default()
71 });
72 left.insert(BorderColor::all(Color::srgb_u8(203, 213, 225)));
73 left.insert(BackgroundColor(Color::WHITE));
74 left.with_children(|parent| {
75 spawn_text(
76 parent,
77 "Interact with these controls",
78 18.0,
79 Color::srgb_u8(15, 23, 42),
80 );
81 parent.spawn(AddInput {
82 name: "display_name".to_string(),
83 placeholder: "Type a display name".to_string(),
84 size_chars: Some(24),
85 ..default()
86 });
87 parent.spawn(AddInput {
88 name: "zoom".to_string(),
89 input_type: InputType::Range,
90 value: "35".to_string(),
91 min: Some(0.0),
92 max: Some(100.0),
93 step: Some(5.0),
94 ..default()
95 });
96 parent.spawn(AddSelect {
97 name: "theme".to_string(),
98 value: "light".to_string(),
99 options: vec![
100 option("theme_light", "light", "Light"),
101 option("theme_dark", "dark", "Dark"),
102 option("theme_hc", "high-contrast", "High Contrast"),
103 ],
104 ..default()
105 });
106 parent
107 .spawn(Node {
108 column_gap: Val::Px(10.0),
109 flex_wrap: FlexWrap::Wrap,
110 ..default()
111 })
112 .with_children(|parent| {
113 parent.spawn(AddButton {
114 name: "save".to_string(),
115 text: "Save".to_string(),
116 class: Some("button-root w-[120px]".to_string()),
117 ..default()
118 });
119 parent.spawn(AddButton {
120 name: "reset".to_string(),
121 text: "Reset".to_string(),
122 class: Some("button-root w-[120px]".to_string()),
123 ..default()
124 });
125 });
126 });
127
128 let mut right = parent.spawn(Node {
129 flex_grow: 1.0,
130 padding: UiRect::all(Val::Px(16.0)),
131 row_gap: Val::Px(10.0),
132 flex_direction: FlexDirection::Column,
133 overflow: Overflow::visible(),
134 border_radius: BorderRadius::all(Val::Px(12.0)),
135 ..default()
136 });
137 right.insert(BorderColor::all(Color::srgb_u8(203, 213, 225)));
138 right.insert(BackgroundColor(Color::WHITE));
139 right.with_children(|parent| {
140 spawn_text(parent, "Event log", 18.0, Color::srgb_u8(15, 23, 42));
141 parent.spawn((
142 EventLogText,
143 Node {
144 width: Val::Percent(100.0),
145 ..default()
146 },
147 TextLayout::default(),
148 AddText {
149 text: "No events yet.\nClick, type, drag, or select.".to_string(),
150 size: 14.0,
151 color: Color::srgb_u8(71, 85, 105),
152 ..default()
153 },
154 ));
155 });
156 });
157}
158
159fn record_button_events(
160 mut commands: Commands,
161 mut events: MessageReader<ButtonClickMessage>,
162 mut log: ResMut<EventLog>,
163 labels: Query<Entity, With<EventLogText>>,
164) {
165 let mut changed = false;
166 for event in events.read() {
167 log.entries.push(format!(
168 "button:{} on {:?}",
169 event.button.name, event.entity
170 ));
171 changed = true;
172 }
173 if changed {
174 sync_event_log_text(&mut commands, &log, &labels);
175 }
176}
177
178fn record_input_events(
179 mut commands: Commands,
180 mut events: MessageReader<InputValueChangedMessage>,
181 mut log: ResMut<EventLog>,
182 labels: Query<Entity, With<EventLogText>>,
183) {
184 let mut changed = false;
185 for event in events.read() {
186 log.entries
187 .push(format!("input:{} = {}", event.name, event.value));
188 changed = true;
189 }
190 if changed {
191 sync_event_log_text(&mut commands, &log, &labels);
192 }
193}
194
195fn record_select_events(
196 mut commands: Commands,
197 mut events: MessageReader<SelectValueChangedMessage>,
198 mut log: ResMut<EventLog>,
199 labels: Query<Entity, With<EventLogText>>,
200) {
201 let mut changed = false;
202 for event in events.read() {
203 log.entries
204 .push(format!("select:{} = {}", event.name, event.value));
205 changed = true;
206 }
207 if changed {
208 sync_event_log_text(&mut commands, &log, &labels);
209 }
210}
211
212fn sync_event_log_text(
213 commands: &mut Commands,
214 log: &EventLog,
215 labels: &Query<Entity, With<EventLogText>>,
216) {
217 let Some(entity) = labels.iter().next() else {
218 return;
219 };
220 let text = log
221 .entries
222 .iter()
223 .rev()
224 .take(12)
225 .cloned()
226 .collect::<Vec<_>>()
227 .join("\n");
228 set_plain_text(commands, entity, text);
229}
230
231fn option(name: &str, value: &str, text: &str) -> AddSelectOption {
232 AddSelectOption {
233 name: name.to_string(),
234 value: value.to_string(),
235 text: text.to_string(),
236 localized_text: None,
237 localized_text_format: None,
238 disabled: false,
239 }
240}
241
242fn spawn_text(parent: &mut ChildSpawnerCommands, text: &str, size: f32, color: Color) {
243 parent.spawn((
244 Node {
245 width: Val::Percent(100.0),
246 ..default()
247 },
248 TextLayout::default(),
249 AddText {
250 text: text.to_string(),
251 size,
252 color,
253 ..default()
254 },
255 ));
256}