import { SidebarRowSurface, SidebarStyle } from "sidebar-row.slint"; import { MailDragApi } from "../models.slint"; import { Avatar } from "avatar.slint"; import { TypeScale } from "../typography.slint"; component SidebarIcon { in property icon; in property color; in property icon-size: SidebarStyle.icon-size; width: SidebarStyle.icon-size; height: 28px; Image { x: (parent.width - root.icon-size) / 2; y: (parent.height - root.icon-size) / 2; width: root.icon-size; height: root.icon-size; source: root.icon; colorize: root.color; image-fit: contain; accessible-role: none; } } // Counts are compact status badges rather than loose right-aligned text. This // keeps large unread values visually grouped and gives selected rows a clear // accent treatment without changing the row's geometry. component SidebarCountBadge inherits Rectangle { in property count; in property selected: false; in property accent-color; in property muted-color; visible: root.count != ""; width: min(52px, max(24px, count-label.preferred-width + 12px)); height: 20px; border-radius: 7px; background: root.selected ? root.accent-color.transparentize(0.78) : root.muted-color.transparentize(0.86); count-label := Text { text: root.count; color: root.selected ? root.accent-color : root.muted-color; font-size: TypeScale.caption; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; } } // A row's pointer area, which sits inside the row's ContextMenuArea so taps // reach it first: on Android, Slint holds every press on a menu's own area to // wait for a long press, which swallowed taps on the rows above it. A // right-click opens the menu, and so does holding a finger still for half a // second, as on Android and iOS. component SidebarRowHit inherits TouchArea { in property menu-enabled; callback activated(); callback open-menu(Point); private property holding: false; private property held: false; private property hold-x; private property hold-y; pointer-event(event) => { if event.kind == PointerEventKind.down { self.held = false; if event.button == PointerEventButton.right && root.menu-enabled { root.open-menu({ x: self.mouse-x, y: self.mouse-y }); } else if event.button == PointerEventButton.left && event.touch-finger-id != 0 && root.menu-enabled { self.hold-x = self.mouse-x; self.hold-y = self.mouse-y; self.holding = true; } } else if event.kind == PointerEventKind.move { if self.holding && (abs(self.mouse-x - self.hold-x) > 8px || abs(self.mouse-y - self.hold-y) > 8px) { self.holding = false; } } else { self.holding = false; } } clicked => { // The finger that opened the menu lifts here; it was not a tap. if !self.held { root.activated(); } self.held = false; } Timer { interval: 500ms; running: root.holding; triggered => { root.holding = false; root.held = true; root.open-menu({ x: root.hold-x, y: root.hold-y }); } } } // A folder row is shared by unified and account sections so indentation, // selection, hover, and counts remain consistent throughout the tree. export component SidebarItem inherits SidebarRowSurface { in property interactive: true; in property label; in property accessible-name: root.label; in property count; in property scope; in property icon; in property selected: false; in property label-has-emoji: false; in property label-path: root.label; in property row-height: 28px; in property left-padding: SidebarStyle.icon-left; in property account-id: -1; in property folder-id: -1; in property label-id: -1; in property has-children: false; in property expanded: true; in property label-tree-node: false; in property folder-menu-enabled: false; in property can-create-child: false; in property can-rename: false; in property can-delete: false; in property can-empty-trash: false; in property label-manageable: false; in property label-can-create-child: false; in property label-is-global: false; in property label-account-name: ""; in property mail-drop-enabled: false; in property mail-drop-copies: false; in property sidebar-background; in property selection-background; in property accent-color; in property body-color; in property muted-color; in property icon-color: muted-color; in property editable-color: icon-color; callback clicked(string); callback toggled(int, bool); callback new-subfolder(int, int, string); callback rename-folder(int, string); callback delete-folder(int, string); callback empty-trash(int); callback new-child-label(int, int, string, string); callback rename-label(int, string, color, string, bool); callback delete-label(int, string, string, bool, bool); height: root.visible ? root.row-height : 0px; idle-background: root.sidebar-background; hover-background: root.selection-background; hovered: row-hit.has-hover; active: root.selected || mail-drop-target.has-drag; border-width: mail-drop-target.has-drag ? 2px : 0px; border-color: root.accent-color; SidebarIcon { x: root.left-padding; y: 0px; height: root.row-height; icon: root.icon; color: root.icon-color; } if root.has-children: Image { x: root.left-padding - 18px; y: (root.row-height - self.height) / 2; width: 12px; height: 12px; source: root.expanded ? @image-url("../icons/lucide/outline/chevron-down.svg") : @image-url("../icons/lucide/outline/chevron-right.svg"); colorize: root.muted-color; image-fit: contain; accessible-role: none; } Text { x: root.left-padding + SidebarStyle.label-offset; y: 0px; width: max(0px, parent.width - self.x - (root.count == "" ? 6px : count-badge.width + 12px)); height: root.row-height; text: root.label; color: root.body-color; font-size: SidebarStyle.label-size; font-weight: root.selected ? SidebarStyle.selected-weight : SidebarStyle.normal-weight; font-family: root.label-has-emoji ? "Noto Emoji" : ""; vertical-alignment: center; overflow: elide; } count-badge := SidebarCountBadge { x: parent.width - self.width - 6px; y: (root.row-height - self.height) / 2; count: root.count; selected: root.selected; accent-color: root.accent-color; muted-color: root.muted-color; } // Folder and label actions. Rows without any keep the area disabled. row-menu := ContextMenuArea { enabled: root.interactive && (root.folder-menu-enabled || root.can-empty-trash || root.label-manageable); width: parent.width; height: parent.height; Menu { if root.folder-menu-enabled: MenuItem { title: @tr("New subfolder"); enabled: root.can-create-child; activated => { root.new-subfolder(root.account-id, root.folder-id, root.label); } } if root.folder-menu-enabled: MenuItem { title: @tr("Rename folder"); enabled: root.can-rename; activated => { root.rename-folder(root.folder-id, root.label); } } if root.folder-menu-enabled: MenuItem { title: @tr("Delete folder"); enabled: root.can-delete; activated => { root.delete-folder(root.folder-id, root.label); } } if root.can-empty-trash: MenuItem { title: @tr("Empty Trash…"); activated => { root.empty-trash(root.account-id); } } if root.label-manageable && root.label-can-create-child: MenuItem { title: @tr("New nested label"); activated => { root.new-child-label(root.account-id, root.label-id, root.label-path, root.label-account-name); } } if root.label-manageable: MenuItem { title: root.label-is-global ? @tr("Rename global label") : @tr("Rename label in {}", root.label-account-name); activated => { root.rename-label(root.label-id, root.label-path, root.editable-color, root.label-account-name, root.label-is-global); } } if root.label-manageable: MenuItem { title: root.label-is-global ? @tr("Delete global label") : @tr("Delete label from {}", root.label-account-name); activated => { root.delete-label(root.label-id, root.label-path, root.label-account-name, root.label-is-global, root.has-children); } } } row-hit := SidebarRowHit { enabled: root.interactive; menu-enabled: row-menu.enabled; accessible-role: button; accessible-label: root.accessible-name; accessible-checked: root.selected; accessible-action-default => { root.clicked(root.scope); } activated => { root.clicked(root.scope); } open-menu(position) => { row-menu.show(position); } mouse-cursor: pointer; } } if root.has-children: TouchArea { enabled: root.interactive; x: max(0px, root.left-padding - 22px); y: 0px; width: 22px; height: root.row-height; accessible-role: button; accessible-label: root.label-tree-node ? (root.expanded ? @tr("Collapse label") : @tr("Expand label")) : (root.expanded ? @tr("Collapse folder") : @tr("Expand folder")); accessible-expanded: root.expanded; accessible-action-default => { root.toggled(root.folder-id, !root.expanded); } clicked => { root.toggled(root.folder-id, !root.expanded); } mouse-cursor: pointer; } mail-drop-target := DropArea { width: parent.width; height: parent.height; enabled: root.interactive && root.mail-drop-enabled; can-drop(event) => { MailDragApi.can-drop-on-mailbox( event.data, root.scope, root.account-id, root.folder-id, ) ? (root.mail-drop-copies ? DragAction.copy : DragAction.move) : DragAction.none } dropped(event) => { if MailDragApi.drop-on-mailbox( event.data, root.scope, root.account-id, root.folder-id, ) { return root.mail-drop-copies ? DragAction.copy : DragAction.move; } return DragAction.none; } } } // Expansion belongs to the host model so recycled headers retain their state. export component SidebarSectionHeader inherits SidebarRowSurface { in property interactive: true; in property label; in property detail; // A collapsed account shows the unread mail of the folders it hides. in property count; in property avatar; in property avatar-image; in property has-avatar-image: false; in property icon; in property has-avatar: false; in property open: true; in property sidebar-background; in property body-color; in property muted-color; in property icon-color; in property label-font-weight: SidebarStyle.selected-weight; in property left-padding: 4px; in property folder-row-style: false; // An account's heading offers that account's actions on right-click. in property account-menu: false; in property can-check-mail: true; // Headers and children share the same icon and text columns. The leading // gutter belongs to disclosure controls, not to an extra icon indent. private property icon-left: root.left-padding + SidebarStyle.icon-left - 4px; private property label-left: root.icon-left + SidebarStyle.label-offset; private property trailing: root.count == "" ? 8px : count-badge.width + 12px; callback toggled(bool); callback check-mail(); callback account-settings(); height: root.folder-row-style ? 28px : root.detail == "" ? 32px : 7px + max(20px, header-label.preferred-height) + header-detail.height; idle-background: root.sidebar-background; hovered: header-hit.has-hover; Image { x: root.left-padding; y: (parent.height - self.height) / 2; width: 12px; height: 12px; source: root.open ? @image-url("../icons/lucide/outline/chevron-down.svg") : @image-url("../icons/lucide/outline/chevron-right.svg"); colorize: root.muted-color; image-fit: contain; accessible-role: none; } if root.has-avatar: Avatar { x: root.icon-left + (SidebarStyle.icon-size - self.width) / 2; y: (parent.height - self.height) / 2; size: 22px; source: root.avatar-image; has-source: root.has-avatar-image; fallback: root.avatar; background-color: root.has-avatar-image ? #ffffff : root.hover-background; foreground-color: root.body-color; } if !root.has-avatar: SidebarIcon { x: root.icon-left; y: (parent.height - self.height) / 2; height: 22px; icon: root.icon; color: root.icon-color; } header-label := Text { x: root.label-left; y: root.detail == "" ? 0px : 4px; width: parent.width - self.x - root.trailing; height: root.detail == "" ? parent.height : max(20px, self.preferred-height); text: root.label; color: root.body-color; font-size: SidebarStyle.label-size; font-weight: root.label-font-weight; vertical-alignment: center; overflow: elide; } header-detail := Text { visible: root.detail != ""; x: root.label-left; y: header-label.y + header-label.height - 2px; width: parent.width - self.x - root.trailing; height: max(15px, self.preferred-height); text: root.detail; color: root.muted-color; font-size: TypeScale.caption; vertical-alignment: center; overflow: elide; } count-badge := SidebarCountBadge { x: parent.width - self.width - 6px; y: (parent.height - self.height) / 2; count: root.count; accent-color: root.muted-color; muted-color: root.muted-color; } heading-menu := ContextMenuArea { enabled: root.interactive && root.account-menu; width: parent.width; height: parent.height; Menu { MenuItem { title: @tr("Check for new mail"); enabled: root.can-check-mail; activated => { root.check-mail(); } } MenuItem { title: @tr("Account settings"); activated => { root.account-settings(); } } } header-hit := SidebarRowHit { enabled: root.interactive; menu-enabled: heading-menu.enabled; accessible-role: button; accessible-label: root.label; accessible-expanded: root.open; accessible-action-default => { root.toggled(!root.open); } activated => { root.toggled(!root.open); } open-menu(position) => { heading-menu.show(position); } mouse-cursor: pointer; } } }