Fluid 1.3.0
Material Design for QtQuick and Qml
Loading...
Searching...
No Matches
TabBar.qml
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com>
2// SPDX-License-Identifier: MPL-2.0
3
4pragma ComponentBehavior: Bound
5
6import QtQuick
7import QtQuick.Templates as T
8import Fluid as MD
9import "../internal" as Internal
10
150T.TabBar {
151 id: control
152
153 enum Variant { Primary, Secondary }
154 enum LayoutMode { Fixed, Scrollable }
156 property int variant: MD.TabBar.Primary
158 property int layoutMode: MD.TabBar.Fixed
160 property color containerColor: control.MD.Style.surfaceColor
162 property color indicatorColor: control.MD.Style.primaryColor
164 property color dividerColor: control.MD.Style.outlineVariantColor
165
166 property int _focusIndex: -1
167 property bool _initialized: false
168 property bool _destroying: false
169 readonly property bool _scrollable: layoutMode === MD.TabBar.Scrollable
170 readonly property int _visibleCount: {
171 let result = 0;
172 for (let i = 0; i < count; ++i) {
173 const item = itemAt(i);
174 if (item && item.visible)
175 ++result;
176 }
177 return result;
178 }
179 readonly property real _naturalWidth: {
180 let result = 0;
181 for (let i = 0; i < count; ++i) {
182 const item = itemAt(i);
183 if (item && item.visible)
184 result += item.implicitWidth;
185 }
186 return result + (layoutMode === MD.TabBar.Scrollable && _visibleCount > 0
187 ? MD.Tokens.tab.scrollableLeadingOffset : 0);
188 }
189 readonly property real _naturalHeight: {
190 let result = variant === MD.TabBar.Primary ? MD.Tokens.tab.primaryContainerHeight
191 : MD.Tokens.tab.secondaryContainerHeight;
192 for (let i = 0; i < count; ++i) {
193 const item = itemAt(i);
194 if (item && item.visible)
195 result = Math.max(result, item.implicitHeight);
196 }
197 return result;
198 }
199 readonly property real _minimumTabWidth: {
200 let result = MD.Tokens.tab.minimumInteractiveSize;
201 for (let i = 0; i < count; ++i) {
202 const item = itemAt(i) as MD.TabButton;
203 if (item && item.visible)
204 result = Math.max(result, item._minimumWidth);
205 }
206 return result;
207 }
208
209 implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
210 _naturalWidth + leftPadding + rightPadding)
211 implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
212 _naturalHeight + topPadding + bottomPadding)
213 padding: 0
214 spacing: 0
215 focusPolicy: Qt.NoFocus
216 wheelEnabled: false
217 LayoutMirroring.childrenInherit: true
218
219 function _indexOf(item) {
220 for (let i = 0; i < count; ++i) {
221 if (itemAt(i) === item)
222 return i;
223 }
224 return -1;
225 }
226 function _available(index) {
227 const item = index >= 0 && index < count ? itemAt(index) : null;
228 return item !== null && item.visible && item.enabled;
229 }
230 function _entryIndex() {
231 if (_available(currentIndex))
232 return currentIndex;
233 for (let i = 0; i < count; ++i) {
234 if (_available(i))
235 return i;
236 }
237 return -1;
238 }
239 function _isTabStop(item) {
240 const index = _available(_focusIndex) ? _focusIndex : _entryIndex();
241 return _indexOf(item) === index;
242 }
243 function _widthFor(item) {
244 if (control._scrollable)
245 return item.implicitWidth;
246 return _visibleCount > 0 ? Math.max(_minimumTabWidth,
247 availableWidth / _visibleCount) : 0;
248 }
249 function _focusEntered(item) {
250 _focusIndex = _indexOf(item);
251 _reveal(item);
252 }
253 function _focusedItem() {
254 for (let i = 0; i < count; ++i) {
255 const item = itemAt(i);
256 if (item && item.activeFocus)
257 return item;
258 }
259 return null;
260 }
261 function _focusLeft() {
262 if (control._destroying)
263 return;
264 if (!_focusedItem())
265 _focusIndex = _entryIndex();
266 }
267 function _focusAt(index) {
268 if (!_available(index))
269 return;
270 _focusIndex = index;
271 const item = itemAt(index);
272 item.forceActiveFocus(Qt.TabFocusReason);
273 _reveal(item);
274 }
275 function _handleNavigation(event) {
276 let delta = 0;
277 if (event.key === Qt.Key_Left)
278 delta = mirrored ? 1 : -1;
279 else if (event.key === Qt.Key_Right)
280 delta = mirrored ? -1 : 1;
281 else if (event.key === Qt.Key_Home || event.key === Qt.Key_End) {
282 const fromEnd = event.key === Qt.Key_End;
283 for (let step = 0; step < count; ++step) {
284 const index = fromEnd ? count - 1 - step : step;
285 if (_available(index)) {
286 _focusAt(index);
287 break;
288 }
289 }
290 event.accepted = true;
291 return true;
292 } else {
293 return false;
294 }
295 const focused = _focusedItem();
296 const start = focused ? _indexOf(focused) : _entryIndex();
297 for (let step = 1; step <= count; ++step) {
298 const index = ((start + delta * step) % count + count) % count;
299 if (_available(index)) {
300 _focusAt(index);
301 break;
302 }
303 }
304 event.accepted = true;
305 return true;
306 }
307 function _availabilityChanged() {
308 if (control._destroying)
309 return;
310 const focused = _focusedItem();
311 if (focused && !_available(_indexOf(focused)))
312 _focusAt(_entryIndex());
313 else if (!focused)
314 _focusIndex = _entryIndex();
315 _scheduleReveal();
316 }
317 function _reveal(item) {
318 if (!item || !item.visible || viewport.width <= 0)
319 return;
320 let bounds = item.mapToItem(viewport, 0, 0);
321 const realItem = viewport.itemAt(item.x + item.width / 2, item.y + item.height / 2) === item;
322 const contained = item.width <= viewport.width
323 ? bounds.x >= 0 && bounds.x + item.width <= viewport.width
324 : bounds.x <= 0 && bounds.x + item.width >= viewport.width;
325 if (realItem && contained)
326 return;
327 const index = control._indexOf(item);
328 if ((!realItem || !contained) && index >= 0)
329 viewport.positionViewAtIndex(index, ListView.Contain);
330 bounds = item.mapToItem(viewport, 0, 0);
331 let destination = viewport.contentX;
332 if (bounds.x < 0)
333 destination += bounds.x;
334 else if (bounds.x + item.width > viewport.width)
335 destination += bounds.x + item.width - viewport.width;
336 viewport.contentX = Math.max(viewport._minimumContentX,
337 Math.min(viewport._maximumContentX, destination));
338 }
339 function _revealCurrent() {
340 const focused = _focusedItem();
341 _reveal(focused || currentItem);
342 }
343 function _revealSelected() {
344 _reveal(currentItem);
345 }
346 function _scheduleSelectedReveal() {
347 if (!control._destroying)
348 Qt.callLater(control._revealSelected);
349 }
350 function _scheduleReveal() {
351 if (!control._destroying)
352 Qt.callLater(control._revealCurrent);
353 }
354
355 onCountChanged: Qt.callLater(control._availabilityChanged)
356 onCurrentIndexChanged: {
357 if (!_focusedItem())
358 _focusIndex = _entryIndex();
359 _scheduleSelectedReveal();
360 }
361 onCurrentItemChanged: _scheduleSelectedReveal()
362 onAvailableWidthChanged: _scheduleReveal()
363 onLayoutModeChanged: _scheduleReveal()
364 onMirroredChanged: _scheduleReveal()
365 Component.onCompleted: {
366 _focusIndex = _entryIndex();
367 Qt.callLater(function() { control._initialized = true; control._revealCurrent(); });
368 }
369 Component.onDestruction: control._destroying = true
370
371 Keys.priority: Keys.BeforeItem
372 Keys.onPressed: event => { control._handleNavigation(event); }
373
374 contentItem: ListView {
375 id: viewport
376 objectName: "tabViewport"
377 model: control.contentModel
378 orientation: ListView.Horizontal
379 currentIndex: -1
380 interactive: contentWidth > width
381 clip: true
382 boundsBehavior: Flickable.StopAtBounds
383 boundsMovement: Flickable.StopAtBounds
384 flickableDirection: Flickable.HorizontalFlick
385 snapMode: ListView.NoSnap
386 readonly property real _contentStart: {
387 let result = headerItem ? headerItem.x : 0;
388 for (let i = 0; i < control.count; ++i) {
389 const item = control.itemAt(i);
390 if (item && item.visible)
391 result = Math.min(result, item.x);
392 }
393 return result;
394 }
395 readonly property real _contentEnd: {
396 let result = headerItem ? headerItem.x + headerItem.width : 0;
397 for (let i = 0; i < control.count; ++i) {
398 const item = control.itemAt(i);
399 if (item && item.visible)
400 result = Math.max(result, item.x + item.width);
401 }
402 return result;
403 }
404 readonly property real _minimumContentX: control.mirrored
405 ? Math.min(_contentStart, _contentEnd - width) : _contentStart
406 readonly property real _maximumContentX: control.mirrored
407 ? _contentEnd - width : Math.max(_contentStart, _contentEnd - width)
408 header: Item {
409 width: control.layoutMode === MD.TabBar.Scrollable && control._visibleCount > 0
410 ? MD.Tokens.tab.scrollableLeadingOffset : 0
411 height: viewport.height
412 }
413
414 Internal.TabIndicator {
415 parent: viewport
416 primary: control.variant === MD.TabBar.Primary
417 readonly property MD.TabButton selectedTab: control.currentItem as MD.TabButton
418 readonly property real selectedWidth: selectedTab && selectedTab.visible
419 ? (primary ? selectedTab._primaryIndicatorWidth : selectedTab.width) : 0
420 targetWidth: selectedWidth
421 targetX: selectedTab ? selectedTab.x - viewport.contentX
422 + (selectedTab.width - selectedWidth) / 2 : 0
423 y: viewport.height - height
424 indicatorColor: control.indicatorColor
425 animationEnabled: control._initialized && !viewport.moving
426 visible: selectedWidth > 0
427 z: 2
428 }
429 }
430
431 background: Rectangle {
432 color: control.containerColor
433 Rectangle {
434 objectName: "tabDivider"
435 parent: viewport
436 z: 1
437 width: viewport.width
438 height: MD.Tokens.tab.dividerHeight
439 y: viewport.height - height
440 color: control.dividerColor
441 Accessible.ignored: true
442 }
443 }
444}
Material Design 3 design tokens.
Definition tokens.h:71