use nalgebra_glm::{Vec2, Vec4};
use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::components::*;
use crate::ecs::ui::layout_types::FlowDirection;
use crate::ecs::ui::state::{UiBase, UiHover};
use crate::ecs::ui::types::Anchor;
use crate::ecs::ui::units::{Ab, Rl};
impl<'a> UiTreeBuilder<'a> {
pub fn add_slider(&mut self, min: f32, max: f32, initial: f32) -> freecs::Entity {
self.add_slider_configured(crate::ecs::ui::components::SliderConfig::new(
min, max, initial,
))
}
pub fn add_slider_logarithmic(&mut self, min: f32, max: f32, initial: f32) -> freecs::Entity {
self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(min, max, initial).logarithmic(),
)
}
pub fn add_slider_configured(
&mut self,
config: crate::ecs::ui::components::SliderConfig<'_>,
) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let slider_height = theme.slider_height;
let corner_radius = theme.corner_radius;
let font_size = theme.font_size;
let normalized = if config.logarithmic && config.min > 0.0 && config.max > config.min {
((config.initial / config.min).ln() / (config.max / config.min).ln()).clamp(0.0, 1.0)
} else {
((config.initial - config.min) / (config.max - config.min)).clamp(0.0, 1.0)
};
let text_slot = self.world_mut().resources.text_cache.add_text(format!(
"{}{:.prec$}{}",
config.prefix,
config.initial,
config.suffix,
prec = config.precision
));
let mut fill_entity = freecs::Entity::default();
let track_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, slider_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
fill_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(0.0, 0.0)),
Rl(Vec2::new(normalized * 100.0, 100.0)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::SliderFill)
.without_pointer_events()
.done();
tree.add_node()
.with_text_slot(text_slot, font_size * 0.85)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().ui.set_ui_widget_state(
track_entity,
UiWidgetState::Slider(UiSliderData {
min: config.min,
max: config.max,
value: config.initial,
changed: false,
fill_entity,
text_slot,
logarithmic: config.logarithmic,
precision: config.precision,
prefix: config.prefix.to_string(),
suffix: config.suffix.to_string(),
}),
);
if let Some(interaction) = self
.world_mut()
.ui
.get_ui_node_interaction_mut(track_entity)
{
interaction.accessible_role = Some(AccessibleRole::Slider);
}
self.assign_tab_index(track_entity);
track_entity
}
pub fn add_range_slider(
&mut self,
min: f32,
max: f32,
initial_low: f32,
initial_high: f32,
) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let slider_height = theme.slider_height;
let corner_radius = theme.corner_radius;
let font_size = theme.font_size;
let thumb_size = slider_height;
let range = max - min;
let low_normalized = if range.abs() > f32::EPSILON {
((initial_low - min) / range).clamp(0.0, 1.0)
} else {
0.0
};
let high_normalized = if range.abs() > f32::EPSILON {
((initial_high - min) / range).clamp(0.0, 1.0)
} else {
1.0
};
let text_slot = self
.world_mut()
.resources
.text_cache
.add_text(format!("{:.1} - {:.1}", initial_low, initial_high));
let mut fill_entity = freecs::Entity::default();
let mut low_thumb_entity = freecs::Entity::default();
let mut high_thumb_entity = freecs::Entity::default();
let track_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, slider_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
fill_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(low_normalized * 100.0, 0.0)),
Rl(Vec2::new(high_normalized * 100.0, 100.0)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::SliderFill)
.without_pointer_events()
.done();
low_thumb_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(low_normalized * 100.0, 0.0))
+ Ab(Vec2::new(-thumb_size * 0.5, 0.0)),
Rl(Vec2::new(low_normalized * 100.0, 100.0))
+ Ab(Vec2::new(thumb_size * 0.5, 0.0)),
)
.with_rect(thumb_size * 0.5, 1.0, Vec4::new(1.0, 1.0, 1.0, 0.3))
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
high_thumb_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(high_normalized * 100.0, 0.0))
+ Ab(Vec2::new(-thumb_size * 0.5, 0.0)),
Rl(Vec2::new(high_normalized * 100.0, 100.0))
+ Ab(Vec2::new(thumb_size * 0.5, 0.0)),
)
.with_rect(thumb_size * 0.5, 1.0, Vec4::new(1.0, 1.0, 1.0, 0.3))
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.with_text_slot(text_slot, font_size * 0.85)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().ui.set_ui_widget_state(
track_entity,
UiWidgetState::RangeSlider(UiRangeSliderData {
min,
max,
low_value: initial_low,
high_value: initial_high,
changed: false,
fill_entity,
low_thumb_entity,
high_thumb_entity,
text_slot,
active_thumb: None,
precision: 1,
thumb_half_size: thumb_size * 0.5,
}),
);
if let Some(interaction) = self
.world_mut()
.ui
.get_ui_node_interaction_mut(track_entity)
{
interaction.accessible_role = Some(AccessibleRole::Slider);
}
self.assign_tab_index(track_entity);
track_entity
}
pub fn add_canvas(&mut self, size: Vec2) -> freecs::Entity {
let canvas_entity = self
.add_node()
.flow_child(Ab(size))
.with_clip()
.without_pointer_events()
.done();
self.world_mut().ui.set_ui_widget_state(
canvas_entity,
UiWidgetState::Canvas(UiCanvasData {
commands: Vec::new(),
hit_test_enabled: false,
command_bounds: Vec::new(),
}),
);
canvas_entity
}
pub fn add_canvas_interactive(&mut self, size: Vec2) -> freecs::Entity {
let canvas_entity = self
.add_node()
.flow_child(Ab(size))
.with_clip()
.with_interaction()
.done();
self.world_mut().ui.set_ui_widget_state(
canvas_entity,
UiWidgetState::Canvas(UiCanvasData {
commands: Vec::new(),
hit_test_enabled: true,
command_bounds: Vec::new(),
}),
);
canvas_entity
}
pub fn add_toggle(&mut self, initial: bool) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let bg_color = if initial {
theme.accent_color
} else {
theme.background_color
};
let toggle_width = theme.toggle_width;
let toggle_height = theme.toggle_height;
let knob_padding = 2.0;
let knob_size = toggle_height - knob_padding * 2.0;
let knob_travel = toggle_width - knob_size - knob_padding * 2.0;
let initial_x = if initial {
knob_padding + knob_travel
} else {
knob_padding
};
let mut knob_entity = freecs::Entity::default();
let toggle_entity = self
.add_node()
.flow_child(Ab(Vec2::new(toggle_width, toggle_height)))
.with_rect(toggle_height / 2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(bg_color)
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
knob_entity = tree
.add_node()
.window(
Ab(Vec2::new(initial_x, knob_padding)),
Ab(Vec2::new(knob_size, knob_size)),
Anchor::TopLeft,
)
.with_rect(knob_size / 2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().ui.set_ui_widget_state(
toggle_entity,
UiWidgetState::Toggle(UiToggleData {
value: initial,
changed: false,
animated_position: if initial { 1.0 } else { 0.0 },
knob_entity,
}),
);
if let Some(interaction) = self
.world_mut()
.ui
.get_ui_node_interaction_mut(toggle_entity)
{
interaction.accessible_role = Some(AccessibleRole::Toggle);
}
self.assign_tab_index(toggle_entity);
toggle_entity
}
pub fn add_checkbox(&mut self, label: &str, initial: bool) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let text_slot = self.world_mut().resources.text_cache.add_text(label);
let mut inner_entity = freecs::Entity::default();
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.8)))
.flow(FlowDirection::Horizontal, 0.0, 8.0)
.entity();
self.push_parent(row_entity);
let box_entity = self
.add_node()
.flow_child(Ab(Vec2::new(20.0, 20.0)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
inner_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(3.0, 3.0)),
Ab(Vec2::new(-3.0, -3.0)) + Rl(Vec2::new(100.0, 100.0)),
)
.with_rect(corner_radius * 0.5, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Accent)
.with_visible(initial)
.without_pointer_events()
.done();
})
.done();
self.add_node()
.flow_child(Ab(Vec2::new(0.0, 20.0)))
.flex_grow(1.0)
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
self.pop_parent();
self.world_mut().ui.set_ui_widget_state(
box_entity,
UiWidgetState::Checkbox(UiCheckboxData {
value: initial,
changed: false,
inner_entity,
}),
);
if let Some(interaction) = self.world_mut().ui.get_ui_node_interaction_mut(box_entity) {
interaction.accessible_role = Some(AccessibleRole::Checkbox);
}
self.assign_tab_index(box_entity);
box_entity
}
pub fn add_radio(&mut self, label: &str, group_id: u32, option_index: usize) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let border_color = theme.border_color;
let text_slot = self.world_mut().resources.text_cache.add_text(label);
let mut inner_entity = freecs::Entity::default();
let selected = option_index == 0;
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.8)))
.flow(FlowDirection::Horizontal, 0.0, 8.0)
.entity();
self.push_parent(row_entity);
let circle_entity = self
.add_node()
.flow_child(Ab(Vec2::new(20.0, 20.0)))
.with_rect(10.0, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
inner_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(5.0, 5.0)),
Ab(Vec2::new(-5.0, -5.0)) + Rl(Vec2::new(100.0, 100.0)),
)
.with_rect(5.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Accent)
.with_visible(selected)
.without_pointer_events()
.done();
})
.done();
self.add_node()
.flow_child(Ab(Vec2::new(0.0, 20.0)))
.flex_grow(1.0)
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
self.pop_parent();
self.world_mut().ui.set_ui_widget_state(
circle_entity,
UiWidgetState::Radio(UiRadioData {
group_id,
option_index,
selected,
changed: false,
inner_entity,
}),
);
if let Some(interaction) = self
.world_mut()
.ui
.get_ui_node_interaction_mut(circle_entity)
{
interaction.accessible_role = Some(AccessibleRole::Button);
}
self.world_mut()
.resources
.retained_ui
.radio_groups
.entry(group_id)
.or_default()
.push(circle_entity);
self.assign_tab_index(circle_entity);
circle_entity
}
pub fn add_progress_bar(&mut self, initial: f32) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let slider_height = theme.slider_height;
let corner_radius = theme.corner_radius;
let normalized = initial.clamp(0.0, 1.0);
let mut fill_entity = freecs::Entity::default();
let bar_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, slider_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.without_pointer_events()
.with_children(|tree| {
fill_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(0.0, 0.0)),
Rl(Vec2::new(normalized * 100.0, 100.0)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Accent)
.without_pointer_events()
.done();
})
.done();
self.world_mut().ui.set_ui_widget_state(
bar_entity,
UiWidgetState::ProgressBar(UiProgressBarData {
value: initial,
fill_entity,
}),
);
self.world_mut().ui.set_ui_node_interaction(
bar_entity,
UiNodeInteraction {
accessible_role: Some(AccessibleRole::ProgressBar),
..UiNodeInteraction::default()
},
);
bar_entity
}
}