ltk/widget/tab_strip/
row.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! The two layout elements behind the desktop strip. A horizontal scroll measures its child with no width limit, so the tabs
//! inside it cannot see how much room there is: [`TabRow`] sits outside the scroll, works the tab width out of its own rect and
//! hands it to the [`TabMotion`], and [`TabCells`], inside the scroll, places the tabs at the widths the motion reports.

use crate::render::Canvas;
use crate::types::{ Length, Rect };
use crate::widget::Element;

use super::motion::{ Placed, TabMotion };
use super::theme::{ EDGE_SCROLL, GAP, MAX_TAB_FRACTION, MIN_TAB_WIDTH, RESERVE };

pub struct TabRow<Msg: Clone>
{
	/// A horizontal scroll over a [`TabCells`].
	pub( crate ) child:    Box<Element<Msg>>,
	pub( crate ) keys:     Vec<u64>,
	pub( crate ) selected: Option<u64>,
	pub( crate ) motion:   TabMotion,
}

impl<Msg: Clone> TabRow<Msg>
{
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
	{
		( max_width, self.child.preferred_size( max_width, canvas ).1 )
	}

	/// Settle the tab width for `rect` and return the rect the tabs scroll in, short of the space kept clear on the right.
	pub( crate ) fn arrange( &self, rect: Rect, canvas: &Canvas ) -> Rect
	{
		let room   = rect.width * ( 1.0 - RESERVE );
		let gap    = canvas.resolve_geom( Length::widget( GAP ) );
		let after  = self.cells().map_or( 0.0, |cells| cells.after_width( canvas, gap ) );
		let count  = self.keys.len();
		let spare  = room - after - gap * count.saturating_sub( 1 ) as f32;
		let screen = crate::types::output_logical_size().map_or( canvas.size().0 as f32, |size| size.0 as f32 * canvas.dpi_scale() );
		let widest = screen * MAX_TAB_FRACTION;
		let least  = canvas.resolve_geom( Length::widget( MIN_TAB_WIDTH ) );
		let width  = if count == 0 { 0.0 } else { ( spare / count as f32 ).min( widest ).max( least ) };
		self.motion.arrange( &self.keys, width );
		Rect { width: room.max( 0.0 ), ..rect }
	}

	/// Where the scroll of the tabs, now at `offset` in a `viewport` that wide, should go; `None` to leave it.
	pub( crate ) fn scroll_to( &self, offset: f32, viewport: f32, canvas: &Canvas ) -> Option<f32>
	{
		self.motion.scroll( self.selected, offset, viewport, canvas.resolve_geom( Length::widget( EDGE_SCROLL ) ) )
	}

	fn cells( &self ) -> Option<&TabCells<Msg>>
	{
		match self.child.as_ref()
		{
			Element::Scroll( scroll ) => match scroll.child.as_ref()
			{
				Element::TabCells( cells ) => Some( cells ),
				_                          => None,
			},
			_ => None,
		}
	}

	pub( crate ) fn map_msg<U>( self, f: &crate::widget::MapFn<Msg, U> ) -> TabRow<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		TabRow { child: Box::new( self.child.map_arc( f ) ), keys: self.keys, selected: self.selected, motion: self.motion }
	}
}

pub struct TabCells<Msg: Clone>
{
	/// The tab handles by key, each clipped to the width it gets.
	pub( crate ) tabs:   Vec<( u64, Element<Msg> )>,
	/// Buttons right after the last tab.
	pub( crate ) after:  Vec<Element<Msg>>,
	pub( crate ) motion: TabMotion,
}

impl<Msg: Clone> TabCells<Msg>
{
	pub( crate ) fn child( &self, idx: usize ) -> &Element<Msg>
	{
		match self.tabs.get( idx )
		{
			Some( ( _, tab ) ) => tab,
			None               => &self.after[ idx - self.tabs.len() ],
		}
	}

	/// The buttons after the tabs, with the gap before each.
	pub( crate ) fn after_width( &self, canvas: &Canvas, gap: f32 ) -> f32
	{
		self.after.iter().map( |button| gap + button.preferred_size( f32::INFINITY, canvas ).0 ).sum()
	}

	pub fn preferred_size( &self, _max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
	{
		let gap    = canvas.resolve_geom( Length::widget( GAP ) );
		let placed = self.motion.placed();
		let tabs   = placed.iter().map( |slot| slot.width ).sum::<f32>() + gaps( &placed, gap );
		let height = self.tabs.iter().map( |( _, tab )| tab ).chain( &self.after )
			.map( |child| child.preferred_size( f32::INFINITY, canvas ).1 )
			.fold( 0.0_f32, f32::max );
		( tabs + self.after_width( canvas, gap ), height )
	}

	/// Tabs at full height, the dragged one last so it is drawn on top, then the buttons centred on the tabs.
	pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<( Rect, usize )>
	{
		let gap           = canvas.resolve_geom( Length::widget( GAP ) );
		let ( laid, end ) = self.motion.lay( rect, gap );
		let mut rects: Vec<_> = laid.iter()
			.filter_map( |laid| self.tabs.iter().position( |( key, _ )| *key == laid.key )
				.map( |idx| ( laid.rect, idx ) ) )
			.collect();
		let mut x = end;
		for ( i, button ) in self.after.iter().enumerate()
		{
			let ( width, height ) = button.preferred_size( f32::INFINITY, canvas );
			x += gap;
			rects.push( ( Rect { x, y: rect.y + ( rect.height - height ) / 2.0, width, height }, self.tabs.len() + i ) );
			x += width;
		}
		rects
	}

	pub( crate ) fn map_msg<U>( self, f: &crate::widget::MapFn<Msg, U> ) -> TabCells<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		TabCells
		{
			tabs:   self.tabs.into_iter().map( |( key, tab )| ( key, tab.map_arc( f ) ) ).collect(),
			after:  self.after.into_iter().map( |button| button.map_arc( f ) ).collect(),
			motion: self.motion,
		}
	}
}

/// The gaps between the slots, each as wide as the slot before it is present, so a closing tab takes its gap with it.
fn gaps( placed: &[ Placed ], gap: f32 ) -> f32
{
	let all: f32 = placed.iter().map( |slot| gap * slot.presence ).sum();
	let last     = placed.last().map_or( 0.0, |slot| gap * slot.presence );
	all - last
}

impl<Msg: Clone> From<TabRow<Msg>> for Element<Msg>
{
	fn from( row: TabRow<Msg> ) -> Self
	{
		Element::TabRow( row )
	}
}

impl<Msg: Clone> From<TabCells<Msg>> for Element<Msg>
{
	fn from( cells: TabCells<Msg> ) -> Self
	{
		Element::TabCells( cells )
	}
}