// SPDX-FileCopyrightText: 2024 vivi developers // SPDX-License-Identifier: MIT import { BorderStyle, Border, TextStyle, IconStyle, LayoutStyle, HorizontalLayoutBase } from "../foundation.slint"; import { ListView } from "./list_view.slint"; import { MagicText } from "./magic_text.slint"; import { MagicIcon } from "./magic_icon.slint"; import { StateLayer } from "./state_layer.slint"; import { MagicSizeSettings, MagicPalette, MagicFontSettings, MagicLayoutSettings, MagicIconSettings, MagicBorderSettings } from "./styling.slint"; export struct MagicListViewItem { prefix_icon: image, text: string, supporting_text: string } export struct MagicListViewItemStyle { // background_layer border_style: BorderStyle, // content_layer text_style: TextStyle, supporting_text_style: TextStyle, icon_style: IconStyle, // layout layout_style: LayoutStyle } export struct MagicListViewStyle { // background_layer border_style: BorderStyle, // content_layer item_style: MagicListViewItemStyle } component MagicListViewItemTemplate { in property prefix_icon; in property text; in property supporting_text; in property selected; in property style; in property has_focus; in property enabled: true; property icon_foreground: root.style.icon_style.foreground; property supporting_foreground: root.style.supporting_text_style.foreground; callback clicked <=> touch_area.clicked; min_width: content_layer.min_width; height: max(MagicSizeSettings.item_height, content_layer.min_height); touch_area := TouchArea { width: background_layer.width; height: background_layer.height; mouse_cursor: MouseCursor.pointer; enabled: root.enabled; } background_layer := Border { width: root.width - 8px; height: root.height - 5px; style: root.style.border_style; } state_layer := StateLayer { width: background_layer.width; height: background_layer.height; border_radius: root.style.border_style.border_radius; pressed: touch_area.pressed; has_hover: touch_area.has_hover; has_focus: root.enabled && root.has_focus; focus_padding: 0; } content_layer := HorizontalLayoutBase { style: root.style.layout_style; if root.prefix_icon.width > 0 && root.prefix_icon.height > 0 : MagicIcon { y: (parent.height - self.height) / 2; icon: root.prefix_icon; style: root.style.icon_style; colorize: root.icon_foreground; } VerticalLayout { alignment: center; text_label := MagicText { text: root.text; style: root.style.text_style; horizontal_alignment: left; } if root.supporting_text != "" : MagicText { text: root.supporting_text; style: root.style.supporting_text_style; color: root.supporting_foreground; } } } states [ selected when root.selected : { background_layer.background: MagicPalette.foreground; text_label.color: MagicPalette.background; root.supporting_foreground: MagicPalette.background.with_alpha(0.9); root.icon_foreground: MagicPalette.background; } ] } component MagicListViewBase inherits ListView { in property <[MagicListViewItem]> model; in_out property current_index: -1; out property current_value: root.model[root.current_index]; in property item_style: { border_style: { background: MagicPalette.transparent, border_radius: MagicBorderSettings.box_border_radius }, text_style: MagicFontSettings.body, supporting_text_style: MagicFontSettings.supporting, icon_style: { icon_size: MagicIconSettings.body.icon_size, foreground: MagicPalette.foreground, }, layout_style: { padding_top: MagicLayoutSettings.alternate_layout_padding, padding_bottom: MagicLayoutSettings.alternate_layout_padding, padding_left: MagicLayoutSettings.control_padding, padding_right: MagicLayoutSettings.control_padding, spacing: MagicLayoutSettings.layout_spacing, } }; property item_height: MagicSizeSettings.item_height; property into_view_item: root.current_index; property into_view_item_y: root.item_y(root.into_view_item); property focus_index: 0; property current_item_y: root.item_y(root.focus_index); callback selected(/* index */ int); for item[index] in root.model : MagicListViewItemTemplate { prefix_icon: item.prefix_icon; text: item.text; supporting_text: item.supporting_text; selected: index == root.current_index; style: root.item_style; has_focus: root.has_focus && index == root.focus_index; enabled: root.enabled; clicked => { root.select(index); } } function select(index: int) { if root.current_index == index { return; } root.current_index = min(max(0, index), root.model.length - 1); root.focus_index = root.current_index; root.bring_into_view(root.current_index); root.selected(root.current_index); } pure function item_y(index: int) -> length { root.viewport_y + index * root.item_height } function bring_into_view(index: int) { if index < 0 || index >= root.model.length { return; } root.into_view_item = index; if root.into_view_item_y < 0 { root.viewport_y += 0 - root.into_view_item_y; } if root.into_view_item_y + item_height > root.visible_height { root.viewport_y -= into_view_item_y + item_height - root.viewport_height; } } pure function first_visible_item() -> int { return min(root.model.length - 1, max(0, round(-root.viewport_y / root.item_height))); } protected function focus_up() { root.set_focus_index(root.focus_index - 1); } protected function focus_down() { root.set_focus_index(root.focus_index + 1); } protected function select_focus_index() { root.select(root.focus_index); } protected function focus_current_item() { root.focus_index = max(0, root.current_index); if (root.current_item_y + root.item_height < 0 || root.current_item_y > root.height) { root.focus_index = root.first_visible_item(); } } protected function set_focus_index(index: int) { root.focus_index = min(root.model.length - 1, max(0, index)); root.bring_into_view(root.focus_index); } states [ disabled when !root.enabled : { root.opacity: 0.5; } ] } export component MagicListView inherits MagicListViewBase { forward_focus: focus_scope; has_focus: focus_scope.has_focus; focus_scope := FocusScope { x: 0; width: 0; enabled: root.enabled; focus_changed_event => { root.focus_current_item(); root.has-focus = self.has-focus; } key_pressed(event) => { if event.text == Key.UpArrow { root.focus_up(); return accept; } else if event.text == Key.DownArrow { root.focus_down(); return accept; } else if event.text == Key.Return { root.select_focus_index(); return accept; } reject } } }