#![warn(missing_docs)]
use crate::style::resource::StyleResourceExt;
use crate::style::{Style, StyledProperty};
use crate::{
border::BorderBuilder,
brush::Brush,
button::{ButtonBuilder, ButtonMessage},
core::{
color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*, uuid_provider,
visitor::prelude::*,
},
decorator::{DecoratorBuilder, DecoratorMessage},
define_constructor,
grid::{Column, GridBuilder, Row},
message::{MessageDirection, MouseButton, UiMessage},
stack_panel::StackPanelBuilder,
utils::make_cross_primitive,
vector_image::VectorImageBuilder,
widget::{Widget, WidgetBuilder, WidgetMessage},
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},
ops::{Deref, DerefMut},
sync::Arc,
};
#[derive(Debug, Clone, PartialEq)]
pub enum TabControlMessage {
ActiveTab(Option<usize>),
ActiveTabUuid(Option<Uuid>),
CloseTab(usize),
CloseTabByUuid(Uuid),
RemoveTab(usize),
RemoveTabByUuid(Uuid),
AddTab(TabDefinition),
}
impl TabControlMessage {
define_constructor!(
TabControlMessage:ActiveTab => fn active_tab(Option<usize>), layout: false
);
define_constructor!(
TabControlMessage:ActiveTabUuid => fn active_tab_uuid(Option<Uuid>), layout: false
);
define_constructor!(
TabControlMessage:CloseTab => fn close_tab(usize), layout: false
);
define_constructor!(
TabControlMessage:CloseTabByUuid => fn close_tab_by_uuid(Uuid), layout: false
);
define_constructor!(
TabControlMessage:RemoveTab => fn remove_tab(usize), layout: false
);
define_constructor!(
TabControlMessage:RemoveTabByUuid => fn remove_tab_by_uuid(Uuid), layout: false
);
define_constructor!(
TabControlMessage:AddTab => fn add_tab(TabDefinition), layout: false
);
}
#[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<UiNode>,
pub content: Handle<UiNode>,
pub close_button: Handle<UiNode>,
pub header_container: Handle<UiNode>,
#[visit(skip)]
#[reflect(hidden)]
pub user_data: Option<TabUserData>,
pub decorator: Handle<UiNode>,
pub header_content: Handle<UiNode>,
}
#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
pub struct TabControl {
pub widget: Widget,
pub tabs: Vec<Tab>,
pub active_tab: Option<usize>,
pub content_container: Handle<UiNode>,
pub headers_container: Handle<UiNode>,
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())
.into()
})
.with_group("Layout")
}
}
crate::define_widget_deref!(TabControl);
uuid_provider!(TabControl = "d54cfac3-0afc-464b-838a-158b3a2253f5");
impl TabControl {
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) {
if let Some(index) = active_tab {
if self.tabs.len() <= index {
return;
}
}
for (existing_tab_index, tab) in self.tabs.iter().enumerate() {
ui.send_message(WidgetMessage::visibility(
tab.content,
MessageDirection::ToWidget,
active_tab == Some(existing_tab_index),
));
ui.send_message(DecoratorMessage::select(
tab.decorator,
MessageDirection::ToWidget,
active_tab == Some(existing_tab_index),
))
}
self.active_tab = active_tab;
let mut msg =
TabControlMessage::active_tab(self.handle, MessageDirection::FromWidget, active_tab);
msg.flags = flags;
ui.send_message(msg);
let tab_id = active_tab.and_then(|i| self.tabs.get(i)).map(|t| t.uuid);
let mut msg =
TabControlMessage::active_tab_uuid(self.handle, MessageDirection::FromWidget, tab_id);
msg.flags = flags;
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_message(WidgetMessage::remove(
tab.header_container,
MessageDirection::ToWidget,
));
ui.send_message(WidgetMessage::remove(
tab.content,
MessageDirection::ToWidget,
));
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);
} else if *active_tab == 0 {
self.set_active_tab(Some(0), ui, 0);
} else {
self.set_active_tab(Some(active_tab - 1), ui, 0);
}
}
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_index, tab) in self.tabs.iter().enumerate() {
if message.destination() == tab.header_button && tab.header_button.is_some() {
ui.send_message(TabControlMessage::active_tab_uuid(
self.handle,
MessageDirection::ToWidget,
Some(tab.uuid),
));
break;
} else if message.destination() == tab.close_button {
ui.send_message(TabControlMessage::close_tab(
self.handle,
MessageDirection::FromWidget,
tab_index,
));
ui.send_message(TabControlMessage::close_tab_by_uuid(
self.handle,
MessageDirection::FromWidget,
tab.uuid,
));
}
}
} else if let Some(WidgetMessage::MouseDown { button, .. }) = message.data() {
if *button == MouseButton::Middle {
for (tab_index, tab) in self.tabs.iter().enumerate() {
if ui.is_node_child_of(message.destination(), tab.header_button) {
ui.send_message(TabControlMessage::close_tab(
self.handle,
MessageDirection::FromWidget,
tab_index,
));
ui.send_message(TabControlMessage::close_tab_by_uuid(
self.handle,
MessageDirection::FromWidget,
tab.uuid,
));
}
}
}
} else if let Some(msg) = message.data::<TabControlMessage>() {
if message.destination() == self.handle()
&& message.direction() == MessageDirection::ToWidget
{
match msg {
TabControlMessage::ActiveTab(active_tab) => {
if self.active_tab != *active_tab {
self.set_active_tab(*active_tab, ui, message.flags);
}
}
TabControlMessage::ActiveTabUuid(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);
}
}
}
None if self.active_tab.is_some() => {
self.set_active_tab(None, ui, message.flags)
}
_ => (),
},
TabControlMessage::CloseTab(_) | TabControlMessage::CloseTabByUuid(_) => {
}
TabControlMessage::RemoveTab(index) => {
if self.remove_tab(*index, ui) {
ui.send_message(message.reverse());
}
}
TabControlMessage::RemoveTabByUuid(uuid) => {
let index = self.tabs.iter().position(|t| t.uuid == *uuid);
if let Some(index) = index {
if self.remove_tab(index, ui) {
ui.send_message(message.reverse());
}
}
}
TabControlMessage::AddTab(definition) => {
let header = Header::build(
definition,
false,
(*self.active_tab_brush).clone(),
&mut ui.build_ctx(),
);
ui.send_message(WidgetMessage::link(
header.button,
MessageDirection::ToWidget,
self.headers_container,
));
ui.send_message(WidgetMessage::link(
definition.content,
MessageDirection::ToWidget,
self.content_container,
));
ui.send_message(message.reverse());
self.tabs.push(Tab {
uuid: Uuid::new_v4(),
header_button: header.button,
content: definition.content,
close_button: header.close_button,
header_container: header.button,
user_data: definition.user_data.clone(),
decorator: header.decorator,
header_content: header.content,
});
self.set_active_tab(Some(self.tabs.len() - 1), ui, 0);
}
}
}
}
}
}
pub struct TabControlBuilder {
widget_builder: WidgetBuilder,
tabs: Vec<TabDefinition>,
active_tab_brush: Option<StyledProperty<Brush>>,
initial_tab: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TabDefinition {
pub header: Handle<UiNode>,
pub content: Handle<UiNode>,
pub can_be_closed: bool,
pub user_data: Option<TabUserData>,
}
struct Header {
button: Handle<UiNode>,
close_button: Handle<UiNode>,
decorator: Handle<UiNode>,
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_stroke_thickness(Thickness::uniform(0.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(),
active_tab_brush: None,
initial_tab: 0,
widget_builder,
}
}
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<UiNode> {
let tab_count = self.tabs.len();
for tab in self.tabs.iter().skip(1) {
if let Some(content) = ctx.try_get_node_mut(tab.content) {
content.set_visibility(false);
}
}
let active_tab_brush = self
.active_tab_brush
.unwrap_or_else(|| ctx.style.property::<Brush>(Style::BRUSH_LIGHTEST));
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 = StackPanelBuilder::new(
WidgetBuilder::new()
.with_children(tab_headers.iter().map(|h| h.button))
.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),
active_tab: if tab_count == 0 {
None
} else {
Some(self.initial_tab)
},
tabs: tab_headers
.iter()
.zip(self.tabs)
.map(|(header, tab)| Tab {
uuid: Uuid::new_v4(),
header_button: header.button,
content: tab.content,
close_button: header.close_button,
header_container: header.button,
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_node(UiNode::new(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));
}
}