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