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_collapsing_header(&mut self, label: &str, default_open: 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 arrow_text = if default_open { "\u{25BC}" } else { "\u{25B6}" };
let arrow_slot = self.world_mut().resources.text_cache.add_text(arrow_text);
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let root_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flex_grow(0.0)
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
self.push_parent(root_entity);
let header_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 2.0)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 4.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Ab(Vec2::new(16.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.with_text_slot(arrow_slot, font_size)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
let content_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flex_grow(0.0)
.flow(FlowDirection::Vertical, 8.0, 4.0)
.with_visible(default_open)
.entity();
self.pop_parent();
self.world_mut().ui.set_ui_widget_state(
header_entity,
UiWidgetState::CollapsingHeader(UiCollapsingHeaderData {
open: default_open,
changed: false,
content_entity,
arrow_text_slot: arrow_slot,
}),
);
if let Some(interaction) = self
.world_mut()
.ui
.get_ui_node_interaction_mut(header_entity)
{
interaction.accessible_role = Some(AccessibleRole::Button);
}
header_entity
}
pub fn add_scroll_area(&mut self, size: Vec2) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let corner_radius = theme.corner_radius;
let mut content_entity = freecs::Entity::default();
let mut thumb_entity = freecs::Entity::default();
let mut track_entity = freecs::Entity::default();
let root_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, size.y)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_clip()
.with_interaction()
.with_children(|tree| {
content_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(-12.0, 0.0)) + Rl(Vec2::new(100.0, 0.0)),
)
.flow(FlowDirection::Vertical, 4.0, 4.0)
.without_pointer_events()
.entity();
track_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(-10.0, 2.0)) + Rl(Vec2::new(100.0, 0.0)),
Ab(Vec2::new(-2.0, -2.0)) + Rl(Vec2::new(100.0, 100.0)),
)
.with_rect(4.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.2))
.with_visible(false)
.without_pointer_events()
.entity();
tree.push_parent(track_entity);
thumb_entity = tree
.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(8.0, 30.0)),
Anchor::TopLeft,
)
.with_rect(4.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Scrollbar)
.with_theme_color::<UiHover>(ThemeColor::ScrollbarHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.done();
tree.pop_parent();
})
.done();
self.world_mut().ui.set_ui_widget_state(
root_entity,
UiWidgetState::ScrollArea(UiScrollAreaData {
scroll_offset: 0.0,
content_entity,
content_height: 0.0,
visible_height: size.y,
thumb_entity,
track_entity,
thumb_dragging: false,
thumb_drag_start_offset: 0.0,
snap_interval: None,
}),
);
root_entity
}
pub fn add_scroll_area_fill(&mut self, padding: f32, spacing: f32) -> freecs::Entity {
let mut content_entity = freecs::Entity::default();
let mut thumb_entity = freecs::Entity::default();
let mut track_entity = freecs::Entity::default();
let root_entity = self
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_clip()
.with_interaction()
.with_children(|tree| {
content_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(-12.0, 0.0)) + Rl(Vec2::new(100.0, 0.0)),
)
.flow(FlowDirection::Vertical, padding, spacing)
.without_pointer_events()
.entity();
track_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(-10.0, 2.0)) + Rl(Vec2::new(100.0, 0.0)),
Ab(Vec2::new(-2.0, -2.0)) + Rl(Vec2::new(100.0, 100.0)),
)
.with_rect(4.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.2))
.with_visible(false)
.without_pointer_events()
.entity();
tree.push_parent(track_entity);
thumb_entity = tree
.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(8.0, 30.0)),
Anchor::TopLeft,
)
.with_rect(4.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Scrollbar)
.with_theme_color::<UiHover>(ThemeColor::ScrollbarHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.done();
tree.pop_parent();
})
.done();
self.world_mut().ui.set_ui_widget_state(
root_entity,
UiWidgetState::ScrollArea(UiScrollAreaData {
scroll_offset: 0.0,
content_entity,
content_height: 0.0,
visible_height: 0.0,
thumb_entity,
track_entity,
thumb_dragging: false,
thumb_drag_start_offset: 0.0,
snap_interval: None,
}),
);
root_entity
}
pub fn add_virtual_list(&mut self, item_height: f32, pool_size: usize) -> freecs::Entity {
use crate::ecs::ui::components::{UiVirtualListData, VirtualListPoolItem};
let root_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flex_grow(1.0)
.entity();
self.push_parent(root_entity);
let scroll_entity = self.add_scroll_area_fill(0.0, 0.0);
let body_entity = self
.world_mut()
.widget::<UiScrollAreaData>(scroll_entity)
.map(|d| d.content_entity)
.unwrap();
self.push_parent(body_entity);
let top_spacer = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.entity();
let mut pool_items = Vec::with_capacity(pool_size);
for _ in 0..pool_size {
let container = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, item_height)))
.with_interaction()
.with_visible(false)
.entity();
pool_items.push(VirtualListPoolItem {
container_entity: container,
});
}
let bottom_spacer = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.entity();
self.pop_parent();
self.pop_parent();
self.world_mut().ui.set_ui_widget_state(
root_entity,
UiWidgetState::VirtualList(UiVirtualListData {
item_height,
pool_size,
total_items: 0,
visible_start: 0,
scroll_entity,
body_entity,
top_spacer,
bottom_spacer,
pool_items,
selection: None,
selection_changed: false,
}),
);
root_entity
}
pub fn add_tab_bar(&mut self, labels: &[&str], initial: usize) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let active_bg = theme.accent_color;
let inactive_bg = theme.background_color;
let corner_radius = theme.corner_radius;
let tab_height = theme.button_height;
let mut tab_entities = Vec::new();
let mut tab_text_slots = Vec::new();
let bar_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, tab_height)))
.flow(FlowDirection::Horizontal, 0.0, 2.0)
.entity();
self.push_parent(bar_entity);
for (index, label) in labels.iter().enumerate() {
let is_active = index == initial;
let bg = if is_active { active_bg } else { inactive_bg };
let text_slot = self.world_mut().resources.text_cache.add_text(*label);
tab_text_slots.push(text_slot);
let tab_entity = self
.add_node()
.flow_child(Ab(Vec2::new(0.0, tab_height)))
.flex_grow(1.0)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(bg)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.with_text_slot(text_slot, font_size)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
if let Some(interaction) = self.world_mut().ui.get_ui_node_interaction_mut(tab_entity) {
interaction.accessible_role = Some(AccessibleRole::Tab);
}
tab_entities.push(tab_entity);
}
self.pop_parent();
self.world_mut().ui.set_ui_widget_state(
bar_entity,
UiWidgetState::TabBar(UiTabBarData {
selected_tab: initial,
changed: false,
tab_entities,
tab_text_slots,
}),
);
bar_entity
}
pub fn add_breadcrumb(&mut self, segments: &[&str]) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let text_color = theme.text_color;
let accent_color = theme.accent_color;
let separator_color = Vec4::new(
text_color.x * 0.5,
text_color.y * 0.5,
text_color.z * 0.5,
text_color.w,
);
let mut segment_entities = Vec::new();
let mut segment_text_slots = Vec::new();
let bar_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 28.0)))
.flow(FlowDirection::Horizontal, 0.0, 2.0)
.entity();
self.push_parent(bar_entity);
for (index, segment) in segments.iter().enumerate() {
if index > 0 {
let separator_slot = self.world_mut().resources.text_cache.add_text("\u{203A}");
self.add_node()
.with_text_slot(separator_slot, font_size)
.with_color::<UiBase>(separator_color)
.flow_child(Ab(Vec2::new(0.0, 28.0)))
.without_pointer_events()
.done();
}
let text_slot = self.world_mut().resources.text_cache.add_text(*segment);
segment_text_slots.push(text_slot);
let is_last = index == segments.len() - 1;
let color = if is_last { text_color } else { accent_color };
let segment_entity = self
.add_node()
.flow_child(Ab(Vec2::new(0.0, 28.0)))
.with_children(|tree| {
tree.add_node()
.with_text_slot(text_slot, font_size)
.with_color::<UiBase>(color)
.without_pointer_events()
.done();
});
let segment_entity = if is_last {
segment_entity.done()
} else {
segment_entity
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.done()
};
segment_entities.push(segment_entity);
}
self.pop_parent();
self.world_mut().ui.set_ui_widget_state(
bar_entity,
UiWidgetState::Breadcrumb(crate::ecs::ui::components::UiBreadcrumbData {
segments: segments.iter().map(|s| s.to_string()).collect(),
clicked_segment: None,
changed: false,
segment_entities,
segment_text_slots,
}),
);
bar_entity
}
pub fn add_splitter(
&mut self,
direction: crate::ecs::ui::components::SplitDirection,
initial_ratio: f32,
) -> freecs::Entity {
use crate::ecs::ui::components::SplitDirection;
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let divider_color = theme.border_color;
let divider_hover_color = theme.accent_color;
let divider_thickness = 4.0;
let ratio = initial_ratio.clamp(0.05, 0.95);
let container_entity = self
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.entity();
self.push_parent(container_entity);
let (p1_1, p1_2, d1, d2, p2_1, p2_2) = match direction {
SplitDirection::Horizontal => (
Rl(Vec2::new(0.0, 0.0)),
Rl(Vec2::new(ratio * 100.0, 100.0)) + Ab(Vec2::new(-divider_thickness * 0.5, 0.0)),
Rl(Vec2::new(ratio * 100.0, 0.0)) + Ab(Vec2::new(-divider_thickness * 0.5, 0.0)),
Rl(Vec2::new(ratio * 100.0, 100.0)) + Ab(Vec2::new(divider_thickness * 0.5, 0.0)),
Rl(Vec2::new(ratio * 100.0, 0.0)) + Ab(Vec2::new(divider_thickness * 0.5, 0.0)),
Rl(Vec2::new(100.0, 100.0)),
),
SplitDirection::Vertical => (
Rl(Vec2::new(0.0, 0.0)),
Rl(Vec2::new(100.0, ratio * 100.0)) + Ab(Vec2::new(0.0, -divider_thickness * 0.5)),
Rl(Vec2::new(0.0, ratio * 100.0)) + Ab(Vec2::new(0.0, -divider_thickness * 0.5)),
Rl(Vec2::new(100.0, ratio * 100.0)) + Ab(Vec2::new(0.0, divider_thickness * 0.5)),
Rl(Vec2::new(0.0, ratio * 100.0)) + Ab(Vec2::new(0.0, divider_thickness * 0.5)),
Rl(Vec2::new(100.0, 100.0)),
),
};
let first_pane = self.add_node().boundary(p1_1, p1_2).with_clip().entity();
let cursor_icon = match direction {
SplitDirection::Horizontal => winit::window::CursorIcon::ColResize,
SplitDirection::Vertical => winit::window::CursorIcon::RowResize,
};
let divider_entity = self
.add_node()
.boundary(d1, d2)
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(divider_color)
.with_color::<UiHover>(divider_hover_color)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(cursor_icon)
.done();
let second_pane = self.add_node().boundary(p2_1, p2_2).with_clip().entity();
self.pop_parent();
self.world_mut().ui.set_ui_widget_state(
container_entity,
UiWidgetState::Splitter(crate::ecs::ui::components::UiSplitterData {
direction,
ratio,
changed: false,
first_pane,
second_pane,
divider_entity,
min_ratio: 0.05,
max_ratio: 0.95,
}),
);
container_entity
}
}