import { HorizontalBox, VerticalBox } from "std-widgets.slint"; import { TypeScale } from "../typography.slint"; import { LayoutMetrics, MotionSettings } from "../design-system.slint"; import { WindowChrome, WindowDragArea } from "../window-chrome.slint"; import { MailProfileRow } from "../models.slint"; import { ProfileSwitcher } from "profile-switcher.slint"; import { PaneDivider } from "workspace-layout.slint"; import { TabRow, TabsUi, WorkspaceIcon } from "tabs-ui.slint"; export { TabRow, TabsUi } component ProductRailItem inherits Rectangle { in property label; in property outline-icon; in property filled-icon; in property active: false; in property accent-color; in property accent-background; in property muted-color; out property hovered: item-hit.has-hover; callback clicked; // Desktop navigation is icon-only. The target and selected surface remain // square so each destination has an even, predictable hit area. height: 48px; background: transparent; Rectangle { x: (parent.width - self.width) / 2; y: 4px; width: 40px; height: 40px; border-radius: 10px; background: root.active || item-hit.has-hover ? root.accent-background : transparent; animate background { duration: 140ms; easing: ease-out; } Image { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: root.active ? LayoutMetrics.icon-size + 2px : LayoutMetrics.icon-size; height: root.active ? LayoutMetrics.icon-size + 2px : LayoutMetrics.icon-size; source: root.active ? root.filled-icon : root.outline-icon; colorize: root.active ? root.accent-color : root.muted-color; animate colorize { duration: 140ms; easing: ease-out; } animate width, height { duration: 180ms; easing: spring(0.12); } image-fit: contain; accessible-role: none; } item-hit := TouchArea { accessible-role: button; accessible-label: root.label; accessible-checked: root.active; accessible-action-default => { root.clicked(); } clicked => { root.clicked(); } mouse-cursor: pointer; } } } // The product rail is deliberately narrow: it switches workspaces without // competing with the context-specific sidebar immediately to its right. export component ProductRail inherits Rectangle { in-out property active-view: "mail"; in property settings-active: false; in property background-color; in property divider-color; in property accent-color; in property accent-background; in property title-color; in property muted-color; out property tooltip-text: mail-item.hovered ? mail-item.label : calendar-item.hovered ? calendar-item.label : files-item.hovered ? files-item.label : contacts-item.hovered ? contacts-item.label : settings-item.hovered ? settings-item.label : ""; out property tooltip-y: mail-item.hovered ? mail-item.y + 10px : calendar-item.hovered ? calendar-item.y + 10px : files-item.hovered ? files-item.y + 10px : contacts-item.hovered ? contacts-item.y + 10px : settings-item.hovered ? settings-item.y + 10px : 0px; callback open-settings; callback select-view(string); width: LayoutMetrics.product-rail-width; background: root.background-color; PaneDivider { x: parent.width - self.width; height: parent.height; divider-color: root.divider-color; } VerticalBox { x: 0px; y: 0px; width: parent.width; height: parent.height; // 8px outer padding and 48px items preserve a compact 8px rhythm. padding-top: 8px; padding-right: 0px; padding-bottom: 10px; padding-left: 0px; spacing: 8px; mail-item := ProductRailItem { label: @tr("Mail"); outline-icon: @image-url("../icons/lucide/outline/mail.svg"); filled-icon: @image-url("../icons/lucide/filled/mail.svg"); active: !root.settings-active && root.active-view == "mail"; accent-color: root.accent-color; accent-background: root.accent-background; muted-color: root.muted-color; clicked => { root.select-view("mail"); } } calendar-item := ProductRailItem { label: @tr("Calendar"); outline-icon: @image-url("../icons/lucide/outline/calendar.svg"); filled-icon: @image-url("../icons/lucide/filled/calendar.svg"); active: !root.settings-active && root.active-view == "calendar"; accent-color: root.accent-color; accent-background: root.accent-background; muted-color: root.muted-color; clicked => { root.select-view("calendar"); } } files-item := ProductRailItem { label: @tr("Files"); outline-icon: @image-url("../icons/lucide/outline/files.svg"); filled-icon: @image-url("../icons/lucide/filled/files.svg"); active: !root.settings-active && root.active-view == "files"; accent-color: root.accent-color; accent-background: root.accent-background; muted-color: root.muted-color; clicked => { root.select-view("files"); } } contacts-item := ProductRailItem { label: @tr("Contacts"); outline-icon: @image-url("../icons/lucide/outline/contacts.svg"); filled-icon: @image-url("../icons/lucide/filled/contacts.svg"); active: !root.settings-active && root.active-view == "contacts"; accent-color: root.accent-color; accent-background: root.accent-background; muted-color: root.muted-color; clicked => { root.select-view("contacts"); } } Rectangle { vertical-stretch: 1; } settings-item := ProductRailItem { label: @tr("Settings"); outline-icon: @image-url("../icons/lucide/outline/gear.svg"); filled-icon: @image-url("../icons/lucide/filled/gear.svg"); active: root.settings-active; accent-color: root.accent-color; accent-background: root.accent-background; muted-color: root.muted-color; clicked => { root.open-settings(); } } } } // Compact navigation stays at the bottom, inside Window.safe-area-insets. This // keeps the primary destinations in thumb reach without duplicating any // application state or business logic. component CompactProductNavItem inherits Rectangle { in property label; in property outline-icon; in property filled-icon; in property active: false; in property accent-color; in property active-background; in property muted-color; in property interactive: true; callback clicked; min-width: 64px; height: 64px; background: transparent; // The selected destination uses a compact indicator behind the icon. // Keeping the label outside it prevents the active item from looking like // an oversized card at narrow window widths. Rectangle { x: (parent.width - 56px) / 2; y: 6px; width: 56px; height: 32px; border-radius: 16px; background: root.active || nav-hit.has-hover ? root.active-background : transparent; animate background { duration: 140ms; easing: ease-out; } Image { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: root.active ? 24px : 22px; height: root.active ? 24px : 22px; source: root.active ? root.filled-icon : root.outline-icon; colorize: root.active ? root.accent-color : root.muted-color; animate colorize { duration: 140ms; easing: ease-out; } animate width, height { duration: 180ms; easing: spring(0.12); } image-fit: contain; accessible-role: none; } } // The label may use the item's full width and bottom inset as the // platform font grows. Text { x: 0px; y: 40px; width: parent.width; height: min(parent.height - self.y, max(18px, self.preferred-height)); text: root.label; color: root.active ? root.accent-color : root.muted-color; font-size: TypeScale.caption; font-weight: root.active ? 700 : 600; animate color { duration: 140ms; easing: ease-out; } horizontal-alignment: center; vertical-alignment: center; overflow: elide; } // Make the complete navigation item interactive, including the space // around its indicator and label. nav-hit := TouchArea { enabled: root.interactive; mouse-cursor: pointer; accessible-role: button; accessible-enabled: root.interactive; accessible-label: root.label; accessible-id: "mobile-nav-" + root.label.to-lowercase(); accessible-checked: root.active; accessible-action-default => { root.clicked(); } clicked => { root.clicked(); } } } export component CompactProductNav inherits Rectangle { in-out property active-view: "mail"; in property settings-active: false; in property background-color; in property divider-color; in property accent-color; in property accent-background; in property muted-color; in property interactive: true; callback open-settings; callback select-view(string); height: LayoutMetrics.compact-navigation-height; background: root.background-color; accessible-role: navigation; accessible-label: @tr("Primary navigation"); Rectangle { x: 0px; y: 0px; width: parent.width; height: 1px; background: root.divider-color; } HorizontalBox { x: 4px; y: 1px; width: parent.width - 8px; height: 79px; padding-top: 7px; padding-bottom: 8px; spacing: 2px; CompactProductNavItem { horizontal-stretch: 1; label: @tr("Mail"); outline-icon: @image-url("../icons/lucide/outline/mail.svg"); filled-icon: @image-url("../icons/lucide/filled/mail.svg"); active: !root.settings-active && root.active-view == "mail"; accent-color: root.accent-color; active-background: root.accent-background; muted-color: root.muted-color; interactive: root.interactive; clicked => { root.select-view("mail"); } } CompactProductNavItem { horizontal-stretch: 1; label: @tr("Calendar"); outline-icon: @image-url("../icons/lucide/outline/calendar.svg"); filled-icon: @image-url("../icons/lucide/filled/calendar.svg"); active: !root.settings-active && root.active-view == "calendar"; accent-color: root.accent-color; active-background: root.accent-background; muted-color: root.muted-color; interactive: root.interactive; clicked => { root.select-view("calendar"); } } CompactProductNavItem { horizontal-stretch: 1; label: @tr("Files"); outline-icon: @image-url("../icons/lucide/outline/files.svg"); filled-icon: @image-url("../icons/lucide/filled/files.svg"); active: !root.settings-active && root.active-view == "files"; accent-color: root.accent-color; active-background: root.accent-background; muted-color: root.muted-color; interactive: root.interactive; clicked => { root.select-view("files"); } } CompactProductNavItem { horizontal-stretch: 1; label: @tr("Contacts"); outline-icon: @image-url("../icons/lucide/outline/contacts.svg"); filled-icon: @image-url("../icons/lucide/filled/contacts.svg"); active: !root.settings-active && root.active-view == "contacts"; accent-color: root.accent-color; active-background: root.accent-background; muted-color: root.muted-color; interactive: root.interactive; clicked => { root.select-view("contacts"); } } CompactProductNavItem { horizontal-stretch: 1; label: @tr("Settings"); outline-icon: @image-url("../icons/lucide/outline/gear.svg"); filled-icon: @image-url("../icons/lucide/filled/gear.svg"); active: root.settings-active; accent-color: root.accent-color; active-background: root.accent-background; muted-color: root.muted-color; interactive: root.interactive; clicked => { root.open-settings(); } } } } // Tabs share the strip's height below a small top margin. global TabMetrics { out property top: 6px; // The active tab's top corner radius. out property radius: 8px; // How far the active tab's foot curves out on each side to meet the // content below. out property foot: 12px; // The line tab labels, the profile pill and "+" are centred on: a touch // above the tab's middle, as its foot is open, and in line with the // window controls. public pure function middle(height: length) -> length { return (height + root.top) / 2 - 2px; } } // The active tab: rounded top corners, and a foot that curves out on both // sides into the strip's bottom border. Its border rises out of that line, // runs round the tab and drops back into it; the open bottom joins the // content below. The border's centre line runs half a pixel inside the edge, // so the 1px line covers whole pixels at 1x and 2x. component ActiveTabShape inherits Rectangle { in property fill; in property stroke; private property w: self.width / 1px; private property h: self.height / 1px; private property r: TabMetrics.radius / 1px; private property f: TabMetrics.foot / 1px; // The border's centre line, half its width in from the edge. private property s: 0.5; // Both paths are drawn in place, as fitting would shrink the outline // inside its stroke. The fill reaches down through the strip's border, // hiding it under the tab. Path { width: parent.width; height: parent.height; fit: preserve; fill: root.fill; accessible-role: none; MoveTo { x: root.s; y: root.h; } LineTo { x: root.s; y: root.h - root.s; } ArcTo { x: root.f + root.s; y: root.h - root.s - root.f; radius-x: root.f; radius-y: root.f; sweep: false; } LineTo { x: root.f + root.s; y: root.r + root.s; } ArcTo { x: root.f + root.s + root.r; y: root.s; radius-x: root.r; radius-y: root.r; sweep: true; } LineTo { x: root.w - root.f - root.s - root.r; y: root.s; } ArcTo { x: root.w - root.f - root.s; y: root.r + root.s; radius-x: root.r; radius-y: root.r; sweep: true; } LineTo { x: root.w - root.f - root.s; y: root.h - root.s - root.f; } ArcTo { x: root.w - root.s; y: root.h - root.s; radius-x: root.f; radius-y: root.f; sweep: false; } LineTo { x: root.w - root.s; y: root.h; } Close {} } // The border is open at the bottom, where the tab meets the content. Path { width: parent.width; height: parent.height; fit: preserve; stroke: root.stroke; stroke-width: 1px; accessible-role: none; MoveTo { x: root.s; y: root.h - root.s; } ArcTo { x: root.f + root.s; y: root.h - root.s - root.f; radius-x: root.f; radius-y: root.f; sweep: false; } LineTo { x: root.f + root.s; y: root.r + root.s; } ArcTo { x: root.f + root.s + root.r; y: root.s; radius-x: root.r; radius-y: root.r; sweep: true; } LineTo { x: root.w - root.f - root.s - root.r; y: root.s; } ArcTo { x: root.w - root.f - root.s; y: root.r + root.s; radius-x: root.r; radius-y: root.r; sweep: true; } LineTo { x: root.w - root.f - root.s; y: root.h - root.s - root.f; } ArcTo { x: root.w - root.s; y: root.h - root.s; radius-x: root.f; radius-y: root.f; sweep: false; } } } // A tab: icon, title and close button. The active tab is the content's // color and curves into it; the others sit on the strip, divided by short // rules. component TabItem inherits Rectangle { in property entry; in property index; in property count; // Where the active tab is now; the saved row only describes inactive tabs. in property live-workspace; in property live-title; // Room this tab makes while another tab is dragged past it. in property shift; in property reordering; in property title-color; in property text-color; in property accent-color; // The color of the content the active tab opens into. in property active-background; in property hover-background; in property divider-color; // The rule on the right; hidden next to the active or a hovered tab. in property divided; out property hovered: hit.has-hover && !hit.dragging; // Horizontal travel while this tab is dragged, and where it was dropped. callback dragged(length); callback dropped(length); // The pointer is closing this tab. callback closing(); private property workspace: root.entry.active && root.live-workspace != "" ? root.live-workspace : root.entry.workspace; private property workspace-name: root.workspace == "calendar" ? @tr("Calendar") : root.workspace == "files" ? @tr("Files") : root.workspace == "contacts" ? @tr("Contacts") : root.workspace == "settings" ? @tr("Settings") : @tr("Mail"); private property title: root.entry.active ? (root.live-title != "" ? root.live-title : root.workspace-name) : (root.entry.title != "" ? root.entry.title : root.workspace-name); // Narrow tabs keep only their icon, as in a browser. private property labelled: root.width >= 72px; private property truncated: !root.labelled || label.preferred-width > label.width; private property tooltip-due; // A click hides the tooltip until the pointer leaves. private property tooltip-dismissed; private property offset: hit.dragging ? hit.travel : root.shift; function close() { root.closing(); TabsUi.close(root.entry.id); } function end-drag() { if hit.dragging { root.dragged(0px); } hit.dragging = false; hit.travel = 0px; } // The active tab's foot overlaps its neighbours. z: hit.dragging ? 2 : root.entry.active ? 1 : 0; changed hovered => { if !root.hovered { root.tooltip-due = false; root.tooltip-dismissed = false; } } // Rows update in place, so this element can come to show another tab, // which starts without the last one's drag or tooltip. private property entry-id: root.entry.id; changed entry-id => { root.end-drag(); root.tooltip-due = false; root.tooltip-dismissed = false; } face := Rectangle { x: root.offset; width: parent.width; height: parent.height; animate x { duration: MotionSettings.enabled && root.reordering && !hit.dragging ? 160ms : 0ms; easing: ease-out; } if root.entry.active : ActiveTabShape { x: -TabMetrics.foot; y: TabMetrics.top; width: parent.width + 2 * TabMetrics.foot; height: parent.height - TabMetrics.top; fill: root.active-background; stroke: root.divider-color; } if !root.entry.active : Rectangle { x: 2px; y: TabMetrics.middle(parent.height) - self.height / 2; width: parent.width - 4px; height: 30px; border-radius: TabMetrics.radius - 1px; background: root.hovered ? root.hover-background : transparent; animate background { duration: MotionSettings.enabled ? 120ms : 0ms; easing: ease-out; } } if root.divided : Rectangle { x: parent.width - 1px; y: TabMetrics.middle(parent.height) - self.height / 2; width: 1px; height: 16px; background: root.divider-color; } WorkspaceIcon { x: root.labelled ? 12px : (parent.width - self.width) / 2; y: TabMetrics.middle(parent.height) - self.height / 2; width: 14px; height: 14px; workspace: root.workspace; filled: root.entry.active; colorize: root.entry.active ? root.accent-color : root.text-color; } label := Text { visible: root.labelled; x: 34px; y: TabMetrics.middle(parent.height) - self.height / 2; width: max(0px, parent.width - self.x - 34px); height: 2 * (parent.height - TabMetrics.middle(parent.height)); text: root.title; color: root.entry.active || root.hovered ? root.title-color : root.text-color; font-size: TypeScale.secondary; font-weight: 500; vertical-alignment: center; overflow: elide; accessible-role: none; } } menu-area := ContextMenuArea { width: parent.width; height: parent.height; hit := TouchArea { width: parent.width; height: parent.height; property dragging; property travel; // The tab the left button went down on; this element can come to // show another while it is held. property pressed-id: -1; // A middle press here closes the tab when released over it. property middle-pressed; mouse-cursor: pointer; accessible-role: tab; accessible-label: root.title; accessible-item-selectable: true; accessible-item-selected: root.entry.active; accessible-item-index: root.index; accessible-action-default => { TabsUi.select(root.entry.id); } pointer-event(event) => { if event.kind == PointerEventKind.cancel { self.pressed-id = -1; self.middle-pressed = false; root.end-drag(); } else if event.kind == PointerEventKind.down && event.button != PointerEventButton.left && (self.pressed || self.dragging) { // Another button while the tab is held lets it go. self.pressed-id = -1; root.end-drag(); } else if event.button == PointerEventButton.left && event.kind == PointerEventKind.down { root.tooltip-dismissed = true; self.pressed-id = root.entry.id; TabsUi.select(root.entry.id); } else if event.button == PointerEventButton.left && event.kind == PointerEventKind.up { if self.dragging { root.dropped(self.travel); } self.dragging = false; self.travel = 0px; self.pressed-id = -1; } else if event.button == PointerEventButton.middle && event.kind == PointerEventKind.down { self.middle-pressed = true; } else if event.button == PointerEventButton.middle && event.kind == PointerEventKind.up { // Only when released over the tab it was pressed on. if self.middle-pressed && self.mouse-x >= 0px && self.mouse-x < self.width && self.mouse-y >= 0px && self.mouse-y < self.height { root.close(); } self.middle-pressed = false; } else if event.button == PointerEventButton.right && event.kind == PointerEventKind.down { menu-area.show({ x: self.mouse-x, y: self.mouse-y }); } } // The wheel leaves the tab and the strip hovered. scroll-event(event) => { return accept; } moved => { if self.pressed && self.pressed-id == root.entry.id && !self.dragging && abs(self.mouse-x - self.pressed-x) > 6px { self.dragging = true; } if self.dragging { // The tab stays within the strip. self.travel = max(-root.index * root.width, min((root.count - 1 - root.index) * root.width, self.mouse-x - self.pressed-x)); root.dragged(self.travel); } } // Inside the tab's hit area, so pointing at it keeps the tab hovered. if root.labelled && (root.entry.active || root.hovered) : Rectangle { x: root.offset + parent.width - 28px; y: TabMetrics.middle(parent.height) - self.height / 2; width: 20px; height: 20px; border-radius: 4px; background: close-hit.pressed ? root.title-color.with-alpha(0.18) : close-hit.has-hover ? root.title-color.with-alpha(0.10) : transparent; Image { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 12px; height: 12px; source: @image-url("../icons/lucide/outline/x.svg"); colorize: close-hit.has-hover ? root.title-color : root.text-color; accessible-role: none; } close-hit := TouchArea { mouse-cursor: pointer; accessible-role: button; accessible-label: @tr("Close tab"); accessible-action-default => { TabsUi.close(root.entry.id); } clicked => { root.close(); } // The button takes every press over it; other buttons act // as they do on the rest of the tab. pointer-event(event) => { if event.button == PointerEventButton.middle && event.kind == PointerEventKind.up && self.mouse-x >= 0px && self.mouse-x < self.width && self.mouse-y >= 0px && self.mouse-y < self.height { root.close(); } else if event.button == PointerEventButton.right && event.kind == PointerEventKind.down { menu-area.show({ x: parent.x + self.mouse-x, y: parent.y + self.mouse-y }); } } } } } Menu { MenuItem { title: @tr("New tab"); activated => { TabsUi.create(); } } MenuItem { title: @tr("Reopen closed tab"); enabled: TabsUi.can-reopen; activated => { TabsUi.reopen(); } } MenuSeparator {} MenuItem { title: @tr("Close tab"); activated => { TabsUi.close(root.entry.id); } } MenuItem { title: @tr("Close other tabs"); enabled: root.count > 1; activated => { TabsUi.close-others(root.entry.id); } } MenuItem { title: @tr("Close tabs to the right"); enabled: root.index < root.count - 1; activated => { TabsUi.close-to-right(root.entry.id); } } } } // The full title of a shortened tab, after a short hover. Timer { interval: 500ms; running: root.hovered && root.truncated && !root.tooltip-due && !root.tooltip-dismissed; triggered => { root.tooltip-due = true; } } if root.tooltip-due && root.hovered && root.truncated && !root.tooltip-dismissed : Rectangle { // Below the tab, kept inside the window. x: max(8px - root.absolute-position.x, min(8px, WindowChrome.window-width - root.absolute-position.x - self.width - 8px)); y: parent.height + 4px; width: min(tooltip-label.preferred-width + 16px, 480px); height: 28px; border-radius: 5px; background: #292929; tooltip-label := Text { x: 8px; width: parent.width - 16px; text: root.title; color: white; font-size: TypeScale.secondary; vertical-alignment: center; overflow: elide; } } } // The new-tab button. Like the tabs, it leaves keyboard focus where it is. component NewTabButton inherits Rectangle { in property hover-background; in property icon-color; out property hovered: hit.has-hover; width: 32px; height: 32px; border-radius: LayoutMetrics.corner-radius - 2px; background: hit.has-hover ? root.hover-background : transparent; animate background { duration: MotionSettings.enabled ? 120ms : 0ms; easing: ease-out; } Image { x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 16px; height: 16px; source: @image-url("../icons/lucide/outline/plus.svg"); colorize: root.icon-color; accessible-role: none; } hit := TouchArea { mouse-cursor: pointer; accessible-role: button; accessible-label: @tr("New tab"); accessible-action-default => { TabsUi.create(); } clicked => { TabsUi.create(); } } if hit.has-hover : Rectangle { // Centred below the button, kept inside the window. x: max(8px - root.absolute-position.x, min((root.width - self.width) / 2, WindowChrome.window-width - root.absolute-position.x - self.width - 8px)); y: parent.height + 6px; width: tooltip-label.preferred-width + 16px; height: 28px; border-radius: 5px; background: #292929; tooltip-label := Text { text: @tr("New tab"); color: white; font-size: TypeScale.secondary; horizontal-alignment: center; vertical-alignment: center; } } } // Desktop top bar with the tabs. It spans the window above the rail and the // sidebar: leading window controls, the profile pill, then the tabs. // Trailing controls sit in its right corner. export component WindowTopBar inherits Rectangle { // The active tab's live place: "settings" or a workspace, and its title. in property live-workspace; in property live-title; in property background-color; // The color the active tab opens into: the panes' shared surface below. in property active-tab-background; in property hover-background; in property divider-color; in property title-color; in property text-color; in property accent-color; in property <[MailProfileRow]> profiles; in property active-profile; in property account-count; callback choose-profile(string); callback create-profile(); callback manage-profiles(); background: root.background-color; accessible-role: window-title-bar; accessible-label: @tr("Flectar Mail"); // The pill follows the leading window controls, and the tabs the pill. private property pill-x: max(8px, WindowChrome.leading-width); // The first tab stands 8px from the pill, about the pill's own inset from // the bar's edges; its foot curves out below the pill's end. Tabs start // and end on whole pixels, so their borders stay sharp. private property tabs-x: ((profile-pill.x + profile-pill.width) / 1px).ceil() * 1px + 8px; private property trailing: WindowChrome.trailing-width + 8px; private property count: TabsUi.rows.length; // Tabs share the room up to the new-tab button. In the integrated frame // some of the bar stays free to drag the window by, however many tabs. // At the narrowest desktop width, 32 tabs still fit as icons. private property drag-room: WindowChrome.controls-visible ? 48px : 0px; private property tabs-room: max(0px, root.width - root.tabs-x - 44px - root.drag-room - root.trailing); private property fitted-slot-width: root.count == 0 ? 0px : max(22px, min(240px, (root.tabs-room / root.count / 1px).floor() * 1px)); // Tabs closed one after another keep their width, and the strip its // length, while the pointer stays on it or on "+", so the next close // button is still under the pointer. Tabs opened meanwhile still fit. private property held-slot-width: 0px; private property held-strip-width: 0px; private property slot-width: root.held-slot-width > 0px ? min(root.held-slot-width, root.fitted-slot-width) : root.fitted-slot-width; private property strip-hovered: strip.has-hover || new-tab.hovered; changed strip-hovered => { if !root.strip-hovered { root.held-slot-width = 0px; root.held-strip-width = 0px; } } // The dragged tab, its travel, and the slot it would land in. private property drag-index: -1; private property drag-travel; private property drop-index: root.drag-index < 0 ? -1 : max(0, min(root.count - 1, round(root.drag-index + root.drag-travel / root.slot-width))); // A tab closed during a drag may have been the dragged one. changed count => { root.drag-index = -1; root.drag-travel = 0px; } // The hovered tab hides its rules like the active one. private property hovered-index: -1; if WindowChrome.controls-visible : WindowDragArea { width: parent.width; height: parent.height; } // The bottom border runs under every tab except the active one. Rectangle { y: parent.height - 1px; height: 1px; background: root.divider-color; } profile-pill := ProfileSwitcher { x: root.pill-x; y: TabMetrics.middle(parent.height) - self.height / 2; profiles: root.profiles; active: root.active-profile; account-count: root.account-count; choose(id) => { root.choose-profile(id); } create-profile => { root.create-profile(); } manage-profiles => { root.manage-profiles(); } } // With tabs turned off the bar keeps the profile pill and, in the // integrated frame, the window controls. strip := TouchArea { visible: TabsUi.enabled; x: root.tabs-x; width: max(root.count * root.slot-width, min(root.held-strip-width, root.tabs-room)); height: parent.height; accessible-role: tab-list; accessible-label: @tr("Tabs"); accessible-item-count: root.count; for tab-row[index] in TabsUi.rows : TabItem { x: index * root.slot-width; width: root.slot-width; height: parent.height; entry: tab-row; index: index; count: root.count; live-workspace: root.live-workspace; live-title: root.live-title; reordering: root.drag-index >= 0; shift: root.drag-index < 0 || index == root.drag-index ? 0px : index > root.drag-index && index <= root.drop-index ? -root.slot-width : index < root.drag-index && index >= root.drop-index ? root.slot-width : 0px; // Rules only separate two resting tabs. divided: index < root.count - 1 && root.drag-index < 0 && !tab-row.active && !TabsUi.rows[index + 1].active && root.hovered-index != index && root.hovered-index != index + 1; title-color: root.title-color; text-color: root.text-color; accent-color: root.accent-color; active-background: root.active-tab-background; hover-background: root.hover-background; divider-color: root.divider-color; changed hovered => { if self.hovered { root.hovered-index = index; } else if root.hovered-index == index { root.hovered-index = -1; } } dragged(travel) => { root.drag-index = travel == 0px ? -1 : index; root.drag-travel = travel; } dropped(travel) => { root.drag-index = index; root.drag-travel = travel; let target = root.drop-index; root.drag-index = -1; root.drag-travel = 0px; if target != index { TabsUi.move(tab-row.id, target); } } closing => { root.held-slot-width = root.slot-width; root.held-strip-width = strip.width; } } } new-tab := NewTabButton { visible: TabsUi.enabled; x: root.tabs-x + strip.width + TabMetrics.foot; y: TabMetrics.middle(parent.height) - self.height / 2; hover-background: root.hover-background; icon-color: root.text-color; } }