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