Skip to main content

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