use bevy::prelude::*;
use bevy::text::{Justify, LineBreak, TextLayout};
use bevy::ui::{OverflowAxis, ScrollPosition};
use bevy_material_ui::icon_button::IconButtonClickEvent;
use bevy_material_ui::prelude::*;
use bevy_material_ui::theme::ThemeMode;
use std::collections::HashMap;
#[cfg(target_arch = "wasm32")]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(not(target_arch = "wasm32"))]
use std::fs::File;
#[cfg(not(target_arch = "wasm32"))]
use std::io::Write;
#[derive(Resource, Default)]
pub struct ComponentTelemetry {
pub states: HashMap<String, String>,
pub events: Vec<String>,
pub elements: HashMap<String, ElementBounds>,
pub enabled: bool,
}
#[cfg(target_arch = "wasm32")]
static EVENT_COUNTER: AtomicU64 = AtomicU64::new(0);
impl ComponentTelemetry {
pub fn log_event(&mut self, event: &str) {
#[cfg(target_arch = "wasm32")]
let timestamp = EVENT_COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
#[cfg(not(target_arch = "wasm32"))]
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
self.events.push(format!("[{}] {}", timestamp, event));
if self.events.len() > 100 {
self.events.remove(0);
}
}
#[cfg(target_arch = "wasm32")]
pub fn write_to_file(&self) {
if !self.enabled {
return;
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn write_to_file(&self) {
if !self.enabled {
return;
}
let elements_json: Vec<_> = self
.elements
.values()
.map(|e| {
serde_json::json!({
"test_id": e.test_id,
"x": e.x,
"y": e.y,
"width": e.width,
"height": e.height,
"parent": e.parent,
})
})
.collect();
let json = serde_json::json!({
"states": self.states,
"events": self.events,
"elements": elements_json,
});
if let Ok(mut file) = File::create("telemetry.json") {
let _ = file.write_all(json.to_string().as_bytes());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ComponentSection {
#[default]
Buttons,
ButtonGroup,
Checkboxes,
Switches,
RadioButtons,
Chips,
Fab,
Badges,
Progress,
Cards,
Dividers,
Lists,
Icons,
IconButtons,
Sliders,
TextFields,
Dialogs,
DatePicker,
TimePicker,
Menus,
Tabs,
Select,
Snackbar,
Tooltips,
AppBar,
Toolbar,
Layouts,
LoadingIndicator,
Search,
Elevation,
Motion,
Ripple,
Scroll,
Typography,
UiShapes,
ThemeColors,
Translations,
}
impl ComponentSection {
pub fn i18n_key(&self) -> &'static str {
match self {
Self::Buttons => "showcase.nav.buttons",
Self::ButtonGroup => "showcase.nav.button_group",
Self::Checkboxes => "showcase.nav.checkboxes",
Self::Switches => "showcase.nav.switches",
Self::RadioButtons => "showcase.nav.radio_buttons",
Self::Chips => "showcase.nav.chips",
Self::Fab => "showcase.nav.fab",
Self::Badges => "showcase.nav.badges",
Self::Progress => "showcase.nav.progress",
Self::Cards => "showcase.nav.cards",
Self::Dividers => "showcase.nav.dividers",
Self::Lists => "showcase.nav.lists",
Self::Icons => "showcase.nav.icons",
Self::IconButtons => "showcase.nav.icon_buttons",
Self::Sliders => "showcase.nav.sliders",
Self::TextFields => "showcase.nav.text_fields",
Self::Dialogs => "showcase.nav.dialogs",
Self::DatePicker => "showcase.nav.date_picker",
Self::TimePicker => "showcase.nav.time_picker",
Self::Menus => "showcase.nav.menus",
Self::Tabs => "showcase.nav.tabs",
Self::Select => "showcase.nav.select",
Self::Snackbar => "showcase.nav.snackbar",
Self::Tooltips => "showcase.nav.tooltips",
Self::AppBar => "showcase.nav.app_bar",
Self::Toolbar => "showcase.nav.toolbar",
Self::Layouts => "showcase.nav.layouts",
Self::LoadingIndicator => "showcase.nav.loading_indicator",
Self::Search => "showcase.nav.search",
Self::Elevation => "showcase.nav.elevation",
Self::Motion => "showcase.nav.motion",
Self::Ripple => "showcase.nav.ripple",
Self::Scroll => "showcase.nav.scroll",
Self::Typography => "showcase.nav.typography",
Self::UiShapes => "showcase.nav.ui_shapes",
Self::ThemeColors => "showcase.nav.theme_colors",
Self::Translations => "showcase.nav.translations",
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::Buttons => "Buttons",
Self::ButtonGroup => "Button Groups",
Self::Checkboxes => "Checkboxes",
Self::Switches => "Switches",
Self::RadioButtons => "Radio Buttons",
Self::Chips => "Chips",
Self::Fab => "FAB",
Self::Badges => "Badges",
Self::Progress => "Progress",
Self::Cards => "Cards",
Self::Dividers => "Dividers",
Self::Lists => "Lists",
Self::Icons => "Icons",
Self::IconButtons => "Icon Buttons",
Self::Sliders => "Sliders",
Self::TextFields => "Text Fields",
Self::Dialogs => "Dialogs",
Self::DatePicker => "Date Picker",
Self::TimePicker => "Time Picker",
Self::Menus => "Menus",
Self::Tabs => "Tabs",
Self::Select => "Select",
Self::Snackbar => "Snackbar",
Self::Tooltips => "Tooltips",
Self::AppBar => "App Bar",
Self::Toolbar => "Toolbar",
Self::Layouts => "Layouts",
Self::LoadingIndicator => "Loading Indicator",
Self::Search => "Search",
Self::Elevation => "Elevation",
Self::Motion => "Motion",
Self::Ripple => "Ripple",
Self::Scroll => "Scroll",
Self::Typography => "Typography",
Self::UiShapes => "UI Shapes",
Self::ThemeColors => "Theme Colors",
Self::Translations => "Translations",
}
}
pub fn telemetry_name(&self) -> &'static str {
match self {
Self::Buttons => "Buttons",
Self::ButtonGroup => "ButtonGroup",
Self::Checkboxes => "Checkboxes",
Self::Switches => "Switches",
Self::RadioButtons => "RadioButtons",
Self::Chips => "Chips",
Self::Fab => "FAB",
Self::Badges => "Badges",
Self::Progress => "Progress",
Self::Cards => "Cards",
Self::Dividers => "Dividers",
Self::Lists => "Lists",
Self::Icons => "Icons",
Self::IconButtons => "IconButtons",
Self::Sliders => "Sliders",
Self::TextFields => "TextFields",
Self::Dialogs => "Dialogs",
Self::DatePicker => "DatePicker",
Self::TimePicker => "TimePicker",
Self::Menus => "Menus",
Self::Tabs => "Tabs",
Self::Select => "Select",
Self::Snackbar => "Snackbar",
Self::Tooltips => "Tooltips",
Self::AppBar => "AppBar",
Self::Toolbar => "Toolbar",
Self::Layouts => "Layouts",
Self::LoadingIndicator => "LoadingIndicator",
Self::Search => "Search",
Self::Elevation => "Elevation",
Self::Motion => "Motion",
Self::Ripple => "Ripple",
Self::Scroll => "Scroll",
Self::Typography => "Typography",
Self::UiShapes => "UiShapes",
Self::ThemeColors => "ThemeColors",
Self::Translations => "Translations",
}
}
pub fn all() -> &'static [ComponentSection] {
&[
Self::Buttons,
Self::ButtonGroup,
Self::Checkboxes,
Self::Switches,
Self::RadioButtons,
Self::Chips,
Self::Fab,
Self::Badges,
Self::Progress,
Self::Cards,
Self::Dividers,
Self::Lists,
Self::Icons,
Self::IconButtons,
Self::Sliders,
Self::TextFields,
Self::Dialogs,
Self::DatePicker,
Self::TimePicker,
Self::Menus,
Self::Tabs,
Self::Select,
Self::Snackbar,
Self::Tooltips,
Self::AppBar,
Self::Toolbar,
Self::Layouts,
Self::LoadingIndicator,
Self::Search,
Self::Elevation,
Self::Motion,
Self::Ripple,
Self::Scroll,
Self::Typography,
Self::UiShapes,
Self::ThemeColors,
Self::Translations,
]
}
}
#[derive(Resource, Default)]
pub struct SelectedSection {
pub current: ComponentSection,
}
#[derive(Component)]
pub struct DetailContent;
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
pub enum DialogPosition {
#[default]
CenterWindow,
CenterParent,
BelowTrigger,
AboveTrigger,
RightOfTrigger,
LeftOfTrigger,
}
#[derive(Resource)]
pub struct TooltipDemoOptions {
pub position: TooltipPosition,
pub delay: f32,
}
impl Default for TooltipDemoOptions {
fn default() -> Self {
Self {
position: TooltipPosition::Bottom,
delay: 0.5,
}
}
}
#[derive(Resource)]
pub struct SnackbarDemoOptions {
pub duration: f32,
pub has_action: bool,
}
impl Default for SnackbarDemoOptions {
fn default() -> Self {
Self {
duration: 4.0,
has_action: false,
}
}
}
#[derive(Component)]
pub struct SelectableListItem;
#[derive(Component)]
pub struct ListDemoRoot;
#[derive(Component)]
pub struct DialogContainer;
#[derive(Component)]
pub struct DialogsSectionRoot;
#[derive(Component)]
pub struct ShowDialogButton;
#[derive(Component)]
pub struct DialogCloseButton;
#[derive(Component)]
pub struct DialogConfirmButton;
#[derive(Component)]
pub struct DialogResultDisplay;
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct DialogModalOption(pub bool);
#[derive(Component)]
pub struct DatePickerOpenButton(pub Entity);
#[derive(Component)]
pub struct DatePickerResultDisplay(pub Entity);
#[derive(Component)]
pub struct TimePickerOpenButton(pub Entity);
#[derive(Component)]
pub struct TimePickerResultDisplay(pub Entity);
#[derive(Component)]
pub struct MenuTrigger;
#[derive(Component)]
pub struct MenuDropdown;
#[derive(Component)]
pub struct MenuItemMarker(pub String);
#[derive(Component)]
pub struct MenuSelectedText;
#[derive(Component)]
pub struct SnackbarTrigger;
#[derive(Component)]
pub struct IconButtonMarker;
#[derive(Component)]
pub struct TooltipDemoButton;
#[derive(Component)]
pub struct TooltipPositionOption(pub TooltipPosition);
#[derive(Component)]
pub struct TooltipDelayOption(pub f32);
#[derive(Component)]
pub struct SnackbarDurationOption(pub f32);
#[derive(Component)]
pub struct SnackbarActionToggle;
#[derive(Component)]
pub struct ThemeModeOption(pub ThemeMode);
#[derive(Resource, Debug, Clone, Copy)]
pub struct ShowcaseThemeSelection {
pub seed_argb: u32,
}
impl Default for ShowcaseThemeSelection {
fn default() -> Self {
Self {
seed_argb: 0xFF6750A4,
}
}
}
#[derive(Component)]
pub struct ThemeSeedOption(pub u32);
#[derive(Component)]
pub struct ThemeSeedTextFieldSlot;
#[derive(Component)]
pub struct ThemeSeedTextField;
#[derive(Component)]
pub struct DialogPositionOption(pub DialogPosition);
#[derive(Component)]
pub struct ListSelectionModeOption(pub bevy_material_ui::list::ListSelectionMode);
#[derive(Component)]
pub struct CodeBlockSnippet(pub String);
#[derive(Component)]
pub struct CodeBlockCopyButton(pub Entity);
#[cfg(feature = "clipboard")]
pub fn try_copy_to_clipboard(text: &str) -> Result<(), String> {
let mut clipboard = arboard::Clipboard::new().map_err(|e| e.to_string())?;
clipboard
.set_text(text.to_string())
.map_err(|e| e.to_string())
}
#[cfg(not(feature = "clipboard"))]
pub fn try_copy_to_clipboard(_text: &str) -> Result<(), String> {
Err("Clipboard support is disabled. Run with `--features clipboard`.".to_string())
}
pub fn code_block_copy_system(
mut click_events: MessageReader<IconButtonClickEvent>,
buttons: Query<&CodeBlockCopyButton>,
snippets: Query<&CodeBlockSnippet>,
mut telemetry: ResMut<ComponentTelemetry>,
) {
for ev in click_events.read() {
let Ok(target) = buttons.get(ev.entity) else {
continue;
};
let Ok(snippet) = snippets.get(target.0) else {
telemetry.log_event("Showcase: failed to copy code block (missing snippet)");
continue;
};
match try_copy_to_clipboard(&snippet.0) {
Ok(()) => {
telemetry.log_event("Showcase: copied code block to clipboard");
info!("Copied code block to clipboard");
}
Err(err) => {
telemetry.log_event("Showcase: failed to copy code block");
warn!("Failed to copy code block to clipboard: {err}");
}
}
}
}
pub fn spawn_code_block(parent: &mut ChildSpawnerCommands, theme: &MaterialTheme, code: &str) {
let mut block_commands = parent.spawn((
CodeBlockSnippet(code.to_owned()),
Node {
width: Val::Percent(100.0),
padding: UiRect::all(Val::Px(16.0)),
margin: UiRect::top(Val::Px(8.0)),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(8.0),
border_radius: BorderRadius::all(Val::Px(8.0)),
..default()
},
BackgroundColor(theme.surface_container.with_alpha(0.8)),
));
let block_entity = block_commands.id();
block_commands.with_children(|block| {
block
.spawn((Node {
width: Val::Percent(100.0),
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::FlexStart,
align_items: AlignItems::Center,
column_gap: Val::Px(8.0),
..default()
},))
.with_children(|header| {
let disabled = !cfg!(feature = "clipboard");
let mut button_style = MaterialIconButton::new("content_copy");
button_style.variant = IconButtonVariant::FilledTonal;
button_style.disabled = disabled;
let icon_color = button_style.icon_color(theme);
header
.spawn((
CodeBlockCopyButton(block_entity),
IconButtonBuilder::new("content_copy")
.filled_tonal()
.disabled(disabled)
.build(theme),
Interaction::None,
))
.with_children(|btn| {
if let Some(icon) = MaterialIcon::from_name("content_copy")
.or_else(|| MaterialIcon::from_name("copy"))
.or_else(|| MaterialIcon::from_name("content_paste"))
{
btn.spawn(
icon.with_size(bevy_material_ui::icon_button::ICON_SIZE)
.with_color(icon_color),
);
}
});
header.spawn((
Text::new("Copy"),
TextFont {
font_size: 14.0,
..default()
},
TextColor(theme.on_surface_variant),
));
});
block
.spawn((
ScrollContainerBuilder::new()
.horizontal()
.with_scrollbars(false)
.build(),
ScrollPosition::default(),
Node {
width: Val::Percent(100.0),
overflow: Overflow {
x: OverflowAxis::Scroll,
y: OverflowAxis::Visible,
},
..default()
},
))
.with_children(|scroller| {
scroller.spawn((
Text::new(code),
TextFont {
font_size: 12.0,
..default()
},
TextColor(theme.on_surface.with_alpha(0.87)),
TextLayout::new(Justify::Left, LineBreak::NoWrap),
Node {
min_width: Val::Px(0.0),
..default()
},
));
});
});
}
pub fn spawn_section_header(
parent: &mut ChildSpawnerCommands,
theme: &MaterialTheme,
title_key: &str,
title_default: &str,
description_key: &str,
description_default: &str,
) {
parent.spawn((
Text::new(""),
LocalizedText::new(title_key).with_default(title_default),
TextFont {
font_size: 22.0,
..default()
},
TextColor(theme.primary),
NeedsInternationalFont, ));
if !description_default.is_empty() {
parent.spawn((
Text::new(""),
LocalizedText::new(description_key).with_default(description_default),
TextFont {
font_size: 14.0,
..default()
},
TextColor(theme.on_surface_variant),
Node {
margin: UiRect::bottom(Val::Px(8.0)),
..default()
},
NeedsInternationalFont, ));
}
}
#[derive(Component)]
pub struct NeedsInternationalFont;