#![warn(missing_docs)]
use crate::message::DeliveryMode;
use crate::{
border::BorderBuilder,
brush::Brush,
button::{Button, ButtonBuilder, ButtonMessage},
core::{
algebra::Vector2, color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
uuid_provider, visitor::prelude::*,
},
decorator::{Decorator, DecoratorBuilder, DecoratorMessage},
grid::{Column, Grid, GridBuilder, Row},
message::{ButtonState, MessageData, MouseButton, UiMessage},
style::{resource::StyleResourceExt, Style, StyledProperty},
utils::make_cross_primitive,
vector_image::VectorImageBuilder,
widget::{Widget, WidgetBuilder, WidgetMessage},
wrap_panel::{WrapPanel, WrapPanelBuilder},
BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
VerticalAlignment,
};
use fyrox_core::variable::InheritableVariable;
use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
use std::{
any::Any,
cmp::Ordering,
fmt::{Debug, Formatter},
sync::Arc,
};
#[derive(Debug, Clone, PartialEq)]
pub enum TabControlMessage {
ActiveTab(Option<Uuid>),
CloseTab(Uuid),
RemoveTab(Uuid),
AddTab(TabDefinition),
}
impl MessageData for TabControlMessage {}
#[derive(Clone)]
pub struct TabUserData(pub Arc<dyn Any + Send + Sync>);
impl TabUserData {
pub fn new<T>(data: T) -> Self
where
T: Any + Send + Sync,
{
Self(Arc::new(data))
}
}
impl PartialEq for TabUserData {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(
(&*self.0) as *const _ as *const (),
(&*other.0) as *const _ as *const (),
)
}
}
impl Debug for TabUserData {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "User-defined data")
}
}
#[derive(Default, Clone, PartialEq, Visit, Reflect, Debug)]
pub struct Tab {
pub uuid: Uuid,
pub header_button: Handle<Button>,
pub content: Handle<UiNode>,
pub close_button: Handle<Button>,
pub header_container: Handle<UiNode>,
#[visit(skip)]
#[reflect(hidden)]
pub user_data: Option<TabUserData>,
pub decorator: Handle<Decorator>,
pub header_content: Handle<UiNode>,
}
#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
#[reflect(derived_type = "UiNode")]
pub struct TabControl {
pub widget: Widget,
pub is_tab_drag_allowed: bool,
pub tabs: Vec<Tab>,
pub active_tab: Option<usize>,
pub content_container: Handle<Grid>,
pub headers_container: Handle<WrapPanel>,
pub active_tab_brush: InheritableVariable<StyledProperty<Brush>>,
}
impl ConstructorProvider<UiNode, UserInterface> for TabControl {
fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
GraphNodeConstructor::new::<Self>()
.with_variant("Tab Control", |ui| {
TabControlBuilder::new(WidgetBuilder::new().with_name("Tab Control"))
.build(&mut ui.build_ctx())
.to_base()
.into()
})
.with_group("Layout")
}
}
crate::define_widget_deref!(TabControl);
uuid_provider!(TabControl = "d54cfac3-0afc-464b-838a-158b3a2253f5");
impl TabControl {
fn do_drag(&mut self, position: Vector2<f32>, ui: &mut UserInterface) {
let mut dragged_index = None;
let mut target_index = None;
for (tab_index, tab) in self.tabs.iter().enumerate() {
let bounds = ui[tab.header_button].screen_bounds();
let node_x = bounds.center().x;
if bounds.contains(position) {
if node_x < position.x {
target_index = Some(tab_index + 1);
} else {
target_index = Some(tab_index);
}
}
if ui.is_node_child_of(ui.captured_node, tab.header_button) {
dragged_index = Some(tab_index);
}
}
if let (Some(dragged_index), Some(mut target_index)) = (dragged_index, target_index) {
if dragged_index < target_index {
target_index -= 1;
}
if target_index != dragged_index {
self.finalize_drag(dragged_index, target_index, ui);
}
}
}
fn finalize_drag(&mut self, from: usize, to: usize, ui: &mut UserInterface) {
let uuid = self.active_tab.map(|i| self.tabs[i].uuid);
let tab = self.tabs.remove(from);
self.tabs.insert(to, tab);
if let Some(uuid) = uuid {
self.active_tab = self.tabs.iter().position(|t| t.uuid == uuid);
}
let new_tab_handles = self.tabs.iter().map(|t| t.header_container).collect();
ui.send(
self.headers_container,
WidgetMessage::ReplaceChildren(new_tab_handles),
);
}
pub fn get_tab_by_uuid(&self, uuid: Uuid) -> Option<&Tab> {
self.tabs.iter().find(|t| t.uuid == uuid)
}
fn set_active_tab(
&mut self,
active_tab: Option<usize>,
ui: &mut UserInterface,
flags: u64,
delivery_mode: DeliveryMode,
) {
if let Some(index) = active_tab {
if self.tabs.len() <= index {
return;
}
}
for (existing_tab_index, tab) in self.tabs.iter().enumerate() {
ui.send(
tab.content,
WidgetMessage::Visibility(active_tab == Some(existing_tab_index)),
);
ui.send(
tab.decorator,
DecoratorMessage::Select(active_tab == Some(existing_tab_index)),
)
}
self.active_tab = active_tab;
let tab_id = active_tab.and_then(|i| self.tabs.get(i)).map(|t| t.uuid);
let mut msg = UiMessage::from_widget(self.handle, TabControlMessage::ActiveTab(tab_id));
msg.flags = flags;
msg.delivery_mode = delivery_mode;
ui.send_message(msg);
}
fn remove_tab(&mut self, index: usize, ui: &mut UserInterface) -> bool {
let Some(tab) = self.tabs.get(index) else {
return false;
};
ui.send(tab.header_container, WidgetMessage::Remove);
ui.send(tab.content, WidgetMessage::Remove);
self.tabs.remove(index);
if let Some(active_tab) = &self.active_tab {
match index.cmp(active_tab) {
Ordering::Less => self.active_tab = Some(active_tab - 1), Ordering::Equal => {
if self.tabs.is_empty() {
self.set_active_tab(None, ui, 0, DeliveryMode::FullCycle);
} else if *active_tab == 0 {
self.set_active_tab(Some(0), ui, 0, DeliveryMode::FullCycle);
} else {
self.set_active_tab(Some(active_tab - 1), ui, 0, DeliveryMode::FullCycle);
}
}
Ordering::Greater => (), }
}
true
}
}
impl Control for TabControl {
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.widget.handle_routed_message(ui, message);
if let Some(ButtonMessage::Click) = message.data() {
for tab in self.tabs.iter() {
if message.destination() == tab.header_button && tab.header_button.is_some() {
ui.send(self.handle, TabControlMessage::ActiveTab(Some(tab.uuid)));
break;
} else if message.destination() == tab.close_button {
ui.post(self.handle, TabControlMessage::CloseTab(tab.uuid));
}
}
} else if let Some(WidgetMessage::MouseDown { button, .. }) = message.data() {
if *button == MouseButton::Middle {
for tab in self.tabs.iter() {
if ui.is_node_child_of(message.destination(), tab.header_button) {
ui.post(self.handle, TabControlMessage::CloseTab(tab.uuid));
}
}
}
} else if let Some(WidgetMessage::MouseMove { pos, state }) = message.data() {
if state.left == ButtonState::Pressed
&& self.is_tab_drag_allowed
&& ui.is_node_child_of(ui.captured_node, self.headers_container)
{
self.do_drag(*pos, ui);
}
} else if let Some(msg) = message.data_for::<TabControlMessage>(self.handle()) {
match msg {
TabControlMessage::ActiveTab(uuid) => match uuid {
Some(uuid) => {
if let Some(active_tab) = self.tabs.iter().position(|t| t.uuid == *uuid) {
if self.active_tab != Some(active_tab) {
self.set_active_tab(
Some(active_tab),
ui,
message.flags,
message.delivery_mode,
);
}
}
}
None if self.active_tab.is_some() => {
self.set_active_tab(None, ui, message.flags, message.delivery_mode)
}
_ => (),
},
TabControlMessage::CloseTab(_) => {
}
TabControlMessage::RemoveTab(uuid) => {
let index = self.tabs.iter().position(|t| t.uuid == *uuid);
if let Some(index) = index {
if self.remove_tab(index, ui) {
ui.try_send_response(message);
}
}
}
TabControlMessage::AddTab(definition) => {
if self.tabs.iter().any(|t| t.uuid == definition.uuid) {
ui.send(definition.header, WidgetMessage::Remove);
ui.send(definition.content, WidgetMessage::Remove);
return;
}
let header = Header::build(
definition,
false,
(*self.active_tab_brush).clone(),
&mut ui.build_ctx(),
);
ui.send(
header.button,
WidgetMessage::link_with(self.headers_container),
);
ui.send(
definition.content,
WidgetMessage::link_with(self.content_container),
);
ui.try_send_response(message);
self.tabs.push(Tab {
uuid: definition.uuid,
header_button: header.button,
content: definition.content,
close_button: header.close_button,
header_container: header.button.to_base(),
user_data: definition.user_data.clone(),
decorator: header.decorator,
header_content: header.content,
});
}
}
}
}
}
pub struct TabControlBuilder {
widget_builder: WidgetBuilder,
is_tab_drag_allowed: bool,
tabs: Vec<TabDefinition>,
active_tab_brush: Option<StyledProperty<Brush>>,
initial_tab: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TabDefinition {
pub uuid: Uuid,
pub header: Handle<UiNode>,
pub content: Handle<UiNode>,
pub can_be_closed: bool,
pub user_data: Option<TabUserData>,
}
struct Header {
button: Handle<Button>,
close_button: Handle<Button>,
decorator: Handle<Decorator>,
content: Handle<UiNode>,
}
impl Header {
fn build(
tab_definition: &TabDefinition,
selected: bool,
active_tab_brush: StyledProperty<Brush>,
ctx: &mut BuildContext,
) -> Self {
let close_button;
let decorator;
let button = ButtonBuilder::new(WidgetBuilder::new().on_row(0).on_column(0))
.with_back({
decorator = DecoratorBuilder::new(
BorderBuilder::new(WidgetBuilder::new().with_margin(Thickness {
left: 0.0,
top: 2.0,
right: 2.0,
bottom: 2.0,
}))
.with_stroke_thickness(Thickness::uniform(0.0).into())
.with_pad_by_corner_radius(false)
.with_corner_radius(4.0.into()),
)
.with_normal_brush(ctx.style.property(Style::BRUSH_DARK))
.with_selected_brush(active_tab_brush)
.with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
.with_hover_brush(ctx.style.property(Style::BRUSH_LIGHT))
.with_selected(selected)
.build(ctx);
decorator
})
.with_content(
GridBuilder::new(
WidgetBuilder::new()
.with_child(tab_definition.header)
.with_child({
close_button = if tab_definition.can_be_closed {
ButtonBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::right(1.0))
.on_row(0)
.on_column(1)
.with_width(16.0)
.with_height(16.0),
)
.with_back(
DecoratorBuilder::new(
BorderBuilder::new(WidgetBuilder::new())
.with_corner_radius(5.0f32.into())
.with_pad_by_corner_radius(false)
.with_stroke_thickness(Thickness::uniform(0.0).into()),
)
.with_normal_brush(Brush::Solid(Color::TRANSPARENT).into())
.with_hover_brush(ctx.style.property(Style::BRUSH_DARK))
.build(ctx),
)
.with_content(
VectorImageBuilder::new(
WidgetBuilder::new()
.with_horizontal_alignment(HorizontalAlignment::Center)
.with_vertical_alignment(VerticalAlignment::Center)
.with_width(8.0)
.with_height(8.0)
.with_foreground(
ctx.style.property(Style::BRUSH_BRIGHTEST),
),
)
.with_primitives(make_cross_primitive(8.0, 2.0))
.build(ctx),
)
.build(ctx)
} else {
Handle::NONE
};
close_button
}),
)
.add_row(Row::auto())
.add_column(Column::stretch())
.add_column(Column::auto())
.build(ctx),
)
.build(ctx);
Header {
button,
close_button,
decorator,
content: tab_definition.header,
}
}
}
impl TabControlBuilder {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self {
tabs: Default::default(),
is_tab_drag_allowed: false,
active_tab_brush: None,
initial_tab: 0,
widget_builder,
}
}
pub fn with_initial_tab(mut self, tab_index: usize) -> Self {
self.initial_tab = tab_index;
self
}
pub fn with_tab_drag(mut self, is_tab_drag_allowed: bool) -> Self {
self.is_tab_drag_allowed = is_tab_drag_allowed;
self
}
pub fn with_tab(mut self, tab: TabDefinition) -> Self {
self.tabs.push(tab);
self
}
pub fn with_active_tab_brush(mut self, brush: StyledProperty<Brush>) -> Self {
self.active_tab_brush = Some(brush);
self
}
pub fn build(self, ctx: &mut BuildContext) -> Handle<TabControl> {
let tab_count = self.tabs.len();
for (i, tab) in self.tabs.iter().enumerate() {
if let Ok(content) = ctx.try_get_node_mut(tab.content) {
content.set_visibility(i == self.initial_tab);
}
}
let active_tab_brush = self
.active_tab_brush
.unwrap_or_else(|| ctx.style.property::<Brush>(Style::BRUSH_BRIGHT_BLUE));
let tab_headers = self
.tabs
.iter()
.enumerate()
.map(|(i, tab_definition)| {
Header::build(
tab_definition,
i == self.initial_tab,
active_tab_brush.clone(),
ctx,
)
})
.collect::<Vec<_>>();
let headers_container = WrapPanelBuilder::new(
WidgetBuilder::new()
.with_children(tab_headers.iter().map(|h| h.button.to_base()))
.on_row(0),
)
.with_orientation(Orientation::Horizontal)
.build(ctx);
let content_container = GridBuilder::new(
WidgetBuilder::new()
.with_children(self.tabs.iter().map(|t| t.content))
.on_row(1),
)
.add_row(Row::stretch())
.add_column(Column::stretch())
.build(ctx);
let grid = GridBuilder::new(
WidgetBuilder::new()
.with_child(headers_container)
.with_child(content_container),
)
.add_column(Column::stretch())
.add_row(Row::auto())
.add_row(Row::stretch())
.build(ctx);
let border = BorderBuilder::new(
WidgetBuilder::new()
.with_background(ctx.style.property(Style::BRUSH_DARK))
.with_child(grid),
)
.build(ctx);
let tc = TabControl {
widget: self.widget_builder.with_child(border).build(ctx),
is_tab_drag_allowed: self.is_tab_drag_allowed,
active_tab: if tab_count == 0 {
None
} else {
Some(self.initial_tab)
},
tabs: tab_headers
.into_iter()
.zip(self.tabs)
.map(|(header, tab)| Tab {
uuid: tab.uuid,
header_button: header.button,
content: tab.content,
close_button: header.close_button,
header_container: header.button.to_base(),
user_data: tab.user_data,
decorator: header.decorator,
header_content: header.content,
})
.collect(),
content_container,
headers_container,
active_tab_brush: active_tab_brush.into(),
};
ctx.add(tc)
}
}
#[cfg(test)]
mod test {
use crate::tab_control::TabControlBuilder;
use crate::{test::test_widget_deletion, widget::WidgetBuilder};
#[test]
fn test_deletion() {
test_widget_deletion(|ctx| TabControlBuilder::new(WidgetBuilder::new()).build(ctx));
}
}