use std::collections::HashMap;
use nalgebra_glm::{Vec2, Vec4};
use crate::ecs::ui::components::{DragPayload, TileId};
use crate::ecs::text::components::{TextMesh, TextProperties};
use crate::prelude::*;
#[derive(Clone, Debug)]
pub struct ActiveDrag {
pub source: freecs::Entity,
pub payload: DragPayload,
pub start_pos: Vec2,
pub current_pos: Vec2,
}
use crate::ecs::ui::theme::ThemeState;
use crate::ecs::ui::types::{Rect, UiTextInstance};
use crate::render::wgpu::passes::geometry::{UiImage, UiLayer, UiRect};
#[derive(Clone)]
pub struct CachedTextMesh {
pub mesh: TextMesh,
pub slot_generation: u64,
pub font_size: f32,
pub wrap_width: Option<f32>,
pub monospace_width: Option<f32>,
}
#[derive(Clone, Debug)]
pub enum UiEvent {
ButtonClicked(freecs::Entity),
SliderChanged {
entity: freecs::Entity,
value: f32,
},
ToggleChanged {
entity: freecs::Entity,
value: bool,
},
CheckboxChanged {
entity: freecs::Entity,
value: bool,
},
RadioChanged {
entity: freecs::Entity,
group_id: u32,
option_index: usize,
},
TabChanged {
entity: freecs::Entity,
tab_index: usize,
},
TextInputChanged {
entity: freecs::Entity,
text: String,
},
TextInputSubmitted {
entity: freecs::Entity,
text: String,
},
DropdownChanged {
entity: freecs::Entity,
selected_index: usize,
},
MenuItemClicked {
entity: freecs::Entity,
item_index: usize,
},
ColorPickerChanged {
entity: freecs::Entity,
color: Vec4,
},
SelectableLabelClicked {
entity: freecs::Entity,
selected: bool,
},
DragValueChanged {
entity: freecs::Entity,
value: f32,
},
ContextMenuItemClicked {
entity: freecs::Entity,
item_index: usize,
tag: u32,
},
TreeNodeSelected {
tree: freecs::Entity,
node: freecs::Entity,
selected: bool,
},
TreeNodeToggled {
tree: freecs::Entity,
node: freecs::Entity,
expanded: bool,
},
TreeNodeContextMenu {
tree: freecs::Entity,
node: freecs::Entity,
position: Vec2,
},
TreeNodeExpandRequested {
tree: freecs::Entity,
node: freecs::Entity,
user_data: u64,
},
ModalClosed {
entity: freecs::Entity,
confirmed: bool,
},
CommandPaletteExecuted {
entity: freecs::Entity,
command_index: usize,
},
TileTabActivated {
container: freecs::Entity,
pane_id: TileId,
},
TileTabClosed {
container: freecs::Entity,
pane_id: TileId,
title: String,
},
TileSplitterMoved {
container: freecs::Entity,
split_id: TileId,
ratio: f32,
},
DragStarted {
source: freecs::Entity,
payload: DragPayload,
},
DragDropped {
source: freecs::Entity,
target: freecs::Entity,
payload: DragPayload,
},
DragCancelled {
source: freecs::Entity,
},
DragEnter {
target: freecs::Entity,
source: freecs::Entity,
payload: DragPayload,
},
DragOver {
target: freecs::Entity,
source: freecs::Entity,
position: Vec2,
},
DragLeave {
target: freecs::Entity,
source: freecs::Entity,
},
ShortcutTriggered {
command_index: usize,
},
CanvasClicked {
entity: freecs::Entity,
command_id: u32,
position: Vec2,
},
VirtualListItemClicked {
entity: freecs::Entity,
item_index: usize,
},
VirtualListItemRightClicked {
entity: freecs::Entity,
item_index: usize,
screen_position: Vec2,
},
TextAreaChanged {
entity: freecs::Entity,
text: String,
},
RichTextEditorChanged {
entity: freecs::Entity,
text: String,
},
DataGridFilterChanged {
entity: freecs::Entity,
filters: Vec<String>,
},
RangeSliderChanged {
entity: freecs::Entity,
low: f32,
high: f32,
},
DataGridCellEdited {
entity: freecs::Entity,
row: usize,
column: usize,
text: String,
},
BreadcrumbClicked {
entity: freecs::Entity,
segment_index: usize,
},
SplitterChanged {
entity: freecs::Entity,
ratio: f32,
},
MultiSelectChanged {
entity: freecs::Entity,
selected_indices: Vec<usize>,
},
DatePickerChanged {
entity: freecs::Entity,
year: i32,
month: u32,
day: u32,
},
}
impl UiEvent {
pub fn target_entity(&self) -> freecs::Entity {
match self {
UiEvent::ButtonClicked(entity) => *entity,
UiEvent::SliderChanged { entity, .. } => *entity,
UiEvent::ToggleChanged { entity, .. } => *entity,
UiEvent::CheckboxChanged { entity, .. } => *entity,
UiEvent::RadioChanged { entity, .. } => *entity,
UiEvent::TabChanged { entity, .. } => *entity,
UiEvent::TextInputChanged { entity, .. } => *entity,
UiEvent::TextInputSubmitted { entity, .. } => *entity,
UiEvent::DropdownChanged { entity, .. } => *entity,
UiEvent::MenuItemClicked { entity, .. } => *entity,
UiEvent::ColorPickerChanged { entity, .. } => *entity,
UiEvent::SelectableLabelClicked { entity, .. } => *entity,
UiEvent::DragValueChanged { entity, .. } => *entity,
UiEvent::ContextMenuItemClicked { entity, .. } => *entity,
UiEvent::TreeNodeSelected { node, .. } => *node,
UiEvent::TreeNodeToggled { node, .. } => *node,
UiEvent::TreeNodeContextMenu { node, .. } => *node,
UiEvent::TreeNodeExpandRequested { node, .. } => *node,
UiEvent::ModalClosed { entity, .. } => *entity,
UiEvent::CommandPaletteExecuted { entity, .. } => *entity,
UiEvent::TileTabActivated { container, .. } => *container,
UiEvent::TileTabClosed { container, .. } => *container,
UiEvent::TileSplitterMoved { container, .. } => *container,
UiEvent::DragStarted { source, .. } => *source,
UiEvent::DragDropped { target, .. } => *target,
UiEvent::DragCancelled { source, .. } => *source,
UiEvent::DragEnter { target, .. } => *target,
UiEvent::DragOver { target, .. } => *target,
UiEvent::DragLeave { target, .. } => *target,
UiEvent::ShortcutTriggered { .. } => freecs::Entity::default(),
UiEvent::CanvasClicked { entity, .. } => *entity,
UiEvent::VirtualListItemClicked { entity, .. } => *entity,
UiEvent::VirtualListItemRightClicked { entity, .. } => *entity,
UiEvent::TextAreaChanged { entity, .. } => *entity,
UiEvent::RichTextEditorChanged { entity, .. } => *entity,
UiEvent::DataGridFilterChanged { entity, .. } => *entity,
UiEvent::RangeSliderChanged { entity, .. } => *entity,
UiEvent::DataGridCellEdited { entity, .. } => *entity,
UiEvent::BreadcrumbClicked { entity, .. } => *entity,
UiEvent::SplitterChanged { entity, .. } => *entity,
UiEvent::MultiSelectChanged { entity, .. } => *entity,
UiEvent::DatePickerChanged { entity, .. } => *entity,
}
}
pub fn bubbles(&self) -> bool {
match self {
UiEvent::ButtonClicked(_)
| UiEvent::SliderChanged { .. }
| UiEvent::ToggleChanged { .. }
| UiEvent::CheckboxChanged { .. }
| UiEvent::RadioChanged { .. }
| UiEvent::TabChanged { .. }
| UiEvent::TextInputChanged { .. }
| UiEvent::TextInputSubmitted { .. }
| UiEvent::DropdownChanged { .. }
| UiEvent::MenuItemClicked { .. }
| UiEvent::ColorPickerChanged { .. }
| UiEvent::SelectableLabelClicked { .. }
| UiEvent::DragValueChanged { .. }
| UiEvent::ContextMenuItemClicked { .. }
| UiEvent::TreeNodeSelected { .. }
| UiEvent::TreeNodeToggled { .. } => true,
UiEvent::TreeNodeContextMenu { .. }
| UiEvent::TreeNodeExpandRequested { .. }
| UiEvent::ModalClosed { .. }
| UiEvent::CommandPaletteExecuted { .. }
| UiEvent::TileTabActivated { .. }
| UiEvent::TileTabClosed { .. }
| UiEvent::TileSplitterMoved { .. }
| UiEvent::DragStarted { .. }
| UiEvent::DragDropped { .. }
| UiEvent::DragCancelled { .. }
| UiEvent::DragEnter { .. }
| UiEvent::DragOver { .. }
| UiEvent::DragLeave { .. }
| UiEvent::ShortcutTriggered { .. }
| UiEvent::CanvasClicked { .. }
| UiEvent::VirtualListItemClicked { .. }
| UiEvent::VirtualListItemRightClicked { .. }
| UiEvent::DataGridFilterChanged { .. }
| UiEvent::DataGridCellEdited { .. } => false,
UiEvent::TextAreaChanged { .. }
| UiEvent::RichTextEditorChanged { .. }
| UiEvent::RangeSliderChanged { .. }
| UiEvent::BreadcrumbClicked { .. }
| UiEvent::SplitterChanged { .. }
| UiEvent::MultiSelectChanged { .. }
| UiEvent::DatePickerChanged { .. } => true,
}
}
}
#[derive(Default, Debug)]
pub struct UiImageLayerAllocator {
pub next_layer: u32,
pub free_layers: Vec<u32>,
}
impl UiImageLayerAllocator {
pub const MAX_LAYERS: u32 = crate::render::wgpu::ui_texture_array::UI_TEXTURE_MAX_LAYERS;
pub fn allocate(&mut self) -> Option<u32> {
if let Some(layer) = self.free_layers.pop() {
return Some(layer);
}
if self.next_layer >= Self::MAX_LAYERS {
return None;
}
let layer = self.next_layer;
self.next_layer += 1;
Some(layer)
}
pub fn release(&mut self, layer: u32) {
if layer >= Self::MAX_LAYERS {
return;
}
if self.free_layers.contains(&layer) {
return;
}
self.free_layers.push(layer);
}
}
#[derive(Default, Debug)]
pub struct RenderSlotAllocator {
pub rect_slots: std::collections::HashMap<freecs::Entity, u32>,
pub image_slots: std::collections::HashMap<freecs::Entity, u32>,
pub text_slots: std::collections::HashMap<freecs::Entity, u32>,
pub free_rect_slots: Vec<u32>,
pub free_image_slots: Vec<u32>,
pub free_text_slots: Vec<u32>,
pub next_rect_slot: u32,
pub next_image_slot: u32,
pub next_text_slot: u32,
pub dirty_rect_slots: std::collections::HashSet<u32>,
pub dirty_image_slots: std::collections::HashSet<u32>,
pub dirty_text_slots: std::collections::HashSet<u32>,
}
pub fn allocate_rect_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) -> u32 {
if let Some(&slot) = allocator.rect_slots.get(&entity) {
return slot;
}
let slot = allocator.free_rect_slots.pop().unwrap_or_else(|| {
let s = allocator.next_rect_slot;
allocator.next_rect_slot += 1;
s
});
allocator.rect_slots.insert(entity, slot);
allocator.dirty_rect_slots.insert(slot);
slot
}
pub fn release_rect_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) {
if let Some(slot) = allocator.rect_slots.remove(&entity) {
allocator.free_rect_slots.push(slot);
allocator.dirty_rect_slots.remove(&slot);
}
}
pub fn mark_rect_dirty(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) {
if let Some(&slot) = allocator.rect_slots.get(&entity) {
allocator.dirty_rect_slots.insert(slot);
}
}
pub fn allocate_image_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) -> u32 {
if let Some(&slot) = allocator.image_slots.get(&entity) {
return slot;
}
let slot = allocator.free_image_slots.pop().unwrap_or_else(|| {
let s = allocator.next_image_slot;
allocator.next_image_slot += 1;
s
});
allocator.image_slots.insert(entity, slot);
allocator.dirty_image_slots.insert(slot);
slot
}
pub fn release_image_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) {
if let Some(slot) = allocator.image_slots.remove(&entity) {
allocator.free_image_slots.push(slot);
allocator.dirty_image_slots.remove(&slot);
}
}
pub fn allocate_text_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) -> u32 {
if let Some(&slot) = allocator.text_slots.get(&entity) {
return slot;
}
let slot = allocator.free_text_slots.pop().unwrap_or_else(|| {
let s = allocator.next_text_slot;
allocator.next_text_slot += 1;
s
});
allocator.text_slots.insert(entity, slot);
allocator.dirty_text_slots.insert(slot);
slot
}
pub fn release_text_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) {
if let Some(slot) = allocator.text_slots.remove(&entity) {
allocator.free_text_slots.push(slot);
allocator.dirty_text_slots.remove(&slot);
}
}
pub fn clear_render_slot_dirty(allocator: &mut RenderSlotAllocator) {
allocator.dirty_rect_slots.clear();
allocator.dirty_image_slots.clear();
allocator.dirty_text_slots.clear();
}
#[derive(Default)]
pub struct RetainedUiInputMirror {
pub frame_chars: Vec<char>,
pub frame_keys: Vec<(winit::keyboard::KeyCode, bool)>,
pub ctrl_held: bool,
pub shift_held: bool,
pub scroll_delta: Vec2,
}
#[derive(Default, Debug, Clone, Copy)]
pub struct RetainedUiTiming {
pub current_time: f64,
pub delta_time: f32,
}
#[derive(Default)]
pub struct RetainedUiDirty {
pub layout_dirty: bool,
pub render_dirty: bool,
pub global_version: u64,
pub last_compute_viewport: Option<(u32, u32)>,
pub last_compute_dpi: f32,
pub last_compute_text_generation: u64,
pub cached_node_rect_count: usize,
pub cached_node_image_count: usize,
pub cached_node_text_count: usize,
pub node_versions: std::collections::HashMap<freecs::Entity, u64>,
}
#[derive(Default)]
pub struct RetainedUiTextCache {
pub mesh: Vec<Option<CachedTextMesh>>,
pub mesh_dpi: f32,
pub character_colors: HashMap<usize, Vec<Option<Vec4>>>,
pub character_background_colors: HashMap<usize, Vec<Option<Vec4>>>,
}
#[derive(Default, Debug)]
pub struct RetainedUiDragDrop {
pub active: Option<ActiveDrag>,
pub over_entity: Option<freecs::Entity>,
}
#[derive(Default)]
pub struct RetainedUiAccessibility {
pub reduced_motion: bool,
pub announce_queue: Vec<String>,
pub test_id_map: HashMap<String, freecs::Entity>,
pub named_entities: HashMap<String, freecs::Entity>,
}
#[derive(Default)]
pub struct RetainedUiGroups {
pub radio: HashMap<u32, Vec<freecs::Entity>>,
pub selectable_labels: HashMap<u32, Vec<freecs::Entity>>,
pub shortcuts: Vec<(crate::ecs::ui::components::ShortcutBinding, usize)>,
}
#[derive(Default, Debug)]
pub struct RetainedUiInteraction {
pub focused_entity: Option<freecs::Entity>,
pub hovered_entity: Option<freecs::Entity>,
pub active_entity: Option<freecs::Entity>,
pub requested_cursor: Option<winit::window::CursorIcon>,
pub applied_cursor: Option<winit::window::CursorIcon>,
pub last_click: Option<(freecs::Entity, f64, Vec2)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NavDirection {
Up,
Down,
Left,
Right,
}
#[derive(Debug)]
pub struct RetainedUiGamepadNav {
pub held_direction: Option<NavDirection>,
pub next_repeat_at: f64,
pub deadzone: f32,
pub initial_repeat_delay: f64,
pub repeat_interval: f64,
pub enabled: bool,
}
impl Default for RetainedUiGamepadNav {
fn default() -> Self {
Self {
held_direction: None,
next_repeat_at: 0.0,
deadzone: 0.5,
initial_repeat_delay: 0.4,
repeat_interval: 0.12,
enabled: true,
}
}
}
#[derive(Default, Debug)]
pub struct RetainedUiOverlays {
pub tooltip_state: Option<TooltipActiveState>,
pub popup_entities: Vec<freecs::Entity>,
pub toast_container: Option<freecs::Entity>,
pub toast_entries: Vec<ToastEntry>,
pub toast_corner: ToastCorner,
pub dock_indicator_active: bool,
pub dock_indicator_panel: Option<freecs::Entity>,
pub active_context_menu: Option<freecs::Entity>,
pub active_modal: Option<freecs::Entity>,
pub focus_ring_visible: bool,
}
#[derive(Default)]
pub struct RetainedUiFrame {
pub rects: Vec<UiRect>,
pub rect_entities: Vec<Option<freecs::Entity>>,
pub ui_images: Vec<UiImage>,
pub text_meshes: Vec<UiTextInstance>,
pub overlay_rects: Vec<UiRect>,
pub overlay_text: Vec<OverlayText>,
pub events: Vec<UiEvent>,
pub bubbled_events: Vec<BubbledUiEvent>,
}
#[derive(Clone, Debug)]
pub struct TopProgressBar {
pub value: Option<f32>,
pub indeterminate: bool,
pub phase: f32,
pub height: f32,
pub color: Option<Vec4>,
}
impl Default for TopProgressBar {
fn default() -> Self {
Self {
value: None,
indeterminate: false,
phase: 0.0,
height: 2.0,
color: None,
}
}
}
impl TopProgressBar {
pub fn set(&mut self, value: f32) {
self.value = Some(value.clamp(0.0, 1.0));
self.indeterminate = false;
}
pub fn set_indeterminate(&mut self) {
self.value = None;
self.indeterminate = true;
}
pub fn clear(&mut self) {
self.value = None;
self.indeterminate = false;
}
pub fn is_active(&self) -> bool {
self.value.is_some() || self.indeterminate
}
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug)]
pub enum UiBreakpoint {
Compact,
Medium,
#[default]
Wide,
}
impl UiBreakpoint {
pub fn from_width(width: f32) -> Self {
if width < 720.0 {
UiBreakpoint::Compact
} else if width < 1024.0 {
UiBreakpoint::Medium
} else {
UiBreakpoint::Wide
}
}
pub fn is_compact(self) -> bool {
matches!(self, UiBreakpoint::Compact)
}
pub fn is_medium_or_narrower(self) -> bool {
matches!(self, UiBreakpoint::Compact | UiBreakpoint::Medium)
}
}
#[derive(Default, Clone, Copy, Debug)]
pub struct UiViewport {
pub size: Vec2,
pub breakpoint: UiBreakpoint,
}
#[derive(Default)]
pub struct RetainedUiState {
pub enabled: bool,
pub theme_state: ThemeState,
pub interaction: RetainedUiInteraction,
pub overlays: RetainedUiOverlays,
pub timing: RetainedUiTiming,
pub dirty: RetainedUiDirty,
pub text_cache: RetainedUiTextCache,
pub drag: RetainedUiDragDrop,
pub accessibility: RetainedUiAccessibility,
pub groups: RetainedUiGroups,
pub z_sorted_nodes: Vec<(freecs::Entity, f32)>,
pub frame: RetainedUiFrame,
pub layout_graph: crate::ecs::ui::layout_graph::LayoutGraph,
pub taffy: crate::ecs::ui::taffy_layout::UiTaffy,
pub render_slots: RenderSlotAllocator,
pub input: RetainedUiInputMirror,
pub background_color: Option<Vec4>,
pub top_progress_bar: TopProgressBar,
pub icon_set: crate::ecs::ui::icons::IconSet,
pub dock_state: DockState,
pub gamepad_nav: RetainedUiGamepadNav,
pub next_focus_order: i32,
pub viewport: UiViewport,
pub picking_grid: Option<crate::ecs::ui::picking::PickingGrid>,
pub custom_state_count: usize,
pub clipboard_text: String,
pub window_chrome: WindowChrome,
}
#[derive(Default, Clone, Debug)]
pub struct WindowChrome {
pub menu_bar: Option<freecs::Entity>,
pub toolbar: Option<freecs::Entity>,
pub status_bar: Option<freecs::Entity>,
pub shortcuts: Vec<WindowShortcut>,
}
#[derive(Clone, Debug)]
pub struct WindowShortcut {
pub binding: crate::ecs::ui::components::ShortcutBinding,
pub command_index: usize,
}
#[derive(Clone, Debug)]
pub struct BubbledUiEvent {
pub event: UiEvent,
pub target: freecs::Entity,
pub ancestor: freecs::Entity,
pub stopped: bool,
}
impl BubbledUiEvent {
pub fn stop_propagation(&mut self) {
self.stopped = true;
}
pub fn stopped(&self) -> bool {
self.stopped
}
}
#[derive(Clone, Debug)]
pub struct TooltipActiveState {
pub entity: freecs::Entity,
pub hover_start: f64,
pub text: String,
pub tooltip_entity: Option<freecs::Entity>,
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ReservedAreas {
pub left: f32,
pub right: f32,
pub top: f32,
pub bottom: f32,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PanelDockConfig {
pub id: String,
pub title: String,
pub kind: crate::ecs::ui::components::UiPanelKind,
pub focus_order: i32,
pub size: f32,
pub visible: bool,
pub collapsed: bool,
pub floating_position: Option<[f32; 2]>,
pub floating_size: Option<[f32; 2]>,
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct WindowDockLayout {
pub reserved_areas: ReservedAreas,
pub panels: Vec<PanelDockConfig>,
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct DockState {
pub primary: WindowDockLayout,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ToastSeverity {
Info,
Success,
Warning,
Error,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ToastCorner {
TopLeft,
TopRight,
BottomLeft,
#[default]
BottomRight,
}
#[derive(Clone, Debug)]
pub struct ToastEntry {
pub entity: freecs::Entity,
pub spawn_time: f64,
pub duration: f32,
pub dismissing: bool,
}
pub struct DrawRectParams {
pub position: Vec2,
pub size: Vec2,
pub color: Vec4,
pub corner_radius: f32,
pub border_width: f32,
pub border_color: Vec4,
pub layer: UiLayer,
pub z_index: i32,
}
pub struct OverlayText {
pub text: String,
pub position: Vec2,
pub properties: TextProperties,
pub clip_rect: Option<Rect>,
pub layer: UiLayer,
pub z_index: i32,
}
impl RetainedUiState {
pub fn draw_overlay_rect(&mut self, rect: UiRect) {
self.frame.overlay_rects.push(rect);
}
pub fn draw_overlay_text(
&mut self,
text: &str,
position: Vec2,
properties: TextProperties,
clip_rect: Option<Rect>,
layer: UiLayer,
z_index: i32,
) {
self.frame.overlay_text.push(OverlayText {
text: text.to_string(),
position,
properties,
clip_rect,
layer,
z_index,
});
}
pub fn register_custom_state(&mut self) -> usize {
let index = crate::ecs::ui::state::STATE_COUNT + self.custom_state_count;
self.custom_state_count += 1;
index
}
pub fn total_state_count(&self) -> usize {
crate::ecs::ui::state::STATE_COUNT + self.custom_state_count
}
pub fn interaction_for_active(&self) -> &RetainedUiInteraction {
&self.interaction
}
pub fn interaction_for_active_mut(&mut self) -> &mut RetainedUiInteraction {
&mut self.interaction
}
pub fn events_for_active(&self) -> &[UiEvent] {
&self.frame.events
}
pub fn events_for_active_mut(&mut self) -> &mut Vec<UiEvent> {
&mut self.frame.events
}
pub fn draw_rect(&mut self, position: Vec2, size: Vec2, color: Vec4) {
self.frame.rects.push(UiRect {
position,
size,
color,
corner_radius: 0.0,
border_width: 0.0,
border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
rotation: 0.0,
clip_rect: None,
layer: UiLayer::FloatingPanels,
z_index: 0,
shadow: None,
effect_kind: 0,
effect_params: [0.0; 4],
quad_corners: None,
});
}
pub fn draw_rect_styled(&mut self, params: DrawRectParams) {
self.frame.rects.push(UiRect {
position: params.position,
size: params.size,
color: params.color,
corner_radius: params.corner_radius,
border_width: params.border_width,
border_color: params.border_color,
rotation: 0.0,
clip_rect: None,
layer: params.layer,
z_index: params.z_index,
shadow: None,
effect_kind: 0,
effect_params: [0.0; 4],
quad_corners: None,
});
}
}
pub fn blend_color(from: Vec4, to: Vec4, t: f32) -> Vec4 {
if t <= 0.0 {
return from;
}
if t >= 1.0 {
return to;
}
let from_hsla = rgba_to_hsla(from);
let to_hsla = rgba_to_hsla(to);
let mut h_diff = to_hsla.x - from_hsla.x;
if h_diff > 0.5 {
h_diff -= 1.0;
} else if h_diff < -0.5 {
h_diff += 1.0;
}
let h = (from_hsla.x + h_diff * t).rem_euclid(1.0);
let s = from_hsla.y + (to_hsla.y - from_hsla.y) * t;
let l = from_hsla.z + (to_hsla.z - from_hsla.z) * t;
let a = from_hsla.w + (to_hsla.w - from_hsla.w) * t;
hsla_to_rgba(Vec4::new(h, s, l, a))
}
fn rgba_to_hsla(color: Vec4) -> Vec4 {
let r = color.x;
let g = color.y;
let b = color.z;
let a = color.w;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let l = (max + min) * 0.5;
if (max - min).abs() < f32::EPSILON {
return Vec4::new(0.0, 0.0, l, a);
}
let d = max - min;
let s = if l > 0.5 {
d / (2.0 - max - min)
} else {
d / (max + min)
};
let h = if (max - r).abs() < f32::EPSILON {
let mut h = (g - b) / d;
if g < b {
h += 6.0;
}
h / 6.0
} else if (max - g).abs() < f32::EPSILON {
((b - r) / d + 2.0) / 6.0
} else {
((r - g) / d + 4.0) / 6.0
};
Vec4::new(h, s, l, a)
}
fn hsla_to_rgba(hsla: Vec4) -> Vec4 {
let h = hsla.x;
let s = hsla.y;
let l = hsla.z;
let a = hsla.w;
if s.abs() < f32::EPSILON {
return Vec4::new(l, l, l, a);
}
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - l * s
};
let p = 2.0 * l - q;
let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
let g = hue_to_rgb(p, q, h);
let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
Vec4::new(r, g, b, a)
}
fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
if t < 1.0 / 6.0 {
return p + (q - p) * 6.0 * t;
}
if t < 0.5 {
return q;
}
if t < 2.0 / 3.0 {
return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
}
p
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_entity(id: u32) -> freecs::Entity {
freecs::Entity { id, generation: 0 }
}
#[test]
fn button_clicked_bubbles() {
assert!(UiEvent::ButtonClicked(dummy_entity(0)).bubbles());
}
#[test]
fn slider_changed_bubbles() {
assert!(
UiEvent::SliderChanged {
entity: dummy_entity(0),
value: 0.0
}
.bubbles()
);
}
#[test]
fn tree_node_context_menu_does_not_bubble() {
assert!(
!UiEvent::TreeNodeContextMenu {
tree: dummy_entity(0),
node: dummy_entity(1),
position: Vec2::zeros(),
}
.bubbles()
);
}
#[test]
fn modal_closed_does_not_bubble() {
assert!(
!UiEvent::ModalClosed {
entity: dummy_entity(0),
confirmed: true,
}
.bubbles()
);
}
#[test]
fn target_entity_button() {
let entity = dummy_entity(42);
assert_eq!(UiEvent::ButtonClicked(entity).target_entity(), entity);
}
#[test]
fn target_entity_tree_node_selected() {
let tree = dummy_entity(1);
let node = dummy_entity(2);
assert_eq!(
UiEvent::TreeNodeSelected {
tree,
node,
selected: true
}
.target_entity(),
node
);
}
#[test]
fn target_entity_modal_closed() {
let entity = dummy_entity(5);
assert_eq!(
UiEvent::ModalClosed {
entity,
confirmed: false
}
.target_entity(),
entity
);
}
#[test]
fn all_bubbling_events_bubble() {
let entity = dummy_entity(0);
let bubbling_events = [
UiEvent::ButtonClicked(entity),
UiEvent::SliderChanged { entity, value: 0.0 },
UiEvent::ToggleChanged {
entity,
value: false,
},
UiEvent::CheckboxChanged {
entity,
value: false,
},
UiEvent::RadioChanged {
entity,
group_id: 0,
option_index: 0,
},
UiEvent::TabChanged {
entity,
tab_index: 0,
},
UiEvent::TextInputChanged {
entity,
text: String::new(),
},
UiEvent::TextInputSubmitted {
entity,
text: String::new(),
},
UiEvent::DropdownChanged {
entity,
selected_index: 0,
},
UiEvent::MenuItemClicked {
entity,
item_index: 0,
},
UiEvent::ColorPickerChanged {
entity,
color: Vec4::zeros(),
},
UiEvent::SelectableLabelClicked {
entity,
selected: false,
},
UiEvent::DragValueChanged { entity, value: 0.0 },
UiEvent::ContextMenuItemClicked {
entity,
item_index: 0,
tag: 0,
},
UiEvent::TreeNodeSelected {
tree: entity,
node: entity,
selected: false,
},
UiEvent::TreeNodeToggled {
tree: entity,
node: entity,
expanded: false,
},
];
for event in &bubbling_events {
assert!(event.bubbles(), "Expected {:?} to bubble", event);
}
}
#[test]
fn all_non_bubbling_events_do_not_bubble() {
let entity = dummy_entity(0);
let non_bubbling_events = [
UiEvent::TreeNodeContextMenu {
tree: entity,
node: entity,
position: Vec2::zeros(),
},
UiEvent::ModalClosed {
entity,
confirmed: false,
},
UiEvent::CommandPaletteExecuted {
entity,
command_index: 0,
},
UiEvent::TileTabActivated {
container: entity,
pane_id: TileId(0),
},
UiEvent::TileTabClosed {
container: entity,
pane_id: TileId(0),
title: String::new(),
},
UiEvent::TileSplitterMoved {
container: entity,
split_id: TileId(0),
ratio: 0.5,
},
];
for event in &non_bubbling_events {
assert!(!event.bubbles(), "Expected {:?} not to bubble", event);
}
}
}