use crate::{
derived_screen_ring,
particles::{MistSmokeConfig, MistSmokePreset, MistSmokeSurface, MistSmokeTarget},
SmokeBorder, SmokeRingPadding,
};
use bevy::input::{
keyboard::{Key, KeyCode, KeyboardInput},
mouse::{MouseButton, MouseScrollUnit, MouseWheel},
ButtonInput, ButtonState,
};
use bevy::prelude::*;
use bevy::text::{Justify, TextLayout};
use bevy::ui::{ComputedNode, RelativeCursorPosition, ScrollPosition};
const CONTROL_HEIGHT: f32 = 50.0;
const CONTROL_RADIUS: f32 = 14.0;
const CONTROL_PADDING_X: f32 = 14.0;
const CONTROL_PADDING_Y: f32 = 10.0;
const SLIDER_HANDLE_WIDTH: f32 = 14.0;
const PROGRESS_LERP_RATE: f32 = 6.0;
const SWITCH_TRACK_WIDTH: f32 = 54.0;
const SWITCH_TRACK_HEIGHT: f32 = 28.0;
const SWITCH_KNOB_SIZE: f32 = 22.0;
const SCROLL_STEP_PX: f32 = 36.0;
fn clamp_choice_index(len: usize, index: usize) -> usize {
index.min(len.saturating_sub(1))
}
fn panel_fill() -> Color {
Color::srgba(0.01, 0.03, 0.05, 0.34)
}
fn control_fill() -> Color {
Color::srgba(0.0, 0.02, 0.04, 0.10)
}
fn hovered_fill() -> Color {
Color::srgba(0.01, 0.04, 0.07, 0.14)
}
fn pressed_fill() -> Color {
Color::srgba(0.02, 0.05, 0.09, 0.18)
}
fn text_primary() -> Color {
Color::srgba(0.97, 0.99, 1.0, 0.98)
}
fn text_secondary() -> Color {
Color::srgba(0.82, 0.88, 0.96, 0.96)
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
enum MistSmokeRole {
StandardButton,
ToolbarButton,
DropdownOption,
PanelFrame,
ScrollContainer,
DialogFrame,
ScalarControl,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum MistSmokeSurfaceRole {
ControlBody,
ContainerBody,
OptionBody,
AccentChip,
AccentOrb,
ScalarTrack,
ScalarFill,
BackdropVeil,
}
fn smoke_padding_for_role(role: MistSmokeRole) -> SmokeRingPadding {
match role {
MistSmokeRole::PanelFrame | MistSmokeRole::ScrollContainer | MistSmokeRole::DialogFrame => {
SmokeRingPadding::all(9.0)
}
MistSmokeRole::DropdownOption => SmokeRingPadding::all(6.0),
MistSmokeRole::StandardButton
| MistSmokeRole::ToolbarButton
| MistSmokeRole::ScalarControl => SmokeRingPadding::all(8.0),
}
}
fn smoke_config_for_role(role: MistSmokeRole, pressed: bool) -> MistSmokeConfig {
let mut smoke = match role {
MistSmokeRole::StandardButton => {
MistSmokeConfig::screen_preset(MistSmokePreset::StandardButton)
}
MistSmokeRole::ToolbarButton => {
MistSmokeConfig::screen_preset(MistSmokePreset::ToolbarButton)
}
MistSmokeRole::DropdownOption => {
MistSmokeConfig::screen_preset(MistSmokePreset::DropdownOption)
}
MistSmokeRole::PanelFrame => MistSmokeConfig::screen_preset(MistSmokePreset::PanelFrame),
MistSmokeRole::ScrollContainer => {
MistSmokeConfig::screen_preset(MistSmokePreset::ScrollbarTrack)
}
MistSmokeRole::DialogFrame => MistSmokeConfig::screen_preset(MistSmokePreset::DialogFrame),
MistSmokeRole::ScalarControl => {
MistSmokeConfig::screen_preset(MistSmokePreset::ScrollbarThumb)
}
};
match role {
MistSmokeRole::StandardButton => {
smoke.thickness = if pressed { 0.20 } else { 0.16 };
smoke.intensity = if pressed { 4.7 } else { 3.95 };
smoke.softness = if pressed { 0.30 } else { 0.27 };
smoke.flow_speed = if pressed { 0.92 } else { 0.84 };
smoke.noise_scale = 24.0;
smoke.pulse_strength = if pressed { 0.20 } else { 0.13 };
smoke.particle_density = if pressed { 2.25 } else { 1.92 };
smoke.particle_size_scale = if pressed { 1.10 } else { 0.98 };
}
MistSmokeRole::ToolbarButton => {
smoke.thickness = if pressed { 0.19 } else { 0.15 };
smoke.intensity = if pressed { 4.6 } else { 3.85 };
smoke.softness = if pressed { 0.29 } else { 0.26 };
smoke.flow_speed = if pressed { 0.96 } else { 0.86 };
smoke.noise_scale = 23.0;
smoke.pulse_strength = if pressed { 0.20 } else { 0.14 };
smoke.particle_density = if pressed { 2.18 } else { 1.86 };
smoke.particle_size_scale = if pressed { 1.08 } else { 0.96 };
}
MistSmokeRole::DropdownOption => {
smoke.thickness = if pressed { 0.17 } else { 0.13 };
smoke.intensity = if pressed { 4.0 } else { 3.25 };
smoke.softness = if pressed { 0.30 } else { 0.27 };
smoke.flow_speed = if pressed { 0.88 } else { 0.78 };
smoke.noise_scale = 24.0;
smoke.pulse_strength = if pressed { 0.18 } else { 0.12 };
smoke.particle_density = if pressed { 1.94 } else { 1.58 };
smoke.particle_size_scale = if pressed { 1.06 } else { 0.94 };
}
MistSmokeRole::PanelFrame => {
smoke.thickness = if pressed { 0.18 } else { 0.16 };
smoke.intensity = if pressed { 4.2 } else { 3.55 };
smoke.softness = if pressed { 0.32 } else { 0.29 };
smoke.flow_speed = if pressed { 0.82 } else { 0.74 };
smoke.noise_scale = 24.0;
smoke.pulse_strength = if pressed { 0.18 } else { 0.12 };
smoke.particle_density = if pressed { 2.10 } else { 1.70 };
smoke.particle_size_scale = if pressed { 1.12 } else { 1.00 };
}
MistSmokeRole::ScrollContainer => {
smoke.thickness = if pressed { 0.17 } else { 0.15 };
smoke.intensity = if pressed { 4.0 } else { 3.35 };
smoke.softness = if pressed { 0.32 } else { 0.29 };
smoke.flow_speed = if pressed { 0.80 } else { 0.72 };
smoke.noise_scale = 24.0;
smoke.pulse_strength = if pressed { 0.18 } else { 0.12 };
smoke.particle_density = if pressed { 1.98 } else { 1.58 };
smoke.particle_size_scale = if pressed { 1.10 } else { 0.98 };
}
MistSmokeRole::DialogFrame => {
smoke.thickness = if pressed { 0.20 } else { 0.17 };
smoke.intensity = if pressed { 4.45 } else { 3.75 };
smoke.softness = if pressed { 0.32 } else { 0.29 };
smoke.flow_speed = if pressed { 0.86 } else { 0.76 };
smoke.noise_scale = 25.0;
smoke.pulse_strength = if pressed { 0.20 } else { 0.13 };
smoke.particle_density = if pressed { 2.14 } else { 1.74 };
smoke.particle_size_scale = if pressed { 1.14 } else { 1.02 };
}
MistSmokeRole::ScalarControl => {
smoke.thickness = if pressed { 0.18 } else { 0.14 };
smoke.intensity = if pressed { 4.05 } else { 3.35 };
smoke.softness = if pressed { 0.30 } else { 0.27 };
smoke.flow_speed = if pressed { 0.92 } else { 0.82 };
smoke.noise_scale = 24.0;
smoke.pulse_strength = if pressed { 0.20 } else { 0.13 };
smoke.particle_density = if pressed { 2.04 } else { 1.64 };
smoke.particle_size_scale = if pressed { 1.10 } else { 0.98 };
}
}
smoke
}
fn smoke_border_for_role(role: MistSmokeRole, pressed: bool, _seed: u64) -> SmokeBorder {
derived_screen_ring(smoke_config_for_role(role, pressed))
}
fn surface_smoke_for_role(role: MistSmokeSurfaceRole, active: bool) -> MistSmokeSurface {
let (mut config, inset) = match role {
MistSmokeSurfaceRole::ControlBody => (
MistSmokeConfig::screen_preset(MistSmokePreset::StandardButton),
Vec2::new(8.0, 6.0),
),
MistSmokeSurfaceRole::ContainerBody => (
MistSmokeConfig::screen_preset(MistSmokePreset::PanelFrame),
Vec2::new(10.0, 8.0),
),
MistSmokeSurfaceRole::OptionBody => (
MistSmokeConfig::screen_preset(MistSmokePreset::DropdownOption),
Vec2::new(6.0, 4.0),
),
MistSmokeSurfaceRole::AccentChip => (
MistSmokeConfig::screen_preset(MistSmokePreset::PrimaryAction),
Vec2::new(2.0, 2.0),
),
MistSmokeSurfaceRole::AccentOrb => (
MistSmokeConfig::screen_preset(MistSmokePreset::PrimaryAction),
Vec2::new(1.0, 1.0),
),
MistSmokeSurfaceRole::ScalarTrack => (
MistSmokeConfig::screen_preset(MistSmokePreset::ScrollbarTrack),
Vec2::new(2.0, 2.0),
),
MistSmokeSurfaceRole::ScalarFill => (
MistSmokeConfig::screen_preset(MistSmokePreset::ScrollbarThumb),
Vec2::new(1.0, 1.0),
),
MistSmokeSurfaceRole::BackdropVeil => (
MistSmokeConfig::screen_preset(MistSmokePreset::PanelFrame),
Vec2::ZERO,
),
};
match role {
MistSmokeSurfaceRole::ControlBody => {
config.thickness = if active { 0.14 } else { 0.10 };
config.intensity = if active { 2.20 } else { 1.55 };
config.flow_speed = if active { 0.78 } else { 0.68 };
config.noise_scale = 34.0;
config.softness = if active { 0.50 } else { 0.46 };
config.pulse_strength = if active { 0.18 } else { 0.10 };
config.particle_density = if active { 0.52 } else { 0.28 };
config.particle_size_scale = if active { 0.92 } else { 0.78 };
}
MistSmokeSurfaceRole::ContainerBody => {
config.thickness = if active { 0.14 } else { 0.12 };
config.intensity = if active { 2.10 } else { 1.45 };
config.flow_speed = if active { 0.68 } else { 0.60 };
config.noise_scale = 36.0;
config.softness = 0.58;
config.pulse_strength = if active { 0.14 } else { 0.08 };
config.particle_density = if active { 0.48 } else { 0.26 };
config.particle_size_scale = if active { 0.96 } else { 0.82 };
}
MistSmokeSurfaceRole::OptionBody => {
config.thickness = if active { 0.12 } else { 0.10 };
config.intensity = if active { 1.95 } else { 1.30 };
config.flow_speed = if active { 0.74 } else { 0.64 };
config.noise_scale = 32.0;
config.softness = 0.52;
config.pulse_strength = if active { 0.14 } else { 0.08 };
config.particle_density = if active { 0.44 } else { 0.24 };
config.particle_size_scale = if active { 0.88 } else { 0.74 };
}
MistSmokeSurfaceRole::AccentChip => {
config.thickness = if active { 0.18 } else { 0.12 };
config.intensity = if active { 3.65 } else { 2.20 };
config.flow_speed = if active { 0.92 } else { 0.74 };
config.noise_scale = 30.0;
config.softness = if active { 0.48 } else { 0.42 };
config.pulse_strength = if active { 0.26 } else { 0.10 };
config.particle_density = if active { 1.64 } else { 0.32 };
config.particle_size_scale = if active { 1.12 } else { 0.84 };
}
MistSmokeSurfaceRole::AccentOrb => {
config.thickness = if active { 0.18 } else { 0.14 };
config.intensity = if active { 3.85 } else { 2.55 };
config.flow_speed = if active { 0.96 } else { 0.80 };
config.noise_scale = 28.0;
config.softness = 0.46;
config.pulse_strength = if active { 0.28 } else { 0.12 };
config.particle_density = if active { 1.82 } else { 0.92 };
config.particle_size_scale = if active { 1.18 } else { 1.0 };
}
MistSmokeSurfaceRole::ScalarTrack => {
config.thickness = if active { 0.16 } else { 0.12 };
config.intensity = if active { 2.90 } else { 2.10 };
config.flow_speed = if active { 0.86 } else { 0.72 };
config.noise_scale = 30.0;
config.softness = 0.50;
config.pulse_strength = if active { 0.20 } else { 0.12 };
config.particle_density = if active { 1.06 } else { 0.72 };
config.particle_size_scale = if active { 1.18 } else { 1.02 };
}
MistSmokeSurfaceRole::ScalarFill => {
config.thickness = if active { 0.18 } else { 0.14 };
config.intensity = if active { 3.55 } else { 2.70 };
config.flow_speed = if active { 0.94 } else { 0.80 };
config.noise_scale = 28.0;
config.softness = 0.46;
config.pulse_strength = if active { 0.24 } else { 0.14 };
config.particle_density = if active { 1.48 } else { 1.08 };
config.particle_size_scale = if active { 1.18 } else { 1.06 };
}
MistSmokeSurfaceRole::BackdropVeil => {
config.thickness = 0.18;
config.intensity = if active { 2.40 } else { 1.75 };
config.flow_speed = 0.56;
config.noise_scale = 42.0;
config.softness = 0.64;
config.pulse_strength = if active { 0.14 } else { 0.08 };
config.particle_density = if active { 0.54 } else { 0.32 };
config.particle_size_scale = 1.64;
}
}
MistSmokeSurface::new(config).with_inset(inset.x, inset.y)
}
fn surface_smoke_for_widget_role(role: MistSmokeRole, active: bool) -> MistSmokeSurface {
let surface_role = match role {
MistSmokeRole::PanelFrame | MistSmokeRole::ScrollContainer | MistSmokeRole::DialogFrame => {
MistSmokeSurfaceRole::ContainerBody
}
MistSmokeRole::DropdownOption => MistSmokeSurfaceRole::OptionBody,
MistSmokeRole::ScalarControl => MistSmokeSurfaceRole::ScalarTrack,
MistSmokeRole::StandardButton | MistSmokeRole::ToolbarButton => {
MistSmokeSurfaceRole::ControlBody
}
};
surface_smoke_for_role(surface_role, active)
}
fn text_line(font: &Handle<Font>, value: &str, size: f32, color: Color) -> impl Bundle {
(
Text::new(value),
Node::default(),
TextFont {
font: font.clone(),
font_size: size,
..default()
},
TextColor(color),
TextLayout::new_with_justify(Justify::Left).with_no_wrap(),
)
}
fn text_block(font: &Handle<Font>, value: &str, size: f32, color: Color) -> impl Bundle {
(
Text::new(value),
Node::default(),
TextFont {
font: font.clone(),
font_size: size,
..default()
},
TextColor(color),
TextLayout::new_with_justify(Justify::Left),
)
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistInteractiveStyle {
pub idle_fill: Color,
pub hover_fill: Color,
pub pressed_fill: Color,
pub idle_border: Color,
pub hover_border: Color,
pub pressed_border: Color,
}
impl Default for MistInteractiveStyle {
fn default() -> Self {
Self {
idle_fill: control_fill(),
hover_fill: hovered_fill(),
pressed_fill: pressed_fill(),
idle_border: Color::NONE,
hover_border: Color::NONE,
pressed_border: Color::NONE,
}
}
}
#[derive(Component)]
pub struct MistPanel;
#[derive(Component)]
pub struct MistLabel;
#[derive(Component)]
pub struct MistImage;
#[derive(Component)]
pub struct MistButton;
#[derive(Component)]
pub struct MistTrigger;
#[derive(Component)]
pub struct MistCheckbox;
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct MistCheckboxState {
pub checked: bool,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistCheckboxParts {
indicator: Entity,
glyph: Entity,
}
#[derive(Component, Clone, Debug)]
pub struct MistRadioGroup {
pub selected: usize,
}
#[derive(Component, Clone, Debug)]
pub struct MistRadioOptions(pub Vec<String>);
#[derive(Component, Clone, Copy, Debug)]
pub struct MistRadioOwner(pub Entity);
#[derive(Component, Clone, Copy, Debug)]
pub struct MistRadioOption {
pub index: usize,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistRadioParts {
indicator: Entity,
glyph: Entity,
label: Entity,
}
#[derive(Component)]
pub struct MistSwitch;
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct MistSwitchState {
pub on: bool,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistSwitchParts {
track: Entity,
knob: Entity,
}
#[derive(Component)]
pub struct MistScrollView;
#[derive(Component)]
pub struct MistScrollViewport;
#[derive(Component)]
pub struct MistScrollContent;
#[derive(Component, Clone, Copy, Debug)]
pub struct MistScrollParts {
pub viewport: Entity,
pub content: Entity,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistSlider {
pub min: f32,
pub max: f32,
}
impl MistSlider {
pub fn new(min: f32, max: f32) -> Self {
Self { min, max }
}
fn normalize(&self, value: f32) -> f32 {
let span = (self.max - self.min).max(f32::EPSILON);
((value - self.min) / span).clamp(0.0, 1.0)
}
fn denormalize(&self, t: f32) -> f32 {
self.min + (self.max - self.min) * t.clamp(0.0, 1.0)
}
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistSliderValue(pub f32);
#[derive(Component, Clone, Copy, Debug)]
pub struct MistSliderParts {
fill: Entity,
handle: Entity,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistProgressBar {
pub displayed: f32,
pub target: f32,
}
impl Default for MistProgressBar {
fn default() -> Self {
Self {
displayed: 0.0,
target: 0.0,
}
}
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistProgressParts {
fill: Entity,
}
#[derive(Component, Clone, Debug)]
pub struct MistInputField {
pub value: String,
pub placeholder: String,
pub max_chars: Option<usize>,
}
impl MistInputField {
pub fn new(placeholder: impl Into<String>) -> Self {
Self {
value: String::new(),
placeholder: placeholder.into(),
max_chars: None,
}
}
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self
}
pub fn with_max_chars(mut self, max_chars: usize) -> Self {
self.max_chars = Some(max_chars);
self
}
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistInputParts {
value_text: Entity,
placeholder_text: Entity,
caret: Entity,
}
#[derive(Component)]
pub struct MistInputFocused;
#[derive(Resource, Default)]
struct MistInputFocusState {
active: Option<Entity>,
}
#[derive(Component)]
pub struct MistTooltipAnchor;
#[derive(Component, Clone, Copy, Debug)]
pub struct MistTooltipOwner(pub Entity);
#[derive(Component, Clone, Debug)]
pub struct MistTooltip {
pub enabled: bool,
}
impl Default for MistTooltip {
fn default() -> Self {
Self { enabled: true }
}
}
#[derive(Component, Clone, Debug, Default)]
pub struct MistDropdown {
pub open: bool,
pub selected: usize,
}
#[derive(Component, Clone, Debug)]
pub struct MistDropdownOptions(pub Vec<String>);
#[derive(Component, Clone, Copy, Debug)]
pub struct MistDropdownParts {
label_text: Entity,
menu: Entity,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistDropdownOwner(pub Entity);
#[derive(Component)]
pub struct MistDropdownTrigger;
#[derive(Component)]
pub struct MistDropdownItem {
pub index: usize,
}
#[derive(Component, Clone, Debug)]
pub struct MistTabs {
pub selected: usize,
}
#[derive(Component, Clone, Debug)]
pub struct MistTabLabels(pub Vec<String>);
#[derive(Component, Clone, Copy, Debug)]
pub struct MistTabOwner(pub Entity);
#[derive(Component, Clone, Copy, Debug)]
pub struct MistTabButton {
pub index: usize,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistTabParts {
label: Entity,
}
#[derive(Component, Clone, Debug)]
pub struct MistDialog {
pub open: bool,
pub dismiss_on_backdrop: bool,
}
impl Default for MistDialog {
fn default() -> Self {
Self {
open: false,
dismiss_on_backdrop: true,
}
}
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistDialogParts {
pub backdrop: Entity,
pub panel: Entity,
pub close_button: Entity,
pub title: Entity,
pub body: Entity,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct MistDialogOwner(pub Entity);
#[derive(Component)]
pub struct MistDialogBackdrop;
#[derive(Component)]
pub struct MistDialogCloseButton;
#[derive(Event, Message, Clone, Debug)]
pub struct MistButtonPressed {
pub entity: Entity,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistTriggerPressed {
pub entity: Entity,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistCheckboxChanged {
pub entity: Entity,
pub checked: bool,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistRadioChanged {
pub entity: Entity,
pub selected: usize,
pub label: String,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistSwitchChanged {
pub entity: Entity,
pub on: bool,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistSliderChanged {
pub entity: Entity,
pub value: f32,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistInputChanged {
pub entity: Entity,
pub value: String,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistInputSubmitted {
pub entity: Entity,
pub value: String,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistDropdownChanged {
pub entity: Entity,
pub selected: usize,
pub label: String,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistTabsChanged {
pub entity: Entity,
pub selected: usize,
pub label: String,
}
#[derive(Event, Message, Clone, Debug)]
pub struct MistDialogDismissed {
pub entity: Entity,
}
pub struct MistUiPlugin;
impl Plugin for MistUiPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<MistInputFocusState>()
.add_message::<MistButtonPressed>()
.add_message::<MistTriggerPressed>()
.add_message::<MistCheckboxChanged>()
.add_message::<MistRadioChanged>()
.add_message::<MistSwitchChanged>()
.add_message::<MistSliderChanged>()
.add_message::<MistInputChanged>()
.add_message::<MistInputSubmitted>()
.add_message::<MistDropdownChanged>()
.add_message::<MistTabsChanged>()
.add_message::<MistDialogDismissed>()
.add_systems(
Update,
(
ensure_smoke_runtime_components,
sync_interactive_styles,
sync_interactive_smoke_borders,
emit_button_pressed,
emit_trigger_pressed,
(toggle_checkboxes, sync_checkbox_visuals).chain(),
(select_radio_items, sync_radio_visuals).chain(),
(toggle_switches, sync_switch_visuals).chain(),
scroll_mist_views,
(drive_sliders, sync_slider_visuals).chain(),
animate_progress_bars,
(
focus_input_fields,
type_into_input_fields,
sync_input_fields,
)
.chain(),
sync_tooltips,
(
toggle_dropdowns,
select_dropdown_items,
close_dropdowns_on_outside_click,
sync_dropdowns,
)
.chain(),
(select_tabs, sync_tabs).chain(),
(dismiss_dialogs, dismiss_dialogs_on_escape, sync_dialogs).chain(),
),
);
}
}
fn ensure_smoke_runtime_components(
mut commands: Commands,
query: Query<
(
Entity,
&MistSmokeRole,
Option<&MistSmokeConfig>,
Option<&MistSmokeTarget>,
Option<&SmokeBorder>,
),
With<MistSmokeRole>,
>,
) {
for (entity, role, config, target, border) in &query {
let default_config = smoke_config_for_role(*role, false);
let mut entity_commands = commands.entity(entity);
if config.is_none() {
entity_commands.insert(default_config);
}
if target.is_none() {
entity_commands.insert(MistSmokeTarget::screen_ui());
}
if border.is_none() {
entity_commands.insert(derived_screen_ring(default_config));
}
}
}
pub fn mist_panel() -> impl Bundle {
(
MistPanel,
MistSmokeRole::PanelFrame,
surface_smoke_for_widget_role(MistSmokeRole::PanelFrame, false),
smoke_border_for_role(MistSmokeRole::PanelFrame, false, 21),
smoke_padding_for_role(MistSmokeRole::PanelFrame),
Node {
padding: UiRect::all(Val::Px(16.0)),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(10.0),
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(18.0)),
..default()
},
BackgroundColor(panel_fill()),
BorderColor::all(Color::NONE),
)
}
pub fn mist_label(font: &Handle<Font>, value: &str, size: f32) -> impl Bundle {
(MistLabel, text_block(font, value, size, text_primary()))
}
pub fn mist_image(image: Handle<Image>, size: Vec2) -> impl Bundle {
(
MistImage,
ImageNode::new(image),
MistSmokeRole::PanelFrame,
smoke_border_for_role(MistSmokeRole::PanelFrame, false, 31),
smoke_padding_for_role(MistSmokeRole::PanelFrame),
Node {
width: Val::Px(size.x),
height: Val::Px(size.y),
..default()
},
)
}
pub fn spawn_mist_button(
commands: &mut Commands,
font: &Handle<Font>,
label: impl Into<String>,
width: f32,
) -> Entity {
let label = label.into();
commands
.spawn((
MistButton,
Button,
MistSmokeRole::StandardButton,
surface_smoke_for_widget_role(MistSmokeRole::StandardButton, false),
MistInteractiveStyle::default(),
smoke_border_for_role(MistSmokeRole::StandardButton, false, 1),
smoke_padding_for_role(MistSmokeRole::StandardButton),
Node {
width: Val::Px(width),
min_height: Val::Px(CONTROL_HEIGHT),
padding: UiRect::axes(Val::Px(CONTROL_PADDING_X), Val::Px(CONTROL_PADDING_Y)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(CONTROL_RADIUS)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
children![text_line(font, &label, 20.0, text_primary())],
))
.id()
}
pub fn spawn_mist_trigger(
commands: &mut Commands,
font: &Handle<Font>,
label: impl Into<String>,
width: f32,
) -> Entity {
let label = label.into();
commands
.spawn((
MistTrigger,
Button,
MistSmokeRole::ToolbarButton,
surface_smoke_for_widget_role(MistSmokeRole::ToolbarButton, false),
MistInteractiveStyle::default(),
smoke_border_for_role(MistSmokeRole::ToolbarButton, false, 2),
smoke_padding_for_role(MistSmokeRole::ToolbarButton),
Node {
width: Val::Px(width),
min_height: Val::Px(CONTROL_HEIGHT),
padding: UiRect::axes(Val::Px(CONTROL_PADDING_X), Val::Px(CONTROL_PADDING_Y)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(CONTROL_RADIUS)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
children![text_line(font, &label, 20.0, text_primary())],
))
.id()
}
pub fn spawn_mist_checkbox(
commands: &mut Commands,
font: &Handle<Font>,
label: impl Into<String>,
checked: bool,
) -> Entity {
let label = label.into();
let root = commands
.spawn((
MistCheckbox,
MistCheckboxState { checked },
Button,
MistSmokeRole::StandardButton,
surface_smoke_for_widget_role(MistSmokeRole::StandardButton, false),
MistInteractiveStyle::default(),
smoke_border_for_role(MistSmokeRole::StandardButton, false, 3),
smoke_padding_for_role(MistSmokeRole::StandardButton),
Node {
min_width: Val::Px(220.0),
min_height: Val::Px(CONTROL_HEIGHT),
padding: UiRect::axes(Val::Px(CONTROL_PADDING_X), Val::Px(CONTROL_PADDING_Y)),
column_gap: Val::Px(12.0),
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(CONTROL_RADIUS)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
))
.id();
let indicator = commands
.spawn((
Node {
width: Val::Px(22.0),
height: Val::Px(22.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(6.0)),
..default()
},
surface_smoke_for_role(MistSmokeSurfaceRole::AccentChip, checked),
BackgroundColor(Color::NONE),
BorderColor::all(Color::NONE),
))
.id();
let glyph = commands
.spawn(text_line(
font,
if checked { "✓" } else { "" },
17.0,
Color::srgba(0.03, 0.06, 0.10, 1.0),
))
.id();
let label_text = commands
.spawn(text_line(font, &label, 18.0, text_primary()))
.id();
commands.entity(indicator).add_child(glyph);
commands.entity(root).add_children(&[indicator, label_text]);
commands
.entity(root)
.insert(MistCheckboxParts { indicator, glyph });
root
}
pub fn spawn_mist_radio_group(
commands: &mut Commands,
font: &Handle<Font>,
width: f32,
options: impl IntoIterator<Item = impl Into<String>>,
selected: usize,
) -> Entity {
let options: Vec<String> = options.into_iter().map(Into::into).collect();
let selected = clamp_choice_index(options.len(), selected);
let root = commands
.spawn((
MistRadioGroup { selected },
MistRadioOptions(options.clone()),
MistSmokeRole::PanelFrame,
surface_smoke_for_widget_role(MistSmokeRole::PanelFrame, false),
smoke_border_for_role(MistSmokeRole::PanelFrame, false, 9),
smoke_padding_for_role(MistSmokeRole::PanelFrame),
Node {
width: Val::Px(width),
padding: UiRect::all(Val::Px(10.0)),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(8.0),
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(18.0)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
))
.id();
for (index, label) in options.iter().enumerate() {
let item = commands
.spawn((
Button,
MistRadioOwner(root),
MistRadioOption { index },
MistSmokeRole::DropdownOption,
surface_smoke_for_widget_role(MistSmokeRole::DropdownOption, false),
smoke_border_for_role(
MistSmokeRole::DropdownOption,
false,
(root.to_bits() ^ index as u64) + 90,
),
smoke_padding_for_role(MistSmokeRole::DropdownOption),
Node {
width: Val::Percent(100.0),
min_height: Val::Px(38.0),
padding: UiRect::axes(Val::Px(10.0), Val::Px(6.0)),
column_gap: Val::Px(10.0),
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(12.0)),
..default()
},
BackgroundColor(Color::NONE),
BorderColor::all(Color::NONE),
))
.id();
let indicator = commands
.spawn((
Node {
width: Val::Px(20.0),
height: Val::Px(20.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(10.0)),
..default()
},
surface_smoke_for_role(MistSmokeSurfaceRole::AccentChip, false),
BackgroundColor(Color::NONE),
BorderColor::all(Color::NONE),
))
.id();
let glyph = commands
.spawn(text_line(
font,
"",
18.0,
Color::srgba(0.03, 0.06, 0.10, 1.0),
))
.id();
let label_text = commands
.spawn(text_line(font, label, 18.0, text_secondary()))
.id();
commands.entity(label_text).insert(Node {
flex_grow: 1.0,
..default()
});
commands.entity(indicator).add_child(glyph);
commands.entity(item).add_children(&[indicator, label_text]);
commands.entity(item).insert(MistRadioParts {
indicator,
glyph,
label: label_text,
});
commands.entity(root).add_child(item);
}
root
}
pub fn spawn_mist_switch(
commands: &mut Commands,
font: &Handle<Font>,
label: impl Into<String>,
on: bool,
) -> Entity {
let label = label.into();
let root = commands
.spawn((
MistSwitch,
MistSwitchState { on },
Button,
MistSmokeRole::StandardButton,
surface_smoke_for_widget_role(MistSmokeRole::StandardButton, false),
smoke_border_for_role(MistSmokeRole::StandardButton, false, 10),
smoke_padding_for_role(MistSmokeRole::StandardButton),
Node {
min_width: Val::Px(220.0),
min_height: Val::Px(CONTROL_HEIGHT),
padding: UiRect::axes(Val::Px(CONTROL_PADDING_X), Val::Px(CONTROL_PADDING_Y)),
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(CONTROL_RADIUS)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
))
.id();
let label_text = commands
.spawn(text_line(font, &label, 18.0, text_primary()))
.id();
let track = commands
.spawn((
Node {
width: Val::Px(SWITCH_TRACK_WIDTH),
height: Val::Px(SWITCH_TRACK_HEIGHT),
position_type: PositionType::Relative,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(SWITCH_TRACK_HEIGHT * 0.5)),
..default()
},
surface_smoke_for_role(MistSmokeSurfaceRole::ScalarTrack, on),
BackgroundColor(Color::NONE),
BorderColor::all(Color::NONE),
))
.id();
let knob = commands
.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(2.0),
top: Val::Px(2.0),
width: Val::Px(SWITCH_KNOB_SIZE),
height: Val::Px(SWITCH_KNOB_SIZE),
border_radius: BorderRadius::all(Val::Px(SWITCH_KNOB_SIZE * 0.5)),
..default()
},
surface_smoke_for_role(MistSmokeSurfaceRole::AccentOrb, on),
BackgroundColor(Color::NONE),
))
.id();
commands.entity(track).add_child(knob);
commands.entity(root).add_children(&[label_text, track]);
commands
.entity(root)
.insert(MistSwitchParts { track, knob });
root
}
pub fn spawn_mist_scroll_view(
commands: &mut Commands,
width: f32,
height: f32,
) -> (Entity, Entity) {
let root = commands
.spawn((
MistScrollView,
MistSmokeRole::ScrollContainer,
surface_smoke_for_widget_role(MistSmokeRole::ScrollContainer, false),
smoke_border_for_role(MistSmokeRole::ScrollContainer, false, 12),
smoke_padding_for_role(MistSmokeRole::ScrollContainer),
Node {
width: Val::Px(width),
height: Val::Px(height),
padding: UiRect::all(Val::Px(8.0)),
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(18.0)),
..default()
},
BackgroundColor(panel_fill()),
BorderColor::all(Color::NONE),
))
.id();
let viewport = commands
.spawn((
MistScrollViewport,
RelativeCursorPosition::default(),
ScrollPosition::default(),
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
overflow: Overflow::scroll_y(),
padding: UiRect::right(Val::Px(6.0)),
..default()
},
))
.id();
let content = commands
.spawn((
MistScrollContent,
Node {
width: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(8.0),
..default()
},
))
.id();
commands.entity(viewport).add_child(content);
commands.entity(root).add_child(viewport);
commands
.entity(root)
.insert(MistScrollParts { viewport, content });
(root, content)
}
pub fn spawn_mist_slider(commands: &mut Commands, width: f32, value: f32) -> Entity {
let slider = MistSlider::new(0.0, 1.0);
let t = slider.normalize(value);
let root = commands
.spawn((
slider,
MistSliderValue(slider.denormalize(t)),
Button,
RelativeCursorPosition::default(),
MistSmokeRole::ScalarControl,
surface_smoke_for_widget_role(MistSmokeRole::ScalarControl, false),
MistInteractiveStyle::default(),
smoke_border_for_role(MistSmokeRole::ScalarControl, false, 4),
smoke_padding_for_role(MistSmokeRole::ScalarControl),
Node {
width: Val::Px(width),
min_height: Val::Px(28.0),
position_type: PositionType::Relative,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(14.0)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
))
.id();
let fill = commands
.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
bottom: Val::Px(0.0),
width: Val::Percent(t * 100.0),
border_radius: BorderRadius::all(Val::Px(14.0)),
..default()
},
surface_smoke_for_role(MistSmokeSurfaceRole::ScalarFill, t > 0.01),
BackgroundColor(Color::NONE),
))
.id();
let handle = commands
.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(t * (width - SLIDER_HANDLE_WIDTH)),
top: Val::Px(-1.0),
width: Val::Px(SLIDER_HANDLE_WIDTH),
height: Val::Px(28.0),
border_radius: BorderRadius::all(Val::Px(14.0)),
..default()
},
surface_smoke_for_role(MistSmokeSurfaceRole::AccentOrb, true),
BackgroundColor(Color::NONE),
BorderColor::all(Color::NONE),
))
.id();
commands.entity(root).add_children(&[fill, handle]);
commands
.entity(root)
.insert(MistSliderParts { fill, handle });
root
}
pub fn spawn_mist_progress_bar(commands: &mut Commands, width: f32, target: f32) -> Entity {
let target = target.clamp(0.0, 1.0);
let root = commands
.spawn((
MistProgressBar {
displayed: target,
target,
},
MistSmokeRole::ScalarControl,
surface_smoke_for_role(MistSmokeSurfaceRole::ScalarTrack, false),
smoke_border_for_role(MistSmokeRole::ScalarControl, false, 5),
smoke_padding_for_role(MistSmokeRole::ScalarControl),
Node {
width: Val::Px(width),
min_height: Val::Px(24.0),
position_type: PositionType::Relative,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(12.0)),
..default()
},
BackgroundColor(panel_fill()),
BorderColor::all(Color::NONE),
))
.id();
let fill = commands
.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
bottom: Val::Px(0.0),
width: Val::Percent(target * 100.0),
border_radius: BorderRadius::all(Val::Px(12.0)),
..default()
},
surface_smoke_for_role(MistSmokeSurfaceRole::ScalarFill, target > 0.01),
BackgroundColor(Color::NONE),
))
.id();
commands.entity(root).add_child(fill);
commands.entity(root).insert(MistProgressParts { fill });
root
}
pub fn spawn_mist_input_field(
commands: &mut Commands,
font: &Handle<Font>,
width: f32,
config: MistInputField,
) -> Entity {
let has_text = !config.value.is_empty();
let root = commands
.spawn((
Button,
MistSmokeRole::StandardButton,
surface_smoke_for_widget_role(MistSmokeRole::StandardButton, false),
MistInteractiveStyle::default(),
smoke_border_for_role(MistSmokeRole::StandardButton, false, 6),
smoke_padding_for_role(MistSmokeRole::StandardButton),
config,
Node {
width: Val::Px(width),
min_height: Val::Px(CONTROL_HEIGHT),
padding: UiRect::axes(Val::Px(CONTROL_PADDING_X), Val::Px(CONTROL_PADDING_Y)),
column_gap: Val::Px(8.0),
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(CONTROL_RADIUS)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
))
.id();
let placeholder_text = commands
.spawn((
text_line(font, "", 18.0, text_secondary()),
if has_text {
Visibility::Hidden
} else {
Visibility::Inherited
},
))
.id();
let value_text = commands
.spawn(text_line(font, "", 18.0, text_primary()))
.id();
let caret = commands
.spawn((
text_line(font, "▏", 20.0, text_primary()),
Visibility::Hidden,
))
.id();
commands
.entity(root)
.add_children(&[placeholder_text, value_text, caret]);
commands.entity(root).insert(MistInputParts {
value_text,
placeholder_text,
caret,
});
root
}
pub fn attach_mist_tooltip(
commands: &mut Commands,
anchor: Entity,
font: &Handle<Font>,
label: impl Into<String>,
max_width: f32,
) -> Entity {
let label = label.into();
commands
.entity(anchor)
.insert((MistTooltipAnchor, RelativeCursorPosition::default()));
let tooltip = commands
.spawn((
MistTooltip::default(),
MistTooltipOwner(anchor),
MistSmokeRole::PanelFrame,
surface_smoke_for_widget_role(MistSmokeRole::PanelFrame, false),
Node {
position_type: PositionType::Absolute,
left: Val::Px(0.0),
bottom: Val::Percent(100.0),
max_width: Val::Px(max_width),
padding: UiRect::all(Val::Px(12.0)),
margin: UiRect::bottom(Val::Px(8.0)),
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(14.0)),
display: Display::None,
..default()
},
smoke_border_for_role(MistSmokeRole::PanelFrame, false, 13),
smoke_padding_for_role(MistSmokeRole::PanelFrame),
BackgroundColor(panel_fill()),
BorderColor::all(Color::NONE),
GlobalZIndex(80),
Visibility::Hidden,
children![text_block(font, &label, 15.0, text_primary())],
))
.id();
commands.entity(anchor).add_child(tooltip);
tooltip
}
pub fn spawn_mist_tabs(
commands: &mut Commands,
font: &Handle<Font>,
width: f32,
labels: impl IntoIterator<Item = impl Into<String>>,
selected: usize,
) -> Entity {
let labels: Vec<String> = labels.into_iter().map(Into::into).collect();
let selected = clamp_choice_index(labels.len(), selected);
let root = commands
.spawn((
MistTabs { selected },
MistTabLabels(labels.clone()),
MistSmokeRole::PanelFrame,
surface_smoke_for_widget_role(MistSmokeRole::PanelFrame, false),
smoke_border_for_role(MistSmokeRole::PanelFrame, false, 11),
smoke_padding_for_role(MistSmokeRole::PanelFrame),
Node {
width: Val::Px(width),
min_height: Val::Px(CONTROL_HEIGHT),
padding: UiRect::all(Val::Px(6.0)),
column_gap: Val::Px(6.0),
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(18.0)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
))
.id();
for (index, label) in labels.iter().enumerate() {
let button = commands
.spawn((
Button,
MistTabOwner(root),
MistTabButton { index },
MistSmokeRole::DropdownOption,
surface_smoke_for_widget_role(MistSmokeRole::DropdownOption, index == selected),
smoke_border_for_role(
MistSmokeRole::DropdownOption,
false,
(root.to_bits() ^ index as u64) + 110,
),
smoke_padding_for_role(MistSmokeRole::DropdownOption),
Node {
flex_grow: 1.0,
min_height: Val::Px(CONTROL_HEIGHT - 12.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(12.0)),
..default()
},
BackgroundColor(Color::NONE),
BorderColor::all(Color::NONE),
))
.id();
let label_text = commands
.spawn(text_line(font, label, 17.0, text_secondary()))
.id();
commands.entity(button).add_child(label_text);
commands
.entity(button)
.insert(MistTabParts { label: label_text });
commands.entity(root).add_child(button);
}
root
}
pub fn spawn_mist_dialog(
commands: &mut Commands,
font: &Handle<Font>,
title: impl Into<String>,
body: impl Into<String>,
width: f32,
) -> Entity {
let title = title.into();
let body = body.into();
let root = commands
.spawn((
MistDialog::default(),
Node {
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
display: Display::None,
..default()
},
Visibility::Hidden,
GlobalZIndex(120),
))
.id();
let backdrop = commands
.spawn((
MistDialogBackdrop,
MistDialogOwner(root),
Button,
surface_smoke_for_role(MistSmokeSurfaceRole::BackdropVeil, false),
Node {
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
BackgroundColor(Color::NONE),
))
.id();
let panel = commands
.spawn((
MistSmokeRole::DialogFrame,
surface_smoke_for_widget_role(MistSmokeRole::DialogFrame, false),
smoke_border_for_role(MistSmokeRole::DialogFrame, false, 14),
smoke_padding_for_role(MistSmokeRole::DialogFrame),
Node {
width: Val::Px(width),
max_width: Val::Percent(88.0),
padding: UiRect::all(Val::Px(18.0)),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(14.0),
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(22.0)),
..default()
},
BackgroundColor(panel_fill()),
BorderColor::all(Color::NONE),
))
.id();
let header = commands
.spawn((Node {
width: Val::Percent(100.0),
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
column_gap: Val::Px(12.0),
..default()
},))
.id();
let title_text = commands
.spawn(text_line(font, &title, 24.0, text_primary()))
.id();
commands.entity(title_text).insert(Node {
flex_grow: 1.0,
..default()
});
let close_button = commands
.spawn((
MistDialogCloseButton,
MistDialogOwner(root),
Button,
MistSmokeRole::ToolbarButton,
surface_smoke_for_widget_role(MistSmokeRole::ToolbarButton, false),
MistInteractiveStyle::default(),
smoke_border_for_role(MistSmokeRole::ToolbarButton, false, 15),
smoke_padding_for_role(MistSmokeRole::ToolbarButton),
Node {
width: Val::Px(40.0),
min_height: Val::Px(40.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(12.0)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
children![text_line(font, "x", 18.0, text_primary())],
))
.id();
let body_text = commands
.spawn(text_block(font, &body, 17.0, text_secondary()))
.id();
commands
.entity(header)
.add_children(&[title_text, close_button]);
commands.entity(panel).add_children(&[header, body_text]);
commands.entity(root).add_children(&[backdrop, panel]);
commands.entity(root).insert(MistDialogParts {
backdrop,
panel,
close_button,
title: title_text,
body: body_text,
});
root
}
pub fn spawn_mist_dropdown(
commands: &mut Commands,
font: &Handle<Font>,
width: f32,
options: impl IntoIterator<Item = impl Into<String>>,
) -> Entity {
let options: Vec<String> = options.into_iter().map(Into::into).collect();
let root = commands
.spawn((
MistDropdown::default(),
MistDropdownOptions(options.clone()),
Node {
position_type: PositionType::Relative,
width: Val::Px(width),
min_height: Val::Px(CONTROL_HEIGHT),
..default()
},
))
.id();
let trigger = commands
.spawn((
MistTrigger,
MistDropdownTrigger,
MistDropdownOwner(root),
Button,
MistSmokeRole::ToolbarButton,
surface_smoke_for_widget_role(MistSmokeRole::ToolbarButton, false),
MistInteractiveStyle::default(),
smoke_border_for_role(MistSmokeRole::ToolbarButton, false, 7),
smoke_padding_for_role(MistSmokeRole::ToolbarButton),
Node {
width: Val::Percent(100.0),
min_height: Val::Px(CONTROL_HEIGHT),
padding: UiRect::axes(Val::Px(CONTROL_PADDING_X), Val::Px(CONTROL_PADDING_Y)),
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(CONTROL_RADIUS)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
))
.id();
let label_text = commands
.spawn(text_line(font, "", 18.0, text_primary()))
.id();
let chevron = commands
.spawn(text_line(font, "v", 18.0, text_secondary()))
.id();
commands
.entity(trigger)
.add_children(&[label_text, chevron]);
let menu = commands
.spawn((
MistSmokeRole::PanelFrame,
surface_smoke_for_widget_role(MistSmokeRole::PanelFrame, false),
Node {
position_type: PositionType::Absolute,
top: Val::Px(CONTROL_HEIGHT + 4.0),
left: Val::Px(0.0),
width: Val::Percent(100.0),
padding: UiRect::all(Val::Px(8.0)),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(6.0),
display: Display::None,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(16.0)),
..default()
},
smoke_border_for_role(MistSmokeRole::PanelFrame, false, 8),
smoke_padding_for_role(MistSmokeRole::PanelFrame),
BackgroundColor(panel_fill()),
BorderColor::all(Color::NONE),
Visibility::Hidden,
GlobalZIndex(20),
))
.id();
for (index, option) in options.iter().enumerate() {
let item = commands
.spawn((
MistDropdownOwner(root),
MistDropdownItem { index },
Button,
MistSmokeRole::DropdownOption,
surface_smoke_for_widget_role(MistSmokeRole::DropdownOption, false),
MistInteractiveStyle::default(),
smoke_border_for_role(
MistSmokeRole::DropdownOption,
false,
(root.to_bits() ^ index as u64) + 70,
),
smoke_padding_for_role(MistSmokeRole::DropdownOption),
Node {
width: Val::Percent(100.0),
min_height: Val::Px(34.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(0.0)),
border_radius: BorderRadius::all(Val::Px(10.0)),
..default()
},
BackgroundColor(control_fill()),
BorderColor::all(Color::NONE),
children![text_line(font, option, 17.0, text_primary())],
))
.id();
commands.entity(menu).add_child(item);
}
commands.entity(root).add_children(&[trigger, menu]);
commands
.entity(root)
.insert(MistDropdownParts { label_text, menu });
root
}
fn sync_interactive_styles(
mut query: Query<
(
&Interaction,
&MistInteractiveStyle,
&mut BackgroundColor,
&mut BorderColor,
),
(Changed<Interaction>, With<Button>),
>,
) {
for (interaction, style, mut fill, mut border) in &mut query {
match *interaction {
Interaction::None => {
fill.0 = style.idle_fill;
*border = BorderColor::all(style.idle_border);
}
Interaction::Hovered => {
fill.0 = style.hover_fill;
*border = BorderColor::all(style.hover_border);
}
Interaction::Pressed => {
fill.0 = style.pressed_fill;
*border = BorderColor::all(style.pressed_border);
}
}
}
}
fn sync_interactive_smoke_borders(
mut query: Query<
(
Entity,
&Interaction,
&MistSmokeRole,
&mut SmokeBorder,
Option<&mut MistSmokeConfig>,
Option<&mut MistSmokeSurface>,
),
(Changed<Interaction>, With<Button>),
>,
) {
for (entity, interaction, role, mut smoke, config, surface) in &mut query {
let pressed = matches!(*interaction, Interaction::Pressed);
let next_config = smoke_config_for_role(*role, pressed);
*smoke = derived_screen_ring(next_config);
if let Some(mut config) = config {
*config = next_config;
} else {
*smoke = smoke_border_for_role(*role, pressed, entity.to_bits());
}
if let Some(mut surface) = surface {
*surface = surface_smoke_for_widget_role(*role, pressed);
}
}
}
fn emit_button_pressed(
query: Query<(Entity, &Interaction), (Changed<Interaction>, With<MistButton>)>,
mut events: MessageWriter<MistButtonPressed>,
) {
for (entity, interaction) in &query {
if *interaction == Interaction::Pressed {
events.write(MistButtonPressed { entity });
}
}
}
fn emit_trigger_pressed(
query: Query<(Entity, &Interaction), (Changed<Interaction>, With<MistTrigger>)>,
mut events: MessageWriter<MistTriggerPressed>,
) {
for (entity, interaction) in &query {
if *interaction == Interaction::Pressed {
events.write(MistTriggerPressed { entity });
}
}
}
fn toggle_checkboxes(
mut query: Query<
(Entity, &Interaction, &mut MistCheckboxState),
(Changed<Interaction>, With<MistCheckbox>),
>,
mut events: MessageWriter<MistCheckboxChanged>,
) {
for (entity, interaction, mut state) in &mut query {
if *interaction != Interaction::Pressed {
continue;
}
state.checked = !state.checked;
events.write(MistCheckboxChanged {
entity,
checked: state.checked,
});
}
}
fn sync_checkbox_visuals(
query: Query<
(&MistCheckboxState, &MistCheckboxParts),
Or<(Changed<MistCheckboxState>, Added<MistCheckboxState>)>,
>,
mut surfaces: Query<&mut MistSmokeSurface>,
mut text_query: Query<&mut Text>,
) {
for (state, parts) in &query {
if let Ok(mut surface) = surfaces.get_mut(parts.indicator) {
*surface = surface_smoke_for_role(MistSmokeSurfaceRole::AccentChip, state.checked);
}
if let Ok(mut text) = text_query.get_mut(parts.glyph) {
text.clear();
text.push_str(if state.checked { "✓" } else { "" });
}
}
}
fn scroll_mist_views(
mut wheel_events: MessageReader<MouseWheel>,
roots: Query<&MistScrollParts, With<MistScrollView>>,
mut viewports: Query<
(&RelativeCursorPosition, &ComputedNode, &mut ScrollPosition),
With<MistScrollViewport>,
>,
content_nodes: Query<&ComputedNode, With<MistScrollContent>>,
) {
let mut delta_pixels = 0.0;
for event in wheel_events.read() {
delta_pixels += match event.unit {
MouseScrollUnit::Line => event.y * SCROLL_STEP_PX,
MouseScrollUnit::Pixel => event.y,
};
}
if delta_pixels.abs() <= f32::EPSILON {
return;
}
for parts in &roots {
let Ok((cursor, viewport_node, mut scroll_position)) = viewports.get_mut(parts.viewport)
else {
continue;
};
if cursor.normalized.is_none() {
continue;
}
let Ok(content_node) = content_nodes.get(parts.content) else {
continue;
};
let max_offset = (content_node.size().y - viewport_node.size().y).max(0.0);
scroll_position.0.y = (scroll_position.0.y - delta_pixels).clamp(0.0, max_offset);
}
}
fn sync_tooltips(
anchors: Query<&RelativeCursorPosition, With<MistTooltipAnchor>>,
mut tooltips: Query<(&MistTooltip, &MistTooltipOwner, &mut Visibility, &mut Node)>,
) {
for (tooltip, owner, mut visibility, mut node) in &mut tooltips {
let hovered = anchors
.get(owner.0)
.ok()
.and_then(|cursor| cursor.normalized)
.is_some();
let visible = tooltip.enabled && hovered;
*visibility = if visible {
Visibility::Inherited
} else {
Visibility::Hidden
};
node.display = if visible {
Display::Flex
} else {
Display::None
};
}
}
fn select_radio_items(
mut query: Query<
(&Interaction, &MistRadioOption, &MistRadioOwner),
(Changed<Interaction>, With<Button>),
>,
mut groups: Query<(&MistRadioOptions, &mut MistRadioGroup)>,
mut events: MessageWriter<MistRadioChanged>,
) {
for (interaction, option, owner) in &mut query {
if *interaction != Interaction::Pressed {
continue;
}
if let Ok((options, mut group)) = groups.get_mut(owner.0) {
group.selected = clamp_choice_index(options.0.len(), option.index);
let label = options.0.get(group.selected).cloned().unwrap_or_default();
events.write(MistRadioChanged {
entity: owner.0,
selected: group.selected,
label,
});
}
}
}
fn sync_radio_visuals(
groups: Query<&MistRadioGroup>,
items: Query<
(
Entity,
&MistRadioOwner,
&MistRadioOption,
&MistRadioParts,
&Interaction,
),
With<Button>,
>,
mut backgrounds: Query<&mut BackgroundColor>,
mut borders: Query<&mut BorderColor>,
mut texts: Query<&mut Text>,
mut text_colors: Query<&mut TextColor>,
mut surfaces: Query<&mut MistSmokeSurface>,
) {
for (entity, owner, option, parts, interaction) in &items {
let Ok(group) = groups.get(owner.0) else {
continue;
};
let selected = group.selected == option.index;
if let Ok(mut background) = backgrounds.get_mut(entity) {
background.0 = Color::NONE;
}
if let Ok(mut border) = borders.get_mut(entity) {
*border = BorderColor::all(Color::NONE);
}
if let Ok(mut item_surface) = surfaces.get_mut(entity) {
*item_surface = surface_smoke_for_role(
MistSmokeSurfaceRole::OptionBody,
selected || *interaction != Interaction::None,
);
}
if let Ok(mut indicator_surface) = surfaces.get_mut(parts.indicator) {
*indicator_surface = surface_smoke_for_role(MistSmokeSurfaceRole::AccentChip, selected);
}
if let Ok(mut glyph_text) = texts.get_mut(parts.glyph) {
glyph_text.clear();
glyph_text.push_str(if selected { "•" } else { "" });
}
if let Ok(mut label_color) = text_colors.get_mut(parts.label) {
label_color.0 = if selected {
text_primary()
} else {
text_secondary()
};
}
}
}
fn toggle_switches(
mut query: Query<
(Entity, &Interaction, &mut MistSwitchState),
(Changed<Interaction>, With<MistSwitch>),
>,
mut events: MessageWriter<MistSwitchChanged>,
) {
for (entity, interaction, mut state) in &mut query {
if *interaction != Interaction::Pressed {
continue;
}
state.on = !state.on;
events.write(MistSwitchChanged {
entity,
on: state.on,
});
}
}
fn sync_switch_visuals(
query: Query<(Entity, &MistSwitchState, &MistSwitchParts, &Interaction), With<MistSwitch>>,
mut backgrounds: Query<&mut BackgroundColor>,
mut borders: Query<&mut BorderColor>,
mut nodes: Query<&mut Node>,
mut surfaces: Query<&mut MistSmokeSurface>,
) {
for (entity, state, parts, interaction) in &query {
if let Ok(mut background) = backgrounds.get_mut(entity) {
background.0 = Color::NONE;
}
if let Ok(mut border) = borders.get_mut(entity) {
*border = BorderColor::all(Color::NONE);
}
if let Ok(mut track_fill) = backgrounds.get_mut(parts.track) {
track_fill.0 = Color::NONE;
}
if let Ok(mut track_border) = borders.get_mut(parts.track) {
*track_border = BorderColor::all(Color::NONE);
}
if let Ok(mut track_surface) = surfaces.get_mut(parts.track) {
*track_surface = surface_smoke_for_role(
MistSmokeSurfaceRole::ScalarTrack,
state.on || *interaction != Interaction::None,
);
}
if let Ok(mut knob_surface) = surfaces.get_mut(parts.knob) {
*knob_surface = surface_smoke_for_role(
MistSmokeSurfaceRole::AccentOrb,
state.on || *interaction != Interaction::None,
);
}
if let Ok(mut knob_node) = nodes.get_mut(parts.knob) {
let left = if state.on {
SWITCH_TRACK_WIDTH - SWITCH_KNOB_SIZE - 3.0
} else {
2.0
};
knob_node.left = Val::Px(left);
}
}
}
fn drive_sliders(
mouse: Res<ButtonInput<MouseButton>>,
mut query: Query<
(
Entity,
&Interaction,
&RelativeCursorPosition,
&MistSlider,
&mut MistSliderValue,
),
With<Button>,
>,
mut events: MessageWriter<MistSliderChanged>,
) {
if !mouse.pressed(MouseButton::Left) {
return;
}
for (entity, interaction, cursor, slider, mut value) in &mut query {
if *interaction == Interaction::None {
continue;
}
let Some(normalized) = cursor.normalized else {
continue;
};
let next = slider.denormalize(normalized.x);
if (next - value.0).abs() <= f32::EPSILON {
continue;
}
value.0 = next;
events.write(MistSliderChanged {
entity,
value: next,
});
}
}
fn sync_slider_visuals(
query: Query<
(
&MistSlider,
&MistSliderValue,
&MistSliderParts,
&ComputedNode,
),
Or<(Changed<MistSliderValue>, Added<MistSliderValue>)>,
>,
mut nodes: Query<&mut Node>,
mut surfaces: Query<&mut MistSmokeSurface>,
) {
for (slider, value, parts, node) in &query {
let normalized = slider.normalize(value.0);
if let Ok(mut fill) = nodes.get_mut(parts.fill) {
fill.width = Val::Percent(normalized * 100.0);
}
if let Ok(mut fill_surface) = surfaces.get_mut(parts.fill) {
*fill_surface =
surface_smoke_for_role(MistSmokeSurfaceRole::ScalarFill, normalized > 0.01);
}
if let Ok(mut handle_surface) = surfaces.get_mut(parts.handle) {
*handle_surface = surface_smoke_for_role(MistSmokeSurfaceRole::AccentOrb, true);
}
if let Ok(mut handle) = nodes.get_mut(parts.handle) {
let left = (node.size().x - SLIDER_HANDLE_WIDTH).max(0.0) * normalized;
handle.left = Val::Px(left);
}
}
}
fn animate_progress_bars(
time: Res<Time>,
mut query: Query<(&mut MistProgressBar, &MistProgressParts)>,
mut nodes: Query<&mut Node>,
mut surfaces: Query<&mut MistSmokeSurface>,
) {
let delta = time.delta_secs().clamp(0.0, 0.25);
let lerp_factor = 1.0 - (-delta * PROGRESS_LERP_RATE).exp();
for (mut bar, parts) in &mut query {
let target = bar.target.clamp(0.0, 1.0);
bar.displayed += (target - bar.displayed) * lerp_factor;
if (target - bar.displayed).abs() < 0.001 {
bar.displayed = target;
}
if let Ok(mut node) = nodes.get_mut(parts.fill) {
node.width = Val::Percent(bar.displayed.clamp(0.0, 1.0) * 100.0);
}
if let Ok(mut fill_surface) = surfaces.get_mut(parts.fill) {
*fill_surface =
surface_smoke_for_role(MistSmokeSurfaceRole::ScalarFill, bar.displayed > 0.01);
}
}
}
fn focus_input_fields(
mouse: Res<ButtonInput<MouseButton>>,
mut commands: Commands,
mut focus: ResMut<MistInputFocusState>,
query: Query<(Entity, &Interaction), (Changed<Interaction>, With<MistInputField>)>,
) {
let mut pressed_input = None;
for (entity, interaction) in &query {
if *interaction == Interaction::Pressed {
pressed_input = Some(entity);
}
}
if let Some(entity) = pressed_input {
if focus.active != Some(entity) {
if let Some(previous) = focus.active.take() {
commands.entity(previous).remove::<MistInputFocused>();
}
commands.entity(entity).insert(MistInputFocused);
focus.active = Some(entity);
}
return;
}
if mouse.just_pressed(MouseButton::Left) {
if let Some(previous) = focus.active.take() {
commands.entity(previous).remove::<MistInputFocused>();
}
}
}
fn type_into_input_fields(
mut commands: Commands,
mut key_evr: MessageReader<KeyboardInput>,
mut focus: ResMut<MistInputFocusState>,
mut query: Query<(Entity, &mut MistInputField), With<MistInputFocused>>,
mut changed: MessageWriter<MistInputChanged>,
mut submitted: MessageWriter<MistInputSubmitted>,
) {
let Some(active) = focus.active else {
return;
};
let Ok((entity, mut input)) = query.get_mut(active) else {
focus.active = None;
return;
};
for ev in key_evr.read() {
if ev.state != ButtonState::Pressed {
continue;
}
let mut input_changed = false;
if ev.key_code == KeyCode::Escape || matches!(ev.logical_key, Key::Escape) {
commands.entity(entity).remove::<MistInputFocused>();
focus.active = None;
break;
} else if ev.key_code == KeyCode::Enter || matches!(ev.logical_key, Key::Enter) {
submitted.write(MistInputSubmitted {
entity,
value: input.value.clone(),
});
continue;
} else if ev.key_code == KeyCode::Backspace || matches!(ev.logical_key, Key::Backspace) {
if input.value.pop().is_some() {
input_changed = true;
}
} else if let Key::Character(ch) = &ev.logical_key {
let value = ch.as_str();
if !value.is_empty() && !value.chars().all(char::is_control) {
let next_len = input.value.chars().count() + value.chars().count();
if input.max_chars.is_none_or(|max| next_len <= max) {
input.value.push_str(value);
input_changed = true;
}
}
}
if input_changed {
changed.write(MistInputChanged {
entity,
value: input.value.clone(),
});
}
}
}
fn sync_input_fields(
focused: Query<(), With<MistInputFocused>>,
query: Query<(&MistInputField, &MistInputParts, Entity)>,
mut texts: Query<&mut Text>,
mut visibility: Query<&mut Visibility>,
) {
for (input, parts, entity) in &query {
if let Ok(mut value_text) = texts.get_mut(parts.value_text) {
value_text.clear();
value_text.push_str(&input.value);
}
if let Ok(mut placeholder_text) = texts.get_mut(parts.placeholder_text) {
placeholder_text.clear();
placeholder_text.push_str(&input.placeholder);
}
if let Ok(mut placeholder_visibility) = visibility.get_mut(parts.placeholder_text) {
*placeholder_visibility = if input.value.is_empty() {
Visibility::Inherited
} else {
Visibility::Hidden
};
}
if let Ok(mut caret_visibility) = visibility.get_mut(parts.caret) {
*caret_visibility = if focused.contains(entity) {
Visibility::Inherited
} else {
Visibility::Hidden
};
}
}
}
fn toggle_dropdowns(
mut query: Query<
(&Interaction, &MistDropdownOwner),
(Changed<Interaction>, With<MistDropdownTrigger>),
>,
mut dropdowns: Query<&mut MistDropdown>,
) {
for (interaction, owner) in &mut query {
if *interaction != Interaction::Pressed {
continue;
}
if let Ok(mut dropdown) = dropdowns.get_mut(owner.0) {
dropdown.open = !dropdown.open;
}
}
}
fn select_dropdown_items(
mut items: Query<
(&Interaction, &MistDropdownItem, &MistDropdownOwner),
(Changed<Interaction>, With<Button>),
>,
mut dropdowns: Query<(&MistDropdownOptions, &mut MistDropdown)>,
mut events: MessageWriter<MistDropdownChanged>,
) {
for (interaction, item, owner) in &mut items {
if *interaction != Interaction::Pressed {
continue;
}
if let Ok((options, mut dropdown)) = dropdowns.get_mut(owner.0) {
dropdown.selected = item.index.min(options.0.len().saturating_sub(1));
dropdown.open = false;
let label = options
.0
.get(dropdown.selected)
.cloned()
.unwrap_or_default();
events.write(MistDropdownChanged {
entity: owner.0,
selected: dropdown.selected,
label,
});
}
}
}
fn close_dropdowns_on_outside_click(
mouse: Res<ButtonInput<MouseButton>>,
mut dropdowns: Query<(Entity, &mut MistDropdown)>,
related_buttons: Query<
(&MistDropdownOwner, &Interaction),
Or<(With<MistDropdownTrigger>, With<MistDropdownItem>)>,
>,
) {
if !mouse.just_pressed(MouseButton::Left) {
return;
}
for (entity, mut dropdown) in &mut dropdowns {
if !dropdown.open {
continue;
}
let interacting = related_buttons
.iter()
.any(|(owner, interaction)| owner.0 == entity && *interaction != Interaction::None);
if !interacting {
dropdown.open = false;
}
}
}
fn sync_dropdowns(
query: Query<
(&MistDropdown, &MistDropdownOptions, &MistDropdownParts),
Or<(Changed<MistDropdown>, Added<MistDropdown>)>,
>,
mut texts: Query<&mut Text>,
mut nodes: Query<&mut Node>,
mut visibility: Query<&mut Visibility>,
) {
for (dropdown, options, parts) in &query {
let label = options
.0
.get(dropdown.selected)
.map(String::as_str)
.unwrap_or("");
if let Ok(mut text) = texts.get_mut(parts.label_text) {
text.clear();
text.push_str(label);
}
if let Ok(mut node) = nodes.get_mut(parts.menu) {
node.display = if dropdown.open {
Display::Flex
} else {
Display::None
};
}
if let Ok(mut menu_visibility) = visibility.get_mut(parts.menu) {
*menu_visibility = if dropdown.open {
Visibility::Inherited
} else {
Visibility::Hidden
};
}
}
}
fn select_tabs(
mut query: Query<
(&Interaction, &MistTabButton, &MistTabOwner),
(Changed<Interaction>, With<Button>),
>,
mut tabs_query: Query<(&MistTabLabels, &mut MistTabs)>,
mut events: MessageWriter<MistTabsChanged>,
) {
for (interaction, button, owner) in &mut query {
if *interaction != Interaction::Pressed {
continue;
}
if let Ok((labels, mut tabs)) = tabs_query.get_mut(owner.0) {
tabs.selected = clamp_choice_index(labels.0.len(), button.index);
let label = labels.0.get(tabs.selected).cloned().unwrap_or_default();
events.write(MistTabsChanged {
entity: owner.0,
selected: tabs.selected,
label,
});
}
}
}
fn sync_tabs(
tabs_query: Query<&MistTabs>,
buttons: Query<
(
Entity,
&MistTabOwner,
&MistTabButton,
&MistTabParts,
&Interaction,
),
With<Button>,
>,
mut backgrounds: Query<&mut BackgroundColor>,
mut borders: Query<&mut BorderColor>,
mut text_colors: Query<&mut TextColor>,
mut surfaces: Query<&mut MistSmokeSurface>,
) {
for (entity, owner, button, parts, interaction) in &buttons {
let Ok(tabs) = tabs_query.get(owner.0) else {
continue;
};
let selected = tabs.selected == button.index;
if let Ok(mut background) = backgrounds.get_mut(entity) {
background.0 = Color::NONE;
}
if let Ok(mut border) = borders.get_mut(entity) {
*border = BorderColor::all(Color::NONE);
}
if let Ok(mut surface) = surfaces.get_mut(entity) {
*surface = surface_smoke_for_role(
MistSmokeSurfaceRole::OptionBody,
selected || *interaction != Interaction::None,
);
}
if let Ok(mut text_color) = text_colors.get_mut(parts.label) {
text_color.0 = if selected {
text_primary()
} else {
text_secondary()
};
}
}
}
fn dismiss_dialogs(
mut controls: Query<
(&Interaction, Option<&MistDialogBackdrop>, &MistDialogOwner),
(
Changed<Interaction>,
Or<(With<MistDialogBackdrop>, With<MistDialogCloseButton>)>,
),
>,
mut dialogs: Query<&mut MistDialog>,
mut events: MessageWriter<MistDialogDismissed>,
) {
for (interaction, backdrop, owner) in &mut controls {
if *interaction != Interaction::Pressed {
continue;
}
let Ok(mut dialog) = dialogs.get_mut(owner.0) else {
continue;
};
if backdrop.is_some() && !dialog.dismiss_on_backdrop {
continue;
}
if dialog.open {
dialog.open = false;
events.write(MistDialogDismissed { entity: owner.0 });
}
}
}
fn dismiss_dialogs_on_escape(
keys: Res<ButtonInput<KeyCode>>,
mut dialogs: Query<(Entity, &mut MistDialog)>,
mut events: MessageWriter<MistDialogDismissed>,
) {
if !keys.just_pressed(KeyCode::Escape) {
return;
}
for (entity, mut dialog) in &mut dialogs {
if dialog.open {
dialog.open = false;
events.write(MistDialogDismissed { entity });
}
}
}
fn sync_dialogs(
query: Query<(&MistDialog, Entity), Or<(Changed<MistDialog>, Added<MistDialog>)>>,
mut visibility: Query<&mut Visibility>,
mut nodes: Query<&mut Node>,
) {
for (dialog, entity) in &query {
if let Ok(mut dialog_visibility) = visibility.get_mut(entity) {
*dialog_visibility = if dialog.open {
Visibility::Inherited
} else {
Visibility::Hidden
};
}
if let Ok(mut dialog_node) = nodes.get_mut(entity) {
dialog_node.display = if dialog.open {
Display::Flex
} else {
Display::None
};
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mist_ui_plugin_registers_messages_and_focus_state() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_plugins(AssetPlugin::default());
app.add_plugins(MistUiPlugin);
assert!(app.world().contains_resource::<MistInputFocusState>());
assert!(app
.world()
.contains_resource::<Messages<MistButtonPressed>>());
assert!(app
.world()
.contains_resource::<Messages<MistRadioChanged>>());
assert!(app
.world()
.contains_resource::<Messages<MistSwitchChanged>>());
assert!(app
.world()
.contains_resource::<Messages<MistDropdownChanged>>());
assert!(app.world().contains_resource::<Messages<MistTabsChanged>>());
assert!(app
.world()
.contains_resource::<Messages<MistDialogDismissed>>());
}
#[test]
fn slider_normalization_is_stable() {
let slider = MistSlider::new(-1.0, 3.0);
assert!((slider.normalize(1.0) - 0.5).abs() < 1e-5);
assert!((slider.denormalize(0.5) - 1.0).abs() < 1e-5);
}
#[test]
fn clamp_choice_index_handles_empty_groups() {
assert_eq!(clamp_choice_index(0, 3), 0);
assert_eq!(clamp_choice_index(3, 9), 2);
}
#[test]
fn pressed_smoke_configs_are_denser_than_idle() {
let idle = smoke_config_for_role(MistSmokeRole::ToolbarButton, false);
let pressed = smoke_config_for_role(MistSmokeRole::ToolbarButton, true);
assert!(pressed.thickness >= idle.thickness);
assert!(pressed.intensity >= idle.intensity);
assert!(pressed.particle_density >= idle.particle_density);
assert!(pressed.particle_size_scale >= idle.particle_size_scale);
}
}