fyrox_ui/
tab_control.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! The Tab Control handles the visibility of several tabs, only showing a single tab that the user has selected via the
22//! tab header buttons. See docs for [`TabControl`] widget for more info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::style::resource::StyleResourceExt;
27use crate::style::{Style, StyledProperty};
28use crate::{
29    border::BorderBuilder,
30    brush::Brush,
31    button::{ButtonBuilder, ButtonMessage},
32    core::{
33        algebra::Vector2, color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
34        uuid_provider, visitor::prelude::*,
35    },
36    decorator::{DecoratorBuilder, DecoratorMessage},
37    define_constructor,
38    grid::{Column, GridBuilder, Row},
39    message::{ButtonState, MessageDirection, MouseButton, UiMessage},
40    utils::make_cross_primitive,
41    vector_image::VectorImageBuilder,
42    widget::{Widget, WidgetBuilder, WidgetMessage},
43    wrap_panel::WrapPanelBuilder,
44    BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
45    VerticalAlignment,
46};
47
48use fyrox_core::variable::InheritableVariable;
49use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
50use fyrox_graph::BaseSceneGraph;
51use std::{
52    any::Any,
53    cmp::Ordering,
54    fmt::{Debug, Formatter},
55    ops::{Deref, DerefMut},
56    sync::Arc,
57};
58
59/// A set of messages for [`TabControl`] widget.
60#[derive(Debug, Clone, PartialEq)]
61pub enum TabControlMessage {
62    /// Used to change the active tab of a [`TabControl`] widget (with [`MessageDirection::ToWidget`]) or to fetch if the active
63    /// tab has changed (with [`MessageDirection::FromWidget`]).
64    /// When the active tab changes, `ActiveTabUuid` will also be sent from the widget.
65    /// When the active tab changes, `ActiveTabUuid` will also be sent from the widget.
66    ActiveTab(Option<usize>),
67    /// Used to change the active tab of a [`TabControl`] widget (with [`MessageDirection::ToWidget`]) or to fetch if the active
68    /// tab has changed (with [`MessageDirection::FromWidget`]).
69    /// When the active tab changes, `ActiveTab` will also be sent from the widget.
70    ActiveTabUuid(Option<Uuid>),
71    /// Emitted by a tab that needs to be closed (and removed). Does **not** remove the tab, its main usage is to catch the moment
72    /// when the tab wants to be closed. To remove the tab use [`TabControlMessage::RemoveTab`] message.
73    CloseTab(usize),
74    /// Emitted by a tab that needs to be closed (and removed). Does **not** remove the tab, its main usage is to catch the moment
75    /// when the tab wants to be closed. To remove the tab use [`TabControlMessage::RemoveTab`] message.
76    CloseTabByUuid(Uuid),
77    /// Used to remove a particular tab by its position in the tab list.
78    RemoveTab(usize),
79    /// Used to remove a particular tab by its UUID.
80    RemoveTabByUuid(Uuid),
81    /// Adds a new tab using its definition and activates the tab.
82    AddTab {
83        /// The UUID of the newly created tab.
84        uuid: Uuid,
85        /// The specifications for the tab.
86        definition: TabDefinition,
87    },
88}
89
90impl TabControlMessage {
91    define_constructor!(
92        /// Creates [`TabControlMessage::ActiveTab`] message.
93        TabControlMessage:ActiveTab => fn active_tab(Option<usize>), layout: false
94    );
95    define_constructor!(
96        /// Creates [`TabControlMessage::ActiveTabUuid`] message.
97        TabControlMessage:ActiveTabUuid => fn active_tab_uuid(Option<Uuid>), layout: false
98    );
99    define_constructor!(
100        /// Creates [`TabControlMessage::CloseTab`] message.
101        TabControlMessage:CloseTab => fn close_tab(usize), layout: false
102    );
103    define_constructor!(
104        /// Creates [`TabControlMessage::CloseTabByUuid`] message.
105        TabControlMessage:CloseTabByUuid => fn close_tab_by_uuid(Uuid), layout: false
106    );
107    define_constructor!(
108        /// Creates [`TabControlMessage::RemoveTab`] message.
109        TabControlMessage:RemoveTab => fn remove_tab(usize), layout: false
110    );
111    define_constructor!(
112        /// Creates [`TabControlMessage::RemoveTabByUuid`] message.
113        TabControlMessage:RemoveTabByUuid => fn remove_tab_by_uuid(Uuid), layout: false
114    );
115    define_constructor!(
116        /// Creates [`TabControlMessage::AddTab`] message.
117        TabControlMessage:AddTab => fn add_tab_with_uuid(uuid: Uuid, definition: TabDefinition), layout: false
118    );
119    /// Creates [`TabControlMessage::AddTab`] message with a random UUID.
120    pub fn add_tab(
121        destination: Handle<UiNode>,
122        direction: MessageDirection,
123        definition: TabDefinition,
124    ) -> UiMessage {
125        UiMessage {
126            handled: std::cell::Cell::new(false),
127            data: Box::new(Self::AddTab {
128                uuid: Uuid::new_v4(),
129                definition,
130            }),
131            destination,
132            direction,
133            routing_strategy: Default::default(),
134            perform_layout: std::cell::Cell::new(false),
135            flags: 0,
136        }
137    }
138}
139
140/// User-defined data of a tab.
141#[derive(Clone)]
142pub struct TabUserData(pub Arc<dyn Any + Send + Sync>);
143
144impl TabUserData {
145    /// Creates new instance of the tab data.
146    pub fn new<T>(data: T) -> Self
147    where
148        T: Any + Send + Sync,
149    {
150        Self(Arc::new(data))
151    }
152}
153
154impl PartialEq for TabUserData {
155    fn eq(&self, other: &Self) -> bool {
156        std::ptr::eq(
157            (&*self.0) as *const _ as *const (),
158            (&*other.0) as *const _ as *const (),
159        )
160    }
161}
162
163impl Debug for TabUserData {
164    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
165        write!(f, "User-defined data")
166    }
167}
168
169/// Tab of the [`TabControl`] widget. It stores important tab data, that is widely used at runtime.
170#[derive(Default, Clone, PartialEq, Visit, Reflect, Debug)]
171pub struct Tab {
172    /// Unique identifier of this tab.
173    pub uuid: Uuid,
174    /// A handle of the header button, that is used to switch tabs.
175    pub header_button: Handle<UiNode>,
176    /// Tab's content.
177    pub content: Handle<UiNode>,
178    /// A handle of a button, that is used to close the tab.
179    pub close_button: Handle<UiNode>,
180    /// A handle to a container widget, that holds the header.
181    pub header_container: Handle<UiNode>,
182    /// User-defined data.
183    #[visit(skip)]
184    #[reflect(hidden)]
185    pub user_data: Option<TabUserData>,
186    /// A handle of a node that is used to highlight tab's state.
187    pub decorator: Handle<UiNode>,
188    /// Content of the tab-switching (header) button.
189    pub header_content: Handle<UiNode>,
190}
191
192/// The Tab Control handles the visibility of several tabs, only showing a single tab that the user has selected via the
193/// tab header buttons. Each tab is defined via a Tab Definition struct which takes two widgets, one representing the tab
194/// header and the other representing the tab's contents.
195///
196/// The following example makes a 2 tab, Tab Control containing some simple text widgets:
197///
198/// ```rust,no_run
199/// # use fyrox_ui::{
200/// #     BuildContext,
201/// #     widget::WidgetBuilder,
202/// #     text::TextBuilder,
203/// #     tab_control::{TabControlBuilder, TabDefinition},
204/// # };
205/// fn create_tab_control(ctx: &mut BuildContext) {
206///     TabControlBuilder::new(WidgetBuilder::new())
207///         .with_tab(
208///             TabDefinition{
209///                 header: TextBuilder::new(WidgetBuilder::new())
210///                             .with_text("First")
211///                             .build(ctx),
212///
213///                 content: TextBuilder::new(WidgetBuilder::new())
214///                             .with_text("First tab's contents!")
215///                             .build(ctx),
216///                 can_be_closed: true,
217///                 user_data: None
218///             }
219///         )
220///         .with_tab(
221///             TabDefinition{
222///                 header: TextBuilder::new(WidgetBuilder::new())
223///                             .with_text("Second")
224///                             .build(ctx),
225///
226///                 content: TextBuilder::new(WidgetBuilder::new())
227///                             .with_text("Second tab's contents!")
228///                             .build(ctx),
229///                 can_be_closed: true,
230///                 user_data: None
231///             }
232///         )
233///         .build(ctx);
234/// }
235/// ```
236///
237/// As usual, we create the widget via the builder TabControlBuilder. Tabs are added via the [`TabControlBuilder::with_tab`]
238/// function in the order you want them to appear, passing each call to the function a directly constructed [`TabDefinition`]
239/// struct. Tab headers will appear from left to right at the top with tab contents shown directly below the tabs. As usual, if no
240/// constraints are given to the base [`WidgetBuilder`] of the [`TabControlBuilder`], then the tab content area will resize to fit
241/// whatever is in the current tab.
242///
243/// Each tab's content is made up of one widget, so to be useful you will want to use one of the container widgets to help
244/// arrange additional widgets within the tab.
245///
246/// ## Tab Header Styling
247///
248/// Notice that you can put any widget into the tab header, so if you want images to denote each tab you can add an Image
249/// widget to each header, and if you want an image *and* some text you can insert a stack panel with an image on top and
250/// text below it.
251///
252/// You will also likely want to style whatever widgets you add. As can be seen when running the code example above, the
253/// tab headers are scrunched when there are no margins provided to your text widgets. Simply add something like the below
254/// code example and you will get a decent look:
255///
256/// ```rust,no_run
257/// # use fyrox_ui::{
258/// #     BuildContext,
259/// #     widget::WidgetBuilder,
260/// #     text::TextBuilder,
261/// #     Thickness,
262/// #     tab_control::{TabDefinition},
263/// # };
264/// # fn build(ctx: &mut BuildContext) {
265/// # TabDefinition{
266/// header: TextBuilder::new(
267///             WidgetBuilder::new()
268///                 .with_margin(Thickness::uniform(4.0))
269///         )
270///             .with_text("First")
271///             .build(ctx),
272/// # content: Default::default(),
273/// # can_be_closed: true,
274/// # user_data: None
275/// # };
276/// # }
277///
278/// ```
279#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
280#[reflect(derived_type = "UiNode")]
281pub struct TabControl {
282    /// Base widget of the tab control.
283    pub widget: Widget,
284    /// True if the user permitted to change the order of the tabs.
285    pub is_tab_drag_allowed: bool,
286    /// A set of tabs used by the tab control.
287    pub tabs: Vec<Tab>,
288    /// Active tab of the tab control.
289    pub active_tab: Option<usize>,
290    /// A handle of a widget, that holds content of every tab.
291    pub content_container: Handle<UiNode>,
292    /// A handle of a widget, that holds headers of every tab.
293    pub headers_container: Handle<UiNode>,
294    /// A brush, that will be used to highlight active tab.
295    pub active_tab_brush: InheritableVariable<StyledProperty<Brush>>,
296}
297
298impl ConstructorProvider<UiNode, UserInterface> for TabControl {
299    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
300        GraphNodeConstructor::new::<Self>()
301            .with_variant("Tab Control", |ui| {
302                TabControlBuilder::new(WidgetBuilder::new().with_name("Tab Control"))
303                    .build(&mut ui.build_ctx())
304                    .into()
305            })
306            .with_group("Layout")
307    }
308}
309
310crate::define_widget_deref!(TabControl);
311
312uuid_provider!(TabControl = "d54cfac3-0afc-464b-838a-158b3a2253f5");
313
314impl TabControl {
315    fn do_drag(&mut self, position: Vector2<f32>, ui: &mut UserInterface) {
316        let mut dragged_index = None;
317        let mut target_index = None;
318        for (tab_index, tab) in self.tabs.iter().enumerate() {
319            let bounds = ui.node(tab.header_button).screen_bounds();
320            let node_x = bounds.center().x;
321            if bounds.contains(position) {
322                if node_x < position.x {
323                    target_index = Some(tab_index + 1);
324                } else {
325                    target_index = Some(tab_index);
326                }
327            }
328            if ui.is_node_child_of(ui.captured_node, tab.header_button) {
329                dragged_index = Some(tab_index);
330            }
331        }
332        if let (Some(dragged_index), Some(mut target_index)) = (dragged_index, target_index) {
333            if dragged_index < target_index {
334                target_index -= 1;
335            }
336            if target_index != dragged_index {
337                self.finalize_drag(dragged_index, target_index, ui);
338            }
339        }
340    }
341    fn finalize_drag(&mut self, from: usize, to: usize, ui: &mut UserInterface) {
342        let uuid = self.active_tab.map(|i| self.tabs[i].uuid);
343        let tab = self.tabs.remove(from);
344        self.tabs.insert(to, tab);
345        if let Some(uuid) = uuid {
346            self.active_tab = self.tabs.iter().position(|t| t.uuid == uuid);
347        }
348        let new_tab_handles = self.tabs.iter().map(|t| t.header_container).collect();
349        ui.send_message(WidgetMessage::replace_children(
350            self.headers_container,
351            MessageDirection::ToWidget,
352            new_tab_handles,
353        ));
354    }
355    /// Use a tab's UUID to look up the tab.
356    pub fn get_tab_by_uuid(&self, uuid: Uuid) -> Option<&Tab> {
357        self.tabs.iter().find(|t| t.uuid == uuid)
358    }
359    /// Send the necessary messages to activate the tab at the given index, or deactivate all tabs if no index is given.
360    /// Do nothing if the given index does not refer to any existing tab.
361    /// If the index was valid, send FromWidget messages to notify listeners of the change, using messages with the given flags.
362    fn set_active_tab(&mut self, active_tab: Option<usize>, ui: &mut UserInterface, flags: u64) {
363        if let Some(index) = active_tab {
364            if self.tabs.len() <= index {
365                return;
366            }
367        }
368        // Send messages to update the state of each tab.
369        for (existing_tab_index, tab) in self.tabs.iter().enumerate() {
370            ui.send_message(WidgetMessage::visibility(
371                tab.content,
372                MessageDirection::ToWidget,
373                active_tab == Some(existing_tab_index),
374            ));
375            ui.send_message(DecoratorMessage::select(
376                tab.decorator,
377                MessageDirection::ToWidget,
378                active_tab == Some(existing_tab_index),
379            ))
380        }
381
382        self.active_tab = active_tab;
383
384        // Notify potential listeners that the active tab has changed.
385        // First we notify by tab index.
386        let mut msg =
387            TabControlMessage::active_tab(self.handle, MessageDirection::FromWidget, active_tab);
388        msg.flags = flags;
389        ui.send_message(msg);
390        // Next we notify by the tab's uuid, which does not change even as the tab moves.
391        let tab_id = active_tab.and_then(|i| self.tabs.get(i)).map(|t| t.uuid);
392        let mut msg =
393            TabControlMessage::active_tab_uuid(self.handle, MessageDirection::FromWidget, tab_id);
394        msg.flags = flags;
395        ui.send_message(msg);
396    }
397    /// Send the messages necessary to remove the tab at the given index and update the currently active tab.
398    /// This does not include sending FromWidget messages to notify listeners.
399    /// If the given index does not refer to any tab, do nothing and return false.
400    /// Otherwise, return true to indicate that some tab was removed.
401    fn remove_tab(&mut self, index: usize, ui: &mut UserInterface) -> bool {
402        let Some(tab) = self.tabs.get(index) else {
403            return false;
404        };
405        ui.send_message(WidgetMessage::remove(
406            tab.header_container,
407            MessageDirection::ToWidget,
408        ));
409        ui.send_message(WidgetMessage::remove(
410            tab.content,
411            MessageDirection::ToWidget,
412        ));
413
414        self.tabs.remove(index);
415
416        if let Some(active_tab) = &self.active_tab {
417            match index.cmp(active_tab) {
418                Ordering::Less => self.active_tab = Some(active_tab - 1), // Just the index needs to change, not the actual tab.
419                Ordering::Equal => {
420                    // The active tab was removed, so we need to change the active tab.
421                    if self.tabs.is_empty() {
422                        self.set_active_tab(None, ui, 0);
423                    } else if *active_tab == 0 {
424                        // The index has not changed, but this is actually a different tab,
425                        // so we need to activate it.
426                        self.set_active_tab(Some(0), ui, 0);
427                    } else {
428                        self.set_active_tab(Some(active_tab - 1), ui, 0);
429                    }
430                }
431                Ordering::Greater => (), // Do nothing, since removed tab was to the right of active tab.
432            }
433        }
434
435        true
436    }
437}
438
439impl Control for TabControl {
440    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
441        self.widget.handle_routed_message(ui, message);
442
443        if let Some(ButtonMessage::Click) = message.data() {
444            for (tab_index, tab) in self.tabs.iter().enumerate() {
445                if message.destination() == tab.header_button && tab.header_button.is_some() {
446                    ui.send_message(TabControlMessage::active_tab_uuid(
447                        self.handle,
448                        MessageDirection::ToWidget,
449                        Some(tab.uuid),
450                    ));
451                    break;
452                } else if message.destination() == tab.close_button {
453                    // Send two messages, one containing the index, one containing the UUID,
454                    // to allow listeners their choice of which system they prefer.
455                    ui.send_message(TabControlMessage::close_tab(
456                        self.handle,
457                        MessageDirection::FromWidget,
458                        tab_index,
459                    ));
460                    ui.send_message(TabControlMessage::close_tab_by_uuid(
461                        self.handle,
462                        MessageDirection::FromWidget,
463                        tab.uuid,
464                    ));
465                }
466            }
467        } else if let Some(WidgetMessage::MouseDown { button, .. }) = message.data() {
468            if *button == MouseButton::Middle {
469                for (tab_index, tab) in self.tabs.iter().enumerate() {
470                    if ui.is_node_child_of(message.destination(), tab.header_button) {
471                        ui.send_message(TabControlMessage::close_tab(
472                            self.handle,
473                            MessageDirection::FromWidget,
474                            tab_index,
475                        ));
476                        ui.send_message(TabControlMessage::close_tab_by_uuid(
477                            self.handle,
478                            MessageDirection::FromWidget,
479                            tab.uuid,
480                        ));
481                    }
482                }
483            }
484        } else if let Some(WidgetMessage::MouseMove { pos, state }) = message.data() {
485            if state.left == ButtonState::Pressed
486                && self.is_tab_drag_allowed
487                && ui.is_node_child_of(ui.captured_node, self.headers_container)
488            {
489                self.do_drag(*pos, ui);
490            }
491        } else if let Some(msg) = message.data::<TabControlMessage>() {
492            if message.destination() == self.handle()
493                && message.direction() == MessageDirection::ToWidget
494            {
495                match msg {
496                    TabControlMessage::ActiveTab(active_tab) => {
497                        if self.active_tab != *active_tab {
498                            self.set_active_tab(*active_tab, ui, message.flags);
499                        }
500                    }
501                    TabControlMessage::ActiveTabUuid(uuid) => match uuid {
502                        Some(uuid) => {
503                            if let Some(active_tab) = self.tabs.iter().position(|t| t.uuid == *uuid)
504                            {
505                                if self.active_tab != Some(active_tab) {
506                                    self.set_active_tab(Some(active_tab), ui, message.flags);
507                                }
508                            }
509                        }
510                        None if self.active_tab.is_some() => {
511                            self.set_active_tab(None, ui, message.flags)
512                        }
513                        _ => (),
514                    },
515                    TabControlMessage::CloseTab(_) | TabControlMessage::CloseTabByUuid(_) => {
516                        // Nothing to do.
517                    }
518                    TabControlMessage::RemoveTab(index) => {
519                        // If a tab was removed, then resend the message.
520                        // Users that remove tabs using the index-based message only get the index-based message in reponse,
521                        // since presumably their application is not using UUIDs.
522                        if self.remove_tab(*index, ui) {
523                            ui.send_message(message.reverse());
524                        }
525                    }
526                    TabControlMessage::RemoveTabByUuid(uuid) => {
527                        // Find the tab that has the given uuid.
528                        let index = self.tabs.iter().position(|t| t.uuid == *uuid);
529                        // Users that remove tabs using the UUID-based message only get the UUID-based message in reponse,
530                        // since presumably their application is not using tab indices.
531                        if let Some(index) = index {
532                            if self.remove_tab(index, ui) {
533                                ui.send_message(message.reverse());
534                            }
535                        }
536                    }
537                    TabControlMessage::AddTab { uuid, definition } => {
538                        if self.tabs.iter().any(|t| &t.uuid == uuid) {
539                            ui.send_message(WidgetMessage::remove(
540                                definition.header,
541                                MessageDirection::ToWidget,
542                            ));
543                            ui.send_message(WidgetMessage::remove(
544                                definition.content,
545                                MessageDirection::ToWidget,
546                            ));
547                            return;
548                        }
549                        let header = Header::build(
550                            definition,
551                            false,
552                            (*self.active_tab_brush).clone(),
553                            &mut ui.build_ctx(),
554                        );
555
556                        ui.send_message(WidgetMessage::link(
557                            header.button,
558                            MessageDirection::ToWidget,
559                            self.headers_container,
560                        ));
561
562                        ui.send_message(WidgetMessage::link(
563                            definition.content,
564                            MessageDirection::ToWidget,
565                            self.content_container,
566                        ));
567
568                        ui.send_message(message.reverse());
569
570                        self.tabs.push(Tab {
571                            uuid: *uuid,
572                            header_button: header.button,
573                            content: definition.content,
574                            close_button: header.close_button,
575                            header_container: header.button,
576                            user_data: definition.user_data.clone(),
577                            decorator: header.decorator,
578                            header_content: header.content,
579                        });
580                    }
581                }
582            }
583        }
584    }
585}
586
587/// Tab control builder is used to create [`TabControl`] widget instances and add them to the user interface.
588pub struct TabControlBuilder {
589    widget_builder: WidgetBuilder,
590    is_tab_drag_allowed: bool,
591    tabs: Vec<(Uuid, TabDefinition)>,
592    active_tab_brush: Option<StyledProperty<Brush>>,
593    initial_tab: usize,
594}
595
596/// Tab definition is used to describe content of each tab for the [`TabControlBuilder`] builder.
597#[derive(Debug, Clone, PartialEq)]
598pub struct TabDefinition {
599    /// Content of the tab-switching (header) button.
600    pub header: Handle<UiNode>,
601    /// Content of the tab.
602    pub content: Handle<UiNode>,
603    /// A flag, that defines whether the tab can be closed or not.
604    pub can_be_closed: bool,
605    /// User-defined data.
606    pub user_data: Option<TabUserData>,
607}
608
609struct Header {
610    button: Handle<UiNode>,
611    close_button: Handle<UiNode>,
612    decorator: Handle<UiNode>,
613    content: Handle<UiNode>,
614}
615
616impl Header {
617    fn build(
618        tab_definition: &TabDefinition,
619        selected: bool,
620        active_tab_brush: StyledProperty<Brush>,
621        ctx: &mut BuildContext,
622    ) -> Self {
623        let close_button;
624        let decorator;
625
626        let button = ButtonBuilder::new(WidgetBuilder::new().on_row(0).on_column(0))
627            .with_back({
628                decorator = DecoratorBuilder::new(
629                    BorderBuilder::new(WidgetBuilder::new())
630                        .with_stroke_thickness(Thickness::uniform(0.0).into()),
631                )
632                .with_normal_brush(ctx.style.property(Style::BRUSH_DARK))
633                .with_selected_brush(active_tab_brush)
634                .with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
635                .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHT))
636                .with_selected(selected)
637                .build(ctx);
638                decorator
639            })
640            .with_content(
641                GridBuilder::new(
642                    WidgetBuilder::new()
643                        .with_child(tab_definition.header)
644                        .with_child({
645                            close_button = if tab_definition.can_be_closed {
646                                ButtonBuilder::new(
647                                    WidgetBuilder::new()
648                                        .with_margin(Thickness::right(1.0))
649                                        .on_row(0)
650                                        .on_column(1)
651                                        .with_width(16.0)
652                                        .with_height(16.0),
653                                )
654                                .with_back(
655                                    DecoratorBuilder::new(
656                                        BorderBuilder::new(WidgetBuilder::new())
657                                            .with_corner_radius(5.0f32.into())
658                                            .with_pad_by_corner_radius(false)
659                                            .with_stroke_thickness(Thickness::uniform(0.0).into()),
660                                    )
661                                    .with_normal_brush(Brush::Solid(Color::TRANSPARENT).into())
662                                    .with_hover_brush(ctx.style.property(Style::BRUSH_DARK))
663                                    .build(ctx),
664                                )
665                                .with_content(
666                                    VectorImageBuilder::new(
667                                        WidgetBuilder::new()
668                                            .with_horizontal_alignment(HorizontalAlignment::Center)
669                                            .with_vertical_alignment(VerticalAlignment::Center)
670                                            .with_width(8.0)
671                                            .with_height(8.0)
672                                            .with_foreground(
673                                                ctx.style.property(Style::BRUSH_BRIGHTEST),
674                                            ),
675                                    )
676                                    .with_primitives(make_cross_primitive(8.0, 2.0))
677                                    .build(ctx),
678                                )
679                                .build(ctx)
680                            } else {
681                                Handle::NONE
682                            };
683                            close_button
684                        }),
685                )
686                .add_row(Row::auto())
687                .add_column(Column::stretch())
688                .add_column(Column::auto())
689                .build(ctx),
690            )
691            .build(ctx);
692
693        Header {
694            button,
695            close_button,
696            decorator,
697            content: tab_definition.header,
698        }
699    }
700}
701
702impl TabControlBuilder {
703    /// Creates new tab control builder.
704    pub fn new(widget_builder: WidgetBuilder) -> Self {
705        Self {
706            tabs: Default::default(),
707            is_tab_drag_allowed: false,
708            active_tab_brush: None,
709            initial_tab: 0,
710            widget_builder,
711        }
712    }
713
714    /// Controls the initially selected tab. The default is 0, the first tab on the left.
715    pub fn with_initial_tab(mut self, tab_index: usize) -> Self {
716        self.initial_tab = tab_index;
717        self
718    }
719
720    /// Controls whether tabs may be dragged. The default is false.
721    pub fn with_tab_drag(mut self, is_tab_drag_allowed: bool) -> Self {
722        self.is_tab_drag_allowed = is_tab_drag_allowed;
723        self
724    }
725
726    /// Adds a new tab to the builder.
727    pub fn with_tab(mut self, tab: TabDefinition) -> Self {
728        self.tabs.push((Uuid::new_v4(), tab));
729        self
730    }
731
732    /// Adds a new tab to the builder, using the given UUID for the tab.
733    pub fn with_tab_uuid(mut self, uuid: Uuid, tab: TabDefinition) -> Self {
734        self.tabs.push((uuid, tab));
735        self
736    }
737
738    /// Sets a desired brush for active tab.
739    pub fn with_active_tab_brush(mut self, brush: StyledProperty<Brush>) -> Self {
740        self.active_tab_brush = Some(brush);
741        self
742    }
743
744    /// Finishes [`TabControl`] building and adds it to the user interface and returns its handle.
745    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
746        let tab_count = self.tabs.len();
747        // Hide everything but initial tab content.
748        for (i, (_, tab)) in self.tabs.iter().enumerate() {
749            if let Some(content) = ctx.try_get_node_mut(tab.content) {
750                content.set_visibility(i == self.initial_tab);
751            }
752        }
753
754        let active_tab_brush = self
755            .active_tab_brush
756            .unwrap_or_else(|| ctx.style.property::<Brush>(Style::BRUSH_LIGHTEST));
757
758        let tab_headers = self
759            .tabs
760            .iter()
761            .enumerate()
762            .map(|(i, (_, tab_definition))| {
763                Header::build(
764                    tab_definition,
765                    i == self.initial_tab,
766                    active_tab_brush.clone(),
767                    ctx,
768                )
769            })
770            .collect::<Vec<_>>();
771
772        let headers_container = WrapPanelBuilder::new(
773            WidgetBuilder::new()
774                .with_children(tab_headers.iter().map(|h| h.button))
775                .on_row(0),
776        )
777        .with_orientation(Orientation::Horizontal)
778        .build(ctx);
779
780        let content_container = GridBuilder::new(
781            WidgetBuilder::new()
782                .with_children(self.tabs.iter().map(|(_, t)| t.content))
783                .on_row(1),
784        )
785        .add_row(Row::stretch())
786        .add_column(Column::stretch())
787        .build(ctx);
788
789        let grid = GridBuilder::new(
790            WidgetBuilder::new()
791                .with_child(headers_container)
792                .with_child(content_container),
793        )
794        .add_column(Column::stretch())
795        .add_row(Row::auto())
796        .add_row(Row::stretch())
797        .build(ctx);
798
799        let border = BorderBuilder::new(
800            WidgetBuilder::new()
801                .with_background(ctx.style.property(Style::BRUSH_DARK))
802                .with_child(grid),
803        )
804        .build(ctx);
805
806        let tc = TabControl {
807            widget: self.widget_builder.with_child(border).build(ctx),
808            is_tab_drag_allowed: self.is_tab_drag_allowed,
809            active_tab: if tab_count == 0 {
810                None
811            } else {
812                Some(self.initial_tab)
813            },
814            tabs: tab_headers
815                .into_iter()
816                .zip(self.tabs)
817                .map(|(header, (uuid, tab))| Tab {
818                    uuid,
819                    header_button: header.button,
820                    content: tab.content,
821                    close_button: header.close_button,
822                    header_container: header.button,
823                    user_data: tab.user_data,
824                    decorator: header.decorator,
825                    header_content: header.content,
826                })
827                .collect(),
828            content_container,
829            headers_container,
830            active_tab_brush: active_tab_brush.into(),
831        };
832
833        ctx.add_node(UiNode::new(tc))
834    }
835}
836
837#[cfg(test)]
838mod test {
839    use crate::tab_control::TabControlBuilder;
840    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
841
842    #[test]
843    fn test_deletion() {
844        test_widget_deletion(|ctx| TabControlBuilder::new(WidgetBuilder::new()).build(ctx));
845    }
846}