use std::collections::{HashMap, HashSet};
use nalgebra_glm::{Vec2, Vec4};
use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::ui::layout_types::{FlowLayout, GridLayout, UiLayoutType};
use crate::ecs::ui::state::{STATE_COUNT, UiBase, UiStateTrait};
use crate::ecs::ui::theme::UiTheme;
use crate::ecs::ui::types::Rect;
use crate::ecs::ui::units::UiValue;
use crate::render::wgpu::passes::geometry::UiLayer;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum AutoSizeMode {
#[default]
None,
Width,
Height,
Both,
}
#[derive(Clone, Debug)]
pub struct UiLayoutRoot {
pub absolute_scale: f32,
pub default_font_size: f32,
pub target_window: Option<usize>,
}
impl Default for UiLayoutRoot {
fn default() -> Self {
Self {
absolute_scale: 1.0,
default_font_size: 16.0,
target_window: None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum TextOverflow {
#[default]
Visible,
Ellipsis,
Clip,
Wrap,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum UiDepthMode {
Add(f32),
Set(f32),
}
impl Default for UiDepthMode {
fn default() -> Self {
Self::Add(1.0)
}
}
#[derive(Clone, Debug)]
pub struct UiLayoutNode {
pub layouts: Vec<Option<UiLayoutType>>,
pub flow_layout: Option<FlowLayout>,
pub responsive_flow: Option<crate::ecs::ui::layout_types::ResponsiveFlowOverride>,
pub grid_layout: Option<GridLayout>,
pub depth: UiDepthMode,
pub font_size: Option<f32>,
pub clip_content: bool,
pub pointer_events: bool,
pub visible: bool,
pub flow_child_size: Option<UiValue<Vec2>>,
pub flex_grow: Option<f32>,
pub flex_shrink: Option<f32>,
pub min_size: Option<Vec2>,
pub max_size: Option<Vec2>,
pub z_index: Option<i32>,
pub auto_size: AutoSizeMode,
pub auto_size_padding: Vec2,
pub computed_rect: Rect,
pub computed_depth: f32,
pub computed_clip_rect: Option<Rect>,
pub layer: Option<UiLayer>,
pub computed_layer: Option<UiLayer>,
pub animation: Option<UiNodeAnimation>,
}
impl Default for UiLayoutNode {
fn default() -> Self {
Self {
layouts: vec![None; STATE_COUNT],
flow_layout: None,
responsive_flow: None,
grid_layout: None,
depth: UiDepthMode::default(),
font_size: None,
clip_content: false,
pointer_events: true,
visible: true,
flow_child_size: None,
flex_grow: None,
flex_shrink: None,
min_size: None,
max_size: None,
z_index: None,
auto_size: AutoSizeMode::None,
auto_size_padding: Vec2::new(0.0, 0.0),
computed_rect: Rect::default(),
computed_depth: 0.0,
computed_clip_rect: None,
layer: None,
computed_layer: None,
animation: None,
}
}
}
impl UiLayoutNode {
pub fn base_layout(&self) -> Option<&UiLayoutType> {
self.layouts[UiBase::INDEX].as_ref()
}
pub fn ensure_state_capacity(&mut self, count: usize) {
if self.layouts.len() < count {
self.layouts.resize(count, None);
}
}
}
#[derive(Clone, Debug)]
pub struct UiNodeColor {
pub colors: Vec<Option<Vec4>>,
pub computed_color: Vec4,
}
impl Default for UiNodeColor {
fn default() -> Self {
Self {
colors: vec![None; STATE_COUNT],
computed_color: Vec4::new(1.0, 1.0, 1.0, 1.0),
}
}
}
impl UiNodeColor {
pub fn ensure_state_capacity(&mut self, count: usize) {
if self.colors.len() < count {
self.colors.resize(count, None);
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum UiNodeContent {
None,
Rect {
corner_radius: f32,
border_width: f32,
border_color: Vec4,
},
Text {
text_slot: usize,
font_index: usize,
font_size_override: Option<f32>,
outline_color: Vec4,
outline_width: f32,
alignment: TextAlignment,
vertical_alignment: VerticalAlignment,
overflow: TextOverflow,
},
Image {
texture_index: u32,
uv_min: Vec2,
uv_max: Vec2,
},
}
impl Default for UiNodeContent {
fn default() -> Self {
Self::None
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum AccessibleRole {
Button,
Slider,
Checkbox,
Toggle,
TextInput,
TextArea,
Dropdown,
Tab,
TabPanel,
Tree,
TreeItem,
Grid,
GridCell,
Dialog,
Alert,
ProgressBar,
Menu,
MenuItem,
}
#[derive(Clone, Debug, Default)]
pub struct UiNodeInteraction {
pub hovered: bool,
pub pressed: bool,
pub clicked: bool,
pub focused: bool,
pub drag_start: Option<Vec2>,
pub dragging: bool,
pub cursor_icon: Option<winit::window::CursorIcon>,
pub double_clicked: bool,
pub right_clicked: bool,
pub tooltip_text: Option<String>,
pub tooltip_entity: Option<freecs::Entity>,
pub tab_index: Option<i32>,
pub disabled: bool,
pub error_text: Option<String>,
pub validation_rules: Vec<ValidationRule>,
pub accessible_role: Option<AccessibleRole>,
pub accessible_label: Option<String>,
}
#[derive(Clone, Copy, Debug)]
pub struct StateTransition {
pub enter_speed: f32,
pub exit_speed: f32,
pub easing: crate::ecs::tween::easing::EasingFunction,
}
impl Default for StateTransition {
fn default() -> Self {
Self {
enter_speed: 10.0,
exit_speed: 4.0,
easing: crate::ecs::tween::easing::EasingFunction::Linear,
}
}
}
#[derive(Clone, Debug)]
pub struct UiStateWeights {
pub weights: Vec<f32>,
pub transitions: Vec<Option<StateTransition>>,
pub progress: Vec<f32>,
pub targets: Vec<f32>,
pub start_weights: Vec<f32>,
}
impl Default for UiStateWeights {
fn default() -> Self {
let mut weights = vec![0.0; STATE_COUNT];
weights[UiBase::INDEX] = 1.0;
let mut targets = vec![0.0; STATE_COUNT];
targets[UiBase::INDEX] = 1.0;
Self {
weights: weights.clone(),
transitions: vec![None; STATE_COUNT],
progress: vec![1.0; STATE_COUNT],
targets,
start_weights: weights,
}
}
}
impl UiStateWeights {
pub fn ensure_state_capacity(&mut self, count: usize) {
if self.weights.len() < count {
self.weights.resize(count, 0.0);
}
if self.transitions.len() < count {
self.transitions.resize(count, None);
}
if self.progress.len() < count {
self.progress.resize(count, 1.0);
}
if self.targets.len() < count {
self.targets.resize(count, 0.0);
}
if self.start_weights.len() < count {
self.start_weights.resize(count, 0.0);
}
}
}
#[derive(Clone, Debug)]
pub struct UiButtonData {
pub clicked: bool,
pub text_slot: usize,
}
#[derive(Clone, Debug)]
pub struct UiSliderData {
pub min: f32,
pub max: f32,
pub value: f32,
pub changed: bool,
pub fill_entity: freecs::Entity,
pub text_slot: usize,
pub logarithmic: bool,
pub precision: usize,
pub prefix: String,
pub suffix: String,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RangeSliderThumb {
Low,
High,
}
#[derive(Clone, Debug)]
pub struct UiRangeSliderData {
pub min: f32,
pub max: f32,
pub low_value: f32,
pub high_value: f32,
pub changed: bool,
pub fill_entity: freecs::Entity,
pub low_thumb_entity: freecs::Entity,
pub high_thumb_entity: freecs::Entity,
pub text_slot: usize,
pub active_thumb: Option<RangeSliderThumb>,
pub precision: usize,
pub thumb_half_size: f32,
}
#[derive(Clone, Debug)]
pub struct UiToggleData {
pub value: bool,
pub changed: bool,
pub animated_position: f32,
pub knob_entity: freecs::Entity,
}
#[derive(Clone, Debug)]
pub struct UiCheckboxData {
pub value: bool,
pub changed: bool,
pub inner_entity: freecs::Entity,
}
#[derive(Clone, Debug)]
pub struct UiRadioData {
pub group_id: u32,
pub option_index: usize,
pub selected: bool,
pub changed: bool,
pub inner_entity: freecs::Entity,
}
#[derive(Clone, Debug)]
pub struct UiProgressBarData {
pub value: f32,
pub fill_entity: freecs::Entity,
}
#[derive(Clone, Debug)]
pub struct UiCollapsingHeaderData {
pub open: bool,
pub changed: bool,
pub content_entity: freecs::Entity,
pub arrow_text_slot: usize,
}
#[derive(Clone, Debug)]
pub struct UiScrollAreaData {
pub scroll_offset: f32,
pub content_entity: freecs::Entity,
pub content_height: f32,
pub visible_height: f32,
pub thumb_entity: freecs::Entity,
pub track_entity: freecs::Entity,
pub thumb_dragging: bool,
pub thumb_drag_start_offset: f32,
pub snap_interval: Option<f32>,
}
#[derive(Clone, Debug)]
pub struct UiTabBarData {
pub selected_tab: usize,
pub changed: bool,
pub tab_entities: Vec<freecs::Entity>,
pub tab_text_slots: Vec<usize>,
}
#[derive(Clone, Debug)]
pub struct UiBreadcrumbData {
pub segments: Vec<String>,
pub clicked_segment: Option<usize>,
pub changed: bool,
pub segment_entities: Vec<freecs::Entity>,
pub segment_text_slots: Vec<usize>,
}
#[derive(Clone, Debug)]
pub struct UiSplitterData {
pub direction: SplitDirection,
pub ratio: f32,
pub changed: bool,
pub first_pane: freecs::Entity,
pub second_pane: freecs::Entity,
pub divider_entity: freecs::Entity,
pub min_ratio: f32,
pub max_ratio: f32,
}
#[derive(Clone, Debug)]
pub struct TextSnapshot {
pub text: String,
pub cursor_position: usize,
pub selection_start: Option<usize>,
}
#[derive(Clone, Debug)]
pub struct RichTextSnapshot {
pub text: String,
pub char_styles: Vec<CharStyle>,
pub cursor_position: usize,
pub selection_start: Option<usize>,
}
#[derive(Clone, Debug)]
pub struct UndoStack<T> {
history: Vec<T>,
position: usize,
max_entries: usize,
last_push_time: f64,
}
impl<T> UndoStack<T> {
pub fn new(max_entries: usize) -> Self {
Self {
history: Vec::new(),
position: 0,
max_entries,
last_push_time: 0.0,
}
}
pub fn push_initial(&mut self, snapshot: T) {
if self.history.is_empty() {
self.history.push(snapshot);
self.position = 0;
}
}
pub fn push(&mut self, snapshot: T, time: f64) {
self.history.truncate(self.position + 1);
if time - self.last_push_time < 0.3 && self.history.len() > 1 {
*self.history.last_mut().unwrap() = snapshot;
} else {
self.history.push(snapshot);
if self.history.len() > self.max_entries {
self.history.remove(0);
}
}
self.position = self.history.len() - 1;
self.last_push_time = time;
}
pub fn undo(&mut self) -> Option<&T> {
if self.position > 0 {
self.position -= 1;
Some(&self.history[self.position])
} else {
None
}
}
pub fn redo(&mut self) -> Option<&T> {
if self.position + 1 < self.history.len() {
self.position += 1;
Some(&self.history[self.position])
} else {
None
}
}
pub fn can_undo(&self) -> bool {
self.position > 0
}
pub fn can_redo(&self) -> bool {
self.position + 1 < self.history.len()
}
pub fn is_empty(&self) -> bool {
self.history.is_empty()
}
}
#[derive(Clone, Debug, Default)]
pub enum InputMask {
#[default]
None,
Numeric,
Decimal,
Alpha,
AlphaNumeric,
Custom(fn(char) -> bool),
}
impl InputMask {
pub fn accepts(&self, character: char) -> bool {
match self {
Self::None => true,
Self::Numeric => character.is_ascii_digit(),
Self::Decimal => character.is_ascii_digit() || character == '.' || character == '-',
Self::Alpha => character.is_alphabetic(),
Self::AlphaNumeric => character.is_alphanumeric(),
Self::Custom(predicate) => predicate(character),
}
}
}
#[derive(Clone)]
pub enum ValidationRule {
Required,
MinLength(usize),
MaxLength(usize),
Custom(fn(&str) -> Result<(), String>),
}
impl std::fmt::Debug for ValidationRule {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Required => write!(formatter, "Required"),
Self::MinLength(n) => write!(formatter, "MinLength({})", n),
Self::MaxLength(n) => write!(formatter, "MaxLength({})", n),
Self::Custom(_) => write!(formatter, "Custom(fn)"),
}
}
}
impl ValidationRule {
pub fn validate(&self, value: &str) -> Result<(), String> {
match self {
Self::Required => {
if value.trim().is_empty() {
Err("This field is required".to_string())
} else {
Ok(())
}
}
Self::MinLength(min) => {
if value.chars().count() < *min {
Err(format!("Minimum {} characters required", min))
} else {
Ok(())
}
}
Self::MaxLength(max) => {
if value.chars().count() > *max {
Err(format!("Maximum {} characters allowed", max))
} else {
Ok(())
}
}
Self::Custom(validator) => validator(value),
}
}
}
#[derive(Clone, Debug)]
pub struct UiTextInputData {
pub text: String,
pub cursor_position: usize,
pub selection_start: Option<usize>,
pub changed: bool,
pub text_slot: usize,
pub cursor_entity: freecs::Entity,
pub selection_entity: freecs::Entity,
pub scroll_offset: f32,
pub cursor_blink_timer: f64,
pub placeholder_entity: Option<freecs::Entity>,
pub undo_stack: UndoStack<TextSnapshot>,
pub input_mask: InputMask,
pub max_length: Option<usize>,
}
#[derive(Clone, Debug)]
pub struct UiDropdownData {
pub options: Vec<String>,
pub selected_index: usize,
pub changed: bool,
pub open: bool,
pub header_text_slot: usize,
pub popup_entities: Vec<freecs::Entity>,
pub popup_container_entity: freecs::Entity,
pub hovered_index: Option<usize>,
pub is_theme_dropdown: bool,
pub searchable: bool,
pub filter_text: String,
pub filter_input_entity: Option<freecs::Entity>,
pub filtered_indices: Vec<usize>,
}
#[derive(Clone, Debug)]
pub struct UiMultiSelectData {
pub options: Vec<String>,
pub selected_indices: HashSet<usize>,
pub changed: bool,
pub open: bool,
pub header_text_slot: usize,
pub popup_entities: Vec<freecs::Entity>,
pub popup_container_entity: freecs::Entity,
pub hovered_index: Option<usize>,
pub check_entities: Vec<freecs::Entity>,
}
#[derive(Clone, Debug)]
pub struct UiDatePickerData {
pub year: i32,
pub month: u32,
pub day: u32,
pub changed: bool,
pub open: bool,
pub header_text_slot: usize,
pub popup_entity: freecs::Entity,
pub day_entities: Vec<freecs::Entity>,
pub day_text_slots: Vec<usize>,
pub month_label_slot: usize,
pub prev_month_entity: freecs::Entity,
pub next_month_entity: freecs::Entity,
pub selected_day_entity: Option<freecs::Entity>,
}
#[derive(Clone, Debug)]
pub struct UiMenuData {
pub items: Vec<String>,
pub clicked_item: Option<usize>,
pub open: bool,
pub label_text_slot: usize,
pub popup_entities: Vec<freecs::Entity>,
pub popup_container_entity: freecs::Entity,
}
#[derive(Clone, Debug)]
pub struct UiPanelData {
pub title: String,
pub title_text_slot: usize,
pub content_entity: freecs::Entity,
pub header_entity: freecs::Entity,
pub collapsed: bool,
pub panel_kind: UiPanelKind,
pub focus_order: i32,
pub pinned: bool,
pub min_size: Vec2,
pub drag_offset: Option<Vec2>,
pub resize_edge: Option<ResizeEdge>,
pub resize_start_rect: Option<Rect>,
pub resize_start_mouse: Option<Vec2>,
pub undocked_rect: Option<Rect>,
pub default_dock_size: f32,
pub collapse_button_entity: Option<freecs::Entity>,
pub collapse_button_text_slot: Option<usize>,
pub header_visible: bool,
pub resizable: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum UiPanelKind {
Floating,
DockedLeft,
DockedRight,
DockedTop,
DockedBottom,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ResizeEdge {
Left,
Right,
Top,
Bottom,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorPickerMode {
Rgb,
Hsv,
}
#[derive(Clone, Debug)]
pub struct UiColorPickerData {
pub color: Vec4,
pub changed: bool,
pub swatch_entity: freecs::Entity,
pub slider_entities: [freecs::Entity; 4],
pub mode: ColorPickerMode,
}
#[derive(Clone, Debug)]
pub struct UiSelectableLabelData {
pub selected: bool,
pub changed: bool,
pub text_slot: usize,
pub group_id: Option<u32>,
}
pub struct DragValueConfig<'a> {
pub initial: f32,
pub min: f32,
pub max: f32,
pub speed: f32,
pub precision: usize,
pub prefix: &'a str,
pub suffix: &'a str,
pub show_arrows: bool,
}
impl<'a> DragValueConfig<'a> {
pub fn new(min: f32, max: f32, initial: f32) -> Self {
Self {
initial,
min,
max,
speed: 0.1,
precision: 2,
prefix: "",
suffix: "",
show_arrows: true,
}
}
pub fn hide_arrows(mut self) -> Self {
self.show_arrows = false;
self
}
pub fn speed(mut self, speed: f32) -> Self {
self.speed = speed;
self
}
pub fn precision(mut self, precision: usize) -> Self {
self.precision = precision;
self
}
pub fn prefix(mut self, prefix: &'a str) -> Self {
self.prefix = prefix;
self
}
pub fn suffix(mut self, suffix: &'a str) -> Self {
self.suffix = suffix;
self
}
}
pub struct SliderConfig<'a> {
pub min: f32,
pub max: f32,
pub initial: f32,
pub logarithmic: bool,
pub precision: usize,
pub prefix: &'a str,
pub suffix: &'a str,
}
impl<'a> SliderConfig<'a> {
pub fn new(min: f32, max: f32, initial: f32) -> Self {
Self {
min,
max,
initial,
logarithmic: false,
precision: 1,
prefix: "",
suffix: "",
}
}
pub fn logarithmic(mut self) -> Self {
self.logarithmic = true;
self.precision = 3;
self
}
pub fn precision(mut self, precision: usize) -> Self {
self.precision = precision;
self
}
pub fn prefix(mut self, prefix: &'a str) -> Self {
self.prefix = prefix;
self
}
pub fn suffix(mut self, suffix: &'a str) -> Self {
self.suffix = suffix;
self
}
}
#[derive(Clone, Debug)]
pub struct UiDragValueData {
pub value: f32,
pub min: f32,
pub max: f32,
pub speed: f32,
pub precision: usize,
pub prefix: String,
pub suffix: String,
pub changed: bool,
pub text_slot: usize,
pub editing: bool,
pub cursor_entity: freecs::Entity,
pub selection_entity: freecs::Entity,
pub edit_text: String,
pub cursor_position: usize,
pub selection_start: Option<usize>,
pub cursor_blink_timer: f64,
pub scroll_offset: f32,
pub drag_start_value: f32,
pub undo_stack: UndoStack<TextSnapshot>,
pub up_entity: Option<freecs::Entity>,
pub down_entity: Option<freecs::Entity>,
pub step: f32,
}
#[derive(Clone, Debug)]
pub struct ContextMenuItem {
pub label: String,
pub shortcut: Option<String>,
pub separator: bool,
}
#[derive(Clone, Debug)]
pub enum ContextMenuItemKind {
Action,
Separator,
Submenu {
children: Vec<ContextMenuItemDef>,
popup_entity: freecs::Entity,
open: bool,
},
Widget {
content_entity: freecs::Entity,
},
}
#[derive(Clone, Debug)]
pub struct ContextMenuItemDef {
pub label: String,
pub shortcut: String,
pub kind: ContextMenuItemKind,
pub row_entity: freecs::Entity,
pub command_id: Option<usize>,
pub binding: Option<ShortcutBinding>,
}
#[derive(Clone, Debug)]
pub struct UiContextMenuData {
pub items: Vec<ContextMenuItem>,
pub open: bool,
pub clicked_item: Option<usize>,
pub popup_entity: freecs::Entity,
pub item_entities: Vec<freecs::Entity>,
pub item_defs: Vec<ContextMenuItemDef>,
}
#[derive(Clone, Debug)]
pub struct UiTreeNodeData {
pub label: String,
pub text_slot: usize,
pub depth: usize,
pub expanded: bool,
pub selected: bool,
pub row_entity: freecs::Entity,
pub arrow_entity: freecs::Entity,
pub arrow_text_slot: usize,
pub children_container: freecs::Entity,
pub user_data: u64,
pub parent_node: Option<freecs::Entity>,
pub wrapper_entity: freecs::Entity,
pub lazy: bool,
pub lazy_loaded: bool,
}
#[derive(Clone, Debug)]
pub struct UiTreeViewData {
pub selected_nodes: Vec<freecs::Entity>,
pub multi_select: bool,
pub node_entities: Vec<freecs::Entity>,
pub changed: bool,
pub content_entity: freecs::Entity,
pub context_menu_node: Option<freecs::Entity>,
pub filter_text: String,
pub filter_active: bool,
pub pre_filter_expanded: HashMap<freecs::Entity, bool>,
}
#[derive(Clone, Debug)]
pub struct UiModalDialogData {
pub title_text_slot: usize,
pub content_entity: freecs::Entity,
pub backdrop_entity: freecs::Entity,
pub ok_button: Option<freecs::Entity>,
pub cancel_button: Option<freecs::Entity>,
pub result: Option<bool>,
}
#[derive(Clone, Debug)]
pub struct TextSpan {
pub text: String,
pub color: Option<Vec4>,
pub font_size_override: Option<f32>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub font_index: Option<usize>,
}
impl TextSpan {
pub fn new(text: &str) -> Self {
Self {
text: text.to_string(),
color: None,
font_size_override: None,
bold: false,
italic: false,
underline: false,
font_index: None,
}
}
pub fn colored(text: &str, color: Vec4) -> Self {
Self {
text: text.to_string(),
color: Some(color),
font_size_override: None,
bold: false,
italic: false,
underline: false,
font_index: None,
}
}
pub fn sized(text: &str, font_size: f32) -> Self {
Self {
text: text.to_string(),
color: None,
font_size_override: Some(font_size),
bold: false,
italic: false,
underline: false,
font_index: None,
}
}
pub fn with_color(mut self, color: Vec4) -> Self {
self.color = Some(color);
self
}
pub fn with_size(mut self, font_size: f32) -> Self {
self.font_size_override = Some(font_size);
self
}
pub fn with_bold(mut self) -> Self {
self.bold = true;
self
}
pub fn with_italic(mut self) -> Self {
self.italic = true;
self
}
pub fn with_underline(mut self) -> Self {
self.underline = true;
self
}
pub fn with_font(mut self, font_index: usize) -> Self {
self.font_index = Some(font_index);
self
}
}
#[derive(Clone, Debug)]
pub struct UiRichTextData {
pub span_entities: Vec<freecs::Entity>,
pub span_text_slots: Vec<usize>,
}
#[derive(Clone, Debug)]
pub struct DataGridColumn {
pub label: String,
pub width: f32,
pub sortable: bool,
pub alignment: TextAlignment,
pub editable: bool,
}
impl DataGridColumn {
pub fn new(label: &str, width: f32) -> Self {
Self {
label: label.to_string(),
width,
sortable: false,
alignment: TextAlignment::Left,
editable: false,
}
}
pub fn sortable(mut self) -> Self {
self.sortable = true;
self
}
pub fn alignment(mut self, alignment: TextAlignment) -> Self {
self.alignment = alignment;
self
}
pub fn editable(mut self) -> Self {
self.editable = true;
self
}
}
#[derive(Clone, Debug)]
pub struct DataGridPoolRow {
pub row_entity: freecs::Entity,
pub cell_text_slots: Vec<usize>,
pub cell_entities: Vec<freecs::Entity>,
}
#[derive(Clone, Debug)]
pub struct UiPropertyGridData {
pub label_width: f32,
pub row_entities: Vec<freecs::Entity>,
pub label_entities: Vec<freecs::Entity>,
pub resize_active: bool,
pub resize_start_x: f32,
pub resize_start_width: f32,
}
pub trait DataGridDataSource {
fn row_count(&self) -> usize;
fn cell_text(&self, row: usize, column: usize) -> String;
}
pub trait VirtualListDataSource {
fn item_count(&self) -> usize;
fn bind_item(
&self,
world: &mut crate::ecs::world::World,
item_index: usize,
container: freecs::Entity,
);
}
#[derive(Clone, Debug)]
pub struct UiDataGridData {
pub columns: Vec<DataGridColumn>,
pub row_height: f32,
pub pool_size: usize,
pub total_rows: usize,
pub scroll_entity: freecs::Entity,
pub body_entity: freecs::Entity,
pub header_entities: Vec<freecs::Entity>,
pub header_text_slots: Vec<usize>,
pub top_spacer: freecs::Entity,
pub bottom_spacer: freecs::Entity,
pub pool_rows: Vec<DataGridPoolRow>,
pub visible_start: usize,
pub sort_column: Option<usize>,
pub sort_ascending: bool,
pub sort_changed: bool,
pub selected_rows: HashSet<usize>,
pub selection_anchor: Option<usize>,
pub selection_changed: bool,
pub focused: bool,
pub resize_column: Option<usize>,
pub resize_start_x: f32,
pub resize_start_width: f32,
pub filtered_indices: Option<Vec<usize>>,
pub header_divider_entities: Vec<freecs::Entity>,
pub filter_row_entity: Option<freecs::Entity>,
pub filter_input_entities: Vec<freecs::Entity>,
pub filter_texts: Vec<String>,
pub editing_cell: Option<(usize, usize)>,
pub editing_input_entity: Option<freecs::Entity>,
}
#[derive(Clone, Debug)]
pub struct CommandEntry {
pub label: String,
pub shortcut: String,
pub category: String,
pub enabled: bool,
}
#[derive(Clone, Debug)]
pub struct UiCommandPaletteData {
pub commands: Vec<CommandEntry>,
pub filter_text: String,
pub filtered_indices: Vec<usize>,
pub selected_index: usize,
pub open: bool,
pub executed_command: Option<usize>,
pub text_input_entity: freecs::Entity,
pub result_entities: Vec<freecs::Entity>,
pub result_text_slots: Vec<usize>,
pub backdrop_entity: freecs::Entity,
pub dialog_entity: freecs::Entity,
pub pool_size: usize,
pub scroll_entity: freecs::Entity,
}
#[derive(Clone, Debug)]
pub struct VirtualListPoolItem {
pub container_entity: freecs::Entity,
}
#[derive(Clone, Debug)]
pub struct UiVirtualListData {
pub item_height: f32,
pub pool_size: usize,
pub total_items: usize,
pub visible_start: usize,
pub scroll_entity: freecs::Entity,
pub body_entity: freecs::Entity,
pub top_spacer: freecs::Entity,
pub bottom_spacer: freecs::Entity,
pub pool_items: Vec<VirtualListPoolItem>,
pub selection: Option<usize>,
pub selection_changed: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ThemeColor {
Text,
TextDisabled,
TextAccent,
Background,
BackgroundHover,
BackgroundActive,
Panel,
PanelHeader,
Border,
BorderFocused,
Accent,
AccentHover,
AccentActive,
Success,
Warning,
Error,
SliderTrack,
SliderFill,
InputBackground,
InputBackgroundFocused,
Selection,
Scrollbar,
ScrollbarHover,
}
impl ThemeColor {
pub fn resolve(self, theme: &UiTheme) -> Vec4 {
match self {
Self::Text => theme.text_color,
Self::TextDisabled => theme.text_color_disabled,
Self::TextAccent => theme.text_color_accent,
Self::Background => theme.background_color,
Self::BackgroundHover => theme.background_color_hovered,
Self::BackgroundActive => theme.background_color_active,
Self::Panel => theme.panel_color,
Self::PanelHeader => theme.panel_header_color,
Self::Border => theme.border_color,
Self::BorderFocused => theme.border_color_focused,
Self::Accent => theme.accent_color,
Self::AccentHover => theme.accent_color_hovered,
Self::AccentActive => theme.accent_color_active,
Self::Success => theme.success_color,
Self::Warning => theme.warning_color,
Self::Error => theme.error_color,
Self::SliderTrack => theme.slider_track_color,
Self::SliderFill => theme.slider_fill_color,
Self::InputBackground => theme.input_background_color,
Self::InputBackgroundFocused => theme.input_background_focused,
Self::Selection => theme.selection_color,
Self::Scrollbar => theme.scrollbar_color,
Self::ScrollbarHover => theme.scrollbar_color_hovered,
}
}
}
#[derive(Clone, Debug)]
pub struct UiThemeBinding {
pub color_roles: Vec<Option<ThemeColor>>,
pub border_color_role: Option<ThemeColor>,
}
impl Default for UiThemeBinding {
fn default() -> Self {
Self {
color_roles: vec![None; STATE_COUNT],
border_color_role: None,
}
}
}
impl UiThemeBinding {
pub fn ensure_state_capacity(&mut self, count: usize) {
if self.color_roles.len() < count {
self.color_roles.resize(count, None);
}
}
}
#[derive(Clone, Debug)]
pub enum CanvasCommand {
Rect {
position: Vec2,
size: Vec2,
color: Vec4,
corner_radius: f32,
id: Option<u32>,
},
Circle {
center: Vec2,
radius: f32,
color: Vec4,
id: Option<u32>,
},
Line {
from: Vec2,
to: Vec2,
thickness: f32,
color: Vec4,
id: Option<u32>,
},
Text {
text: String,
position: Vec2,
font_size: f32,
color: Vec4,
id: Option<u32>,
},
Polyline {
points: Vec<Vec2>,
thickness: f32,
color: Vec4,
closed: bool,
id: Option<u32>,
},
RectStroke {
position: Vec2,
size: Vec2,
color: Vec4,
thickness: f32,
corner_radius: f32,
id: Option<u32>,
},
CircleStroke {
center: Vec2,
radius: f32,
color: Vec4,
thickness: f32,
id: Option<u32>,
},
Arc {
center: Vec2,
radius: f32,
start_angle: f32,
end_angle: f32,
thickness: f32,
color: Vec4,
id: Option<u32>,
},
QuadraticBezier {
start: Vec2,
control: Vec2,
end: Vec2,
thickness: f32,
color: Vec4,
id: Option<u32>,
},
CubicBezier {
start: Vec2,
control1: Vec2,
control2: Vec2,
end: Vec2,
thickness: f32,
color: Vec4,
id: Option<u32>,
},
}
#[derive(Clone, Debug)]
pub struct UiTextAreaData {
pub text: String,
pub cursor_position: usize,
pub selection_start: Option<usize>,
pub changed: bool,
pub text_slot: usize,
pub cursor_entity: freecs::Entity,
pub selection_pool: Vec<freecs::Entity>,
pub scroll_offset_y: f32,
pub cursor_blink_timer: f64,
pub placeholder_entity: Option<freecs::Entity>,
pub line_height: f32,
pub visible_rows: usize,
pub syntax_language: Option<String>,
pub undo_stack: UndoStack<TextSnapshot>,
pub max_length: Option<usize>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CharStyle {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub color: Option<Vec4>,
}
#[derive(Clone, Debug)]
pub struct UiRichTextEditorData {
pub text: String,
pub char_styles: Vec<CharStyle>,
pub current_style: CharStyle,
pub cursor_position: usize,
pub selection_start: Option<usize>,
pub changed: bool,
pub text_slot: usize,
pub cursor_entity: freecs::Entity,
pub selection_pool: Vec<freecs::Entity>,
pub scroll_offset_y: f32,
pub cursor_blink_timer: f64,
pub line_height: f32,
pub visible_rows: usize,
pub placeholder_entity: Option<freecs::Entity>,
pub undo_stack: UndoStack<RichTextSnapshot>,
}
impl UiRichTextEditorData {
pub fn spans(&self) -> Vec<TextSpan> {
if self.text.is_empty() {
return Vec::new();
}
let chars: Vec<char> = self.text.chars().collect();
let mut spans = Vec::new();
let mut run_start = 0;
let default_style = CharStyle::default();
for index in 1..=chars.len() {
let current_style = self.char_styles.get(index).unwrap_or(&default_style);
let prev_style = self.char_styles.get(run_start).unwrap_or(&default_style);
if index == chars.len() || current_style != prev_style {
let run_text: String = chars[run_start..index].iter().collect();
let style = self.char_styles.get(run_start).cloned().unwrap_or_default();
let mut span = TextSpan::new(&run_text);
span.bold = style.bold;
span.italic = style.italic;
span.underline = style.underline;
span.color = style.color;
spans.push(span);
run_start = index;
}
}
spans
}
}
#[derive(Clone, Debug)]
pub enum DragPayload {
Index(usize),
Entity(freecs::Entity),
Text(String),
Custom(u64),
}
impl Default for DragPayload {
fn default() -> Self {
Self::Custom(0)
}
}
#[derive(Clone, Debug, Default)]
pub struct UiDragSource {
pub payload: DragPayload,
}
#[derive(Clone, Debug, Default)]
pub enum DragAcceptFilter {
#[default]
Any,
IndexOnly,
EntityOnly,
TextOnly,
CustomOnly,
}
impl DragAcceptFilter {
pub fn accepts(&self, payload: &DragPayload) -> bool {
match self {
Self::Any => true,
Self::IndexOnly => matches!(payload, DragPayload::Index(_)),
Self::EntityOnly => matches!(payload, DragPayload::Entity(_)),
Self::TextOnly => matches!(payload, DragPayload::Text(_)),
Self::CustomOnly => matches!(payload, DragPayload::Custom(_)),
}
}
}
#[derive(Clone, Debug)]
pub struct UiDropTarget {
pub accepted: bool,
pub filter: DragAcceptFilter,
}
impl Default for UiDropTarget {
fn default() -> Self {
Self {
accepted: true,
filter: DragAcceptFilter::Any,
}
}
}
#[derive(Clone, Debug)]
pub struct ShortcutBinding {
pub ctrl: bool,
pub shift: bool,
pub alt: bool,
pub key: winit::keyboard::KeyCode,
}
impl ShortcutBinding {
pub fn parse(shortcut: &str) -> Option<Self> {
let mut ctrl = false;
let mut shift = false;
let mut alt = false;
let mut key_part = "";
for part in shortcut.split('+') {
let trimmed = part.trim();
match trimmed.to_lowercase().as_str() {
"ctrl" => ctrl = true,
"shift" => shift = true,
"alt" => alt = true,
_ => key_part = trimmed,
}
}
let key = match key_part.to_uppercase().as_str() {
"A" => winit::keyboard::KeyCode::KeyA,
"B" => winit::keyboard::KeyCode::KeyB,
"C" => winit::keyboard::KeyCode::KeyC,
"D" => winit::keyboard::KeyCode::KeyD,
"E" => winit::keyboard::KeyCode::KeyE,
"F" => winit::keyboard::KeyCode::KeyF,
"G" => winit::keyboard::KeyCode::KeyG,
"H" => winit::keyboard::KeyCode::KeyH,
"I" => winit::keyboard::KeyCode::KeyI,
"J" => winit::keyboard::KeyCode::KeyJ,
"K" => winit::keyboard::KeyCode::KeyK,
"L" => winit::keyboard::KeyCode::KeyL,
"M" => winit::keyboard::KeyCode::KeyM,
"N" => winit::keyboard::KeyCode::KeyN,
"O" => winit::keyboard::KeyCode::KeyO,
"P" => winit::keyboard::KeyCode::KeyP,
"Q" => winit::keyboard::KeyCode::KeyQ,
"R" => winit::keyboard::KeyCode::KeyR,
"S" => winit::keyboard::KeyCode::KeyS,
"T" => winit::keyboard::KeyCode::KeyT,
"U" => winit::keyboard::KeyCode::KeyU,
"V" => winit::keyboard::KeyCode::KeyV,
"W" => winit::keyboard::KeyCode::KeyW,
"X" => winit::keyboard::KeyCode::KeyX,
"Y" => winit::keyboard::KeyCode::KeyY,
"Z" => winit::keyboard::KeyCode::KeyZ,
"F1" => winit::keyboard::KeyCode::F1,
"F2" => winit::keyboard::KeyCode::F2,
"F3" => winit::keyboard::KeyCode::F3,
"F4" => winit::keyboard::KeyCode::F4,
"F5" => winit::keyboard::KeyCode::F5,
"F6" => winit::keyboard::KeyCode::F6,
"F7" => winit::keyboard::KeyCode::F7,
"F8" => winit::keyboard::KeyCode::F8,
"F9" => winit::keyboard::KeyCode::F9,
"F10" => winit::keyboard::KeyCode::F10,
"F11" => winit::keyboard::KeyCode::F11,
"F12" => winit::keyboard::KeyCode::F12,
"ESCAPE" | "ESC" => winit::keyboard::KeyCode::Escape,
"ENTER" | "RETURN" => winit::keyboard::KeyCode::Enter,
"SPACE" => winit::keyboard::KeyCode::Space,
"DELETE" | "DEL" => winit::keyboard::KeyCode::Delete,
"BACKSPACE" => winit::keyboard::KeyCode::Backspace,
"TAB" => winit::keyboard::KeyCode::Tab,
_ => return None,
};
Some(Self {
ctrl,
shift,
alt,
key,
})
}
}
#[derive(Clone, Debug)]
pub struct UiCanvasData {
pub commands: Vec<CanvasCommand>,
pub hit_test_enabled: bool,
pub command_bounds: Vec<(u32, crate::ecs::ui::types::Rect)>,
}
#[derive(Clone, Debug)]
pub enum UiWidgetState {
Button(UiButtonData),
Slider(UiSliderData),
Toggle(UiToggleData),
Checkbox(UiCheckboxData),
Radio(UiRadioData),
ProgressBar(UiProgressBarData),
CollapsingHeader(UiCollapsingHeaderData),
ScrollArea(UiScrollAreaData),
TabBar(UiTabBarData),
TextInput(UiTextInputData),
Dropdown(UiDropdownData),
Menu(UiMenuData),
Panel(UiPanelData),
ColorPicker(UiColorPickerData),
SelectableLabel(UiSelectableLabelData),
DragValue(UiDragValueData),
ContextMenu(UiContextMenuData),
TreeView(UiTreeViewData),
TreeNode(UiTreeNodeData),
ModalDialog(UiModalDialogData),
RichText(UiRichTextData),
DataGrid(UiDataGridData),
PropertyGrid(UiPropertyGridData),
CommandPalette(UiCommandPaletteData),
Canvas(UiCanvasData),
TextArea(UiTextAreaData),
TileContainer(UiTileContainerData),
VirtualList(UiVirtualListData),
RichTextEditor(UiRichTextEditorData),
RangeSlider(UiRangeSliderData),
Breadcrumb(UiBreadcrumbData),
Splitter(UiSplitterData),
MultiSelect(UiMultiSelectData),
DatePicker(UiDatePickerData),
}
impl Default for UiWidgetState {
fn default() -> Self {
Self::Button(UiButtonData {
clicked: false,
text_slot: 0,
})
}
}
pub trait FromWidgetState {
fn from_widget_state(state: &UiWidgetState) -> Option<&Self>;
}
macro_rules! impl_from_widget_state {
($variant:ident, $data:ty) => {
impl FromWidgetState for $data {
fn from_widget_state(state: &UiWidgetState) -> Option<&Self> {
match state {
UiWidgetState::$variant(d) => Some(d),
_ => None,
}
}
}
};
}
impl_from_widget_state!(Button, UiButtonData);
impl_from_widget_state!(Slider, UiSliderData);
impl_from_widget_state!(Toggle, UiToggleData);
impl_from_widget_state!(Checkbox, UiCheckboxData);
impl_from_widget_state!(Radio, UiRadioData);
impl_from_widget_state!(ProgressBar, UiProgressBarData);
impl_from_widget_state!(CollapsingHeader, UiCollapsingHeaderData);
impl_from_widget_state!(ScrollArea, UiScrollAreaData);
impl_from_widget_state!(TabBar, UiTabBarData);
impl_from_widget_state!(TextInput, UiTextInputData);
impl_from_widget_state!(Dropdown, UiDropdownData);
impl_from_widget_state!(Menu, UiMenuData);
impl_from_widget_state!(Panel, UiPanelData);
impl_from_widget_state!(ColorPicker, UiColorPickerData);
impl_from_widget_state!(SelectableLabel, UiSelectableLabelData);
impl_from_widget_state!(DragValue, UiDragValueData);
impl_from_widget_state!(ContextMenu, UiContextMenuData);
impl_from_widget_state!(TreeView, UiTreeViewData);
impl_from_widget_state!(TreeNode, UiTreeNodeData);
impl_from_widget_state!(ModalDialog, UiModalDialogData);
impl_from_widget_state!(RichText, UiRichTextData);
impl_from_widget_state!(DataGrid, UiDataGridData);
impl_from_widget_state!(PropertyGrid, UiPropertyGridData);
impl_from_widget_state!(CommandPalette, UiCommandPaletteData);
impl_from_widget_state!(Canvas, UiCanvasData);
impl_from_widget_state!(TextArea, UiTextAreaData);
impl_from_widget_state!(TileContainer, UiTileContainerData);
impl_from_widget_state!(VirtualList, UiVirtualListData);
impl_from_widget_state!(RichTextEditor, UiRichTextEditorData);
impl_from_widget_state!(RangeSlider, UiRangeSliderData);
impl_from_widget_state!(Breadcrumb, UiBreadcrumbData);
impl_from_widget_state!(Splitter, UiSplitterData);
impl_from_widget_state!(MultiSelect, UiMultiSelectData);
impl_from_widget_state!(DatePicker, UiDatePickerData);
impl UiWidgetState {
pub fn changed(&self) -> bool {
match self {
Self::Slider(d) => d.changed,
Self::RangeSlider(d) => d.changed,
Self::Toggle(d) => d.changed,
Self::Checkbox(d) => d.changed,
Self::Radio(d) => d.changed,
Self::TabBar(d) => d.changed,
Self::Breadcrumb(d) => d.changed,
Self::Splitter(d) => d.changed,
Self::TextInput(d) => d.changed,
Self::Dropdown(d) => d.changed,
Self::MultiSelect(d) => d.changed,
Self::DatePicker(d) => d.changed,
Self::ColorPicker(d) => d.changed,
Self::SelectableLabel(d) => d.changed,
Self::DragValue(d) => d.changed,
Self::TreeView(d) => d.changed,
Self::TextArea(d) => d.changed,
Self::RichTextEditor(d) => d.changed,
_ => false,
}
}
pub fn child_entities(&self) -> Vec<freecs::Entity> {
match self {
Self::Button(_) => Vec::new(),
Self::Slider(data) => vec![data.fill_entity],
Self::Toggle(data) => vec![data.knob_entity],
Self::Checkbox(data) => vec![data.inner_entity],
Self::Radio(data) => vec![data.inner_entity],
Self::ProgressBar(data) => vec![data.fill_entity],
Self::CollapsingHeader(data) => vec![data.content_entity],
Self::ScrollArea(data) => {
vec![data.content_entity, data.thumb_entity, data.track_entity]
}
Self::TabBar(data) => data.tab_entities.clone(),
Self::TextInput(data) => {
let mut entities = vec![data.cursor_entity, data.selection_entity];
if let Some(placeholder) = data.placeholder_entity {
entities.push(placeholder);
}
entities
}
Self::Dropdown(data) => {
let mut entities = vec![data.popup_container_entity];
entities.extend(&data.popup_entities);
entities
}
Self::Menu(data) => {
let mut entities = vec![data.popup_container_entity];
entities.extend(&data.popup_entities);
entities
}
Self::Panel(data) => {
let mut entities = vec![data.content_entity, data.header_entity];
if let Some(collapse_btn) = data.collapse_button_entity {
entities.push(collapse_btn);
}
entities
}
Self::ColorPicker(data) => {
let mut entities = vec![data.swatch_entity];
entities.extend(data.slider_entities);
entities
}
Self::SelectableLabel(_) => Vec::new(),
Self::DragValue(data) => vec![data.cursor_entity, data.selection_entity],
Self::ContextMenu(data) => {
let mut entities = vec![data.popup_entity];
entities.extend(&data.item_entities);
entities
}
Self::TreeView(data) => {
let mut entities = vec![data.content_entity];
entities.extend(&data.node_entities);
entities
}
Self::TreeNode(data) => {
vec![
data.row_entity,
data.arrow_entity,
data.children_container,
data.wrapper_entity,
]
}
Self::ModalDialog(data) => {
let mut entities = vec![data.content_entity, data.backdrop_entity];
if let Some(ok) = data.ok_button {
entities.push(ok);
}
if let Some(cancel) = data.cancel_button {
entities.push(cancel);
}
entities
}
Self::RichText(data) => data.span_entities.clone(),
Self::DataGrid(data) => {
let mut entities = vec![
data.scroll_entity,
data.body_entity,
data.top_spacer,
data.bottom_spacer,
];
entities.extend(&data.header_entities);
entities.extend(&data.header_divider_entities);
entities.extend(&data.filter_input_entities);
entities.extend(data.filter_row_entity);
entities.extend(data.editing_input_entity);
for row in &data.pool_rows {
entities.push(row.row_entity);
entities.extend(&row.cell_entities);
}
entities
}
Self::PropertyGrid(data) => {
let mut entities = Vec::new();
entities.extend(&data.row_entities);
entities.extend(&data.label_entities);
entities
}
Self::CommandPalette(data) => {
let mut entities = vec![
data.text_input_entity,
data.backdrop_entity,
data.dialog_entity,
data.scroll_entity,
];
entities.extend(&data.result_entities);
entities
}
Self::Canvas(_) => Vec::new(),
Self::TextArea(data) => {
let mut entities = vec![data.cursor_entity];
entities.extend(&data.selection_pool);
if let Some(placeholder) = data.placeholder_entity {
entities.push(placeholder);
}
entities
}
Self::TileContainer(data) => data.collect_pane_entities(),
Self::VirtualList(data) => {
let mut entities = vec![
data.scroll_entity,
data.body_entity,
data.top_spacer,
data.bottom_spacer,
];
for item in &data.pool_items {
entities.push(item.container_entity);
}
entities
}
Self::RichTextEditor(data) => {
let mut entities = vec![data.cursor_entity];
entities.extend(&data.selection_pool);
entities.extend(data.placeholder_entity);
entities
}
Self::RangeSlider(data) => {
vec![
data.fill_entity,
data.low_thumb_entity,
data.high_thumb_entity,
]
}
Self::Breadcrumb(data) => data.segment_entities.clone(),
Self::Splitter(data) => {
vec![data.first_pane, data.second_pane, data.divider_entity]
}
Self::MultiSelect(data) => {
let mut entities = vec![data.popup_container_entity];
entities.extend(&data.popup_entities);
entities.extend(&data.check_entities);
entities
}
Self::DatePicker(data) => {
let mut entities = vec![
data.popup_entity,
data.prev_month_entity,
data.next_month_entity,
];
entities.extend(&data.day_entities);
entities
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum UiAnimationType {
Fade,
SlideLeft,
SlideRight,
SlideUp,
SlideDown,
Scale,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum UiAnimationPhase {
Idle,
IntroPlaying,
OutroPlaying,
OutroComplete,
}
#[derive(Clone, Copy, Debug)]
pub struct UiNodeAnimation {
pub intro: Option<UiAnimationType>,
pub outro: Option<UiAnimationType>,
pub duration: f32,
pub progress: f32,
pub phase: UiAnimationPhase,
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct TileId(pub usize);
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SplitDirection {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DropZone {
Left,
Right,
Top,
Bottom,
Center,
}
#[derive(Clone, Debug)]
pub struct DropPreview {
pub target_tile: TileId,
pub zone: DropZone,
}
#[derive(Clone, Debug)]
pub enum TileNode {
Pane {
content_entity: freecs::Entity,
title: String,
},
Split {
direction: SplitDirection,
ratio: f32,
children: [TileId; 2],
},
Tabs {
panes: Vec<TileId>,
active: usize,
},
}
#[derive(Clone, Debug)]
pub struct UiTileContainerData {
pub tiles: Vec<Option<TileNode>>,
pub root: TileId,
pub rects: Vec<Rect>,
pub dragging_splitter: Option<(TileId, f32)>,
pub pending_tab_drag: Option<(TileId, usize, Vec2)>,
pub dragging_tab: Option<(TileId, usize, Vec2)>,
pub drop_preview: Option<DropPreview>,
pub container_entity: freecs::Entity,
pub splitter_width: f32,
pub tab_bar_height: f32,
pub next_free: Vec<usize>,
pub hovered_close: Option<(TileId, usize)>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum TileLayoutNode {
Pane {
title: String,
},
Split {
direction: SplitDirection,
ratio: f32,
children: [TileId; 2],
},
Tabs {
panes: Vec<TileId>,
active: usize,
},
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct TileLayout {
pub nodes: Vec<Option<TileLayoutNode>>,
pub root: TileId,
}
impl UiTileContainerData {
pub fn tab_close_width(&self) -> f32 {
(self.tab_bar_height * 0.75).round()
}
pub(crate) fn alloc(&mut self, node: TileNode) -> TileId {
if let Some(index) = self.next_free.pop() {
self.tiles[index] = Some(node);
if index >= self.rects.len() {
self.rects.resize(self.tiles.len(), Rect::default());
}
TileId(index)
} else {
let index = self.tiles.len();
self.tiles.push(Some(node));
self.rects.push(Rect::default());
TileId(index)
}
}
pub(crate) fn free(&mut self, tile_id: TileId) {
self.tiles[tile_id.0] = None;
self.next_free.push(tile_id.0);
}
pub fn get(&self, tile_id: TileId) -> Option<&TileNode> {
self.tiles.get(tile_id.0).and_then(|t| t.as_ref())
}
pub fn get_mut(&mut self, tile_id: TileId) -> Option<&mut TileNode> {
self.tiles.get_mut(tile_id.0).and_then(|t| t.as_mut())
}
fn collect_pane_entities(&self) -> Vec<freecs::Entity> {
let mut entities = Vec::new();
for tile in &self.tiles {
if let Some(TileNode::Pane { content_entity, .. }) = tile {
entities.push(*content_entity);
}
}
entities
}
pub fn find_parent_tabs(&self, pane_id: TileId) -> Option<TileId> {
for (index, tile) in self.tiles.iter().enumerate() {
if let Some(TileNode::Tabs { panes, .. }) = tile
&& panes.contains(&pane_id)
{
return Some(TileId(index));
}
}
None
}
pub(crate) fn find_parent_split(&self, child_id: TileId) -> Option<(TileId, usize)> {
for (index, tile) in self.tiles.iter().enumerate() {
if let Some(TileNode::Split { children, .. }) = tile {
if children[0] == child_id {
return Some((TileId(index), 0));
}
if children[1] == child_id {
return Some((TileId(index), 1));
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_span_new_defaults() {
let span = TextSpan::new("hello");
assert_eq!(span.text, "hello");
assert!(span.color.is_none());
assert!(span.font_size_override.is_none());
assert!(!span.bold);
assert!(!span.italic);
assert!(!span.underline);
assert!(span.font_index.is_none());
}
#[test]
fn text_span_colored() {
let color = Vec4::new(1.0, 0.0, 0.0, 1.0);
let span = TextSpan::colored("red", color);
assert_eq!(span.text, "red");
assert_eq!(span.color, Some(color));
}
#[test]
fn text_span_builder_chain() {
let span = TextSpan::new("styled")
.with_bold()
.with_italic()
.with_underline()
.with_font(2)
.with_color(Vec4::new(0.0, 1.0, 0.0, 1.0))
.with_size(24.0);
assert!(span.bold);
assert!(span.italic);
assert!(span.underline);
assert_eq!(span.font_index, Some(2));
assert_eq!(span.color, Some(Vec4::new(0.0, 1.0, 0.0, 1.0)));
assert_eq!(span.font_size_override, Some(24.0));
}
#[test]
fn data_grid_column_defaults() {
let col = DataGridColumn::new("Name", 100.0);
assert_eq!(col.label, "Name");
assert_eq!(col.width, 100.0);
assert!(!col.sortable);
}
#[test]
fn data_grid_column_sortable() {
let col = DataGridColumn::new("Name", 100.0).sortable();
assert!(col.sortable);
}
#[test]
fn data_grid_column_non_sortable() {
let col = DataGridColumn {
label: "Actions".to_string(),
width: 80.0,
sortable: false,
alignment: TextAlignment::Left,
editable: false,
};
assert!(!col.sortable);
}
#[test]
fn context_menu_item_separator() {
let item = ContextMenuItem {
label: String::new(),
shortcut: None,
separator: true,
};
assert!(item.separator);
assert!(item.label.is_empty());
assert!(item.shortcut.is_none());
}
#[test]
fn selected_rows_hashset_operations() {
let mut selected: HashSet<usize> = HashSet::new();
selected.insert(5);
selected.insert(10);
selected.insert(5);
assert_eq!(selected.len(), 2);
assert!(selected.contains(&5));
assert!(selected.contains(&10));
selected.remove(&5);
assert_eq!(selected.len(), 1);
assert!(!selected.contains(&5));
}
}