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}