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}