slint/ui-libraries/material/ui/components/search_bar.slint
2025-09-18 15:47:22 +02:00

312 lines
9.8 KiB
Text

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ListItem } from "../items/list_item.slint";
import { Avatar, ListTile } from "./list.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { Typography } from "../styling/typography.slint";
import { StateLayerArea } from "./state_layer.slint";
import { MaterialText } from "./material_text.slint";
import { Icon } from "./icon.slint";
import { IconButton } from "./icon_button.slint";
import { Icons } from "../icons/icons.slint";
import { HorizontalDivider } from "./divider.slint";
import { ListView } from "./list_view.slint";
import { Animations } from "../styling/animations.slint";
component SearchListTile inherits ListTile {
in property <image> action_icon;
callback action();
if root.action_icon.width > 0 && root.action_icon.height > 0 : IconButton {
icon: root.action_icon;
clicked => {
root.action();
}
}
}
component SearchIcon {
in property <image> icon;
in property <color> color;
VerticalLayout {
alignment: center;
padding_left: MaterialStyleMetrics.padding_8;
padding_right: self.padding_left;
Icon {
source: root.icon;
colorize: root.color;
}
}
}
component SearchTextInput {
in_out property <string> text <=> text_input.text;
in property <string> placeholder;
property <length> computed_x;
callback accepted(text: string);
callback edited(text: string);
callback key_pressed(event: KeyEvent) -> EventResult;
callback key_released(event: KeyEvent) -> EventResult;
forward_focus: text_input;
horizontal_stretch: 1;
Rectangle {
clip: true;
text_input := TextInput {
x: min(0px, max(parent.width - self.width - self.text_cursor_width, root.computed_x));
width: max(parent.width - self.text-cursor-width, self.preferred-width);
height: 100%;
font_size: Typography.body_large.font_size;
font_weight: Typography.body_large.font_weight;
vertical_alignment: center;
single_line: true;
color: MaterialPalette.on_surface;
selection_background_color: MaterialPalette.secondary_container;
selection_foreground_color: self.color;
// Disable TextInput's built-in accessibility support as the component takes care of that.
accessible-role: none;
cursor_position_changed(cursor_position) => {
if cursor_position.x + root.computed_x < 0 {
root.computed_x = - cursor_position.x;
} else if cursor-position.x + root.computed_x > parent.width - self.text-cursor-width {
root.computed_x = parent.width - cursor_position.x - self.text-cursor-width;
}
}
accepted => {
root.accepted(self.text);
}
edited => {
root.edited(self.text);
}
key_pressed(event) => {
root.key_pressed(event)
}
key_released(event) => {
root.key_released(event)
}
}
if root.text == "" && root.placeholder != "" : MaterialText {
width: 100%;
height: 100%;
text: root.placeholder;
color: MaterialPalette.on_surface_variant;
vertical_alignment: center;
style: Typography.body_large;
}
}
}
export component SearchBar {
in property <image> leading_icon: Icons.menu;
in property <image> trailing_icon;
in property <image> avatar;
in property <color> avatar_background: #00000000;
in property <string> placeholder;
in property <string> empty_text;
in_out property <string> text;
in_out property <int> current_item;
in property <[ListItem]> items;
callback accepted(text: string);
callback edited(text: string);
callback action(index: int);
callback key_pressed(event: KeyEvent) -> EventResult;
callback key_released(event: KeyEvent) -> EventResult;
property <color> color: MaterialPalette.on_surface_variant;
property <length> item_height: MaterialStyleMetrics.size_72;
min_height: max(MaterialStyleMetrics.size_56, layout.min_height);
forward_focus: state_layer;
Rectangle {
background: MaterialPalette.surface_container_high;
border_radius: MaterialStyleMetrics.border_radius_28;
state_layer := StateLayerArea {
border_radius: parent.border_radius;
color: root.color;
layout := HorizontalLayout {
padding: MaterialStyleMetrics.padding_4;
spacing: MaterialStyleMetrics.spacing_4;
SearchIcon {
icon: root.leading_icon;
color: root.color;
}
MaterialText {
text: root.text;
font_size: Typography.body_large.font_size;
font_weight: Typography.body_large.font_weight;
vertical_alignment: center;
color: MaterialPalette.on_surface;
states [
placeholder when root.text == "" : {
text: root.placeholder;
color: MaterialPalette.on_surface;
}
]
}
SearchIcon {
icon: root.trailing_icon;
color: root.color;
}
if (root.avatar.width > 0 && root.avatar.height > 0) || root.avatar_background != #00000000 : VerticalLayout {
alignment: center;
Avatar {
image: root.avatar;
background: root.avatar_background;
}
}
}
changed has_focus => {
if self.has_focus {
popup.show();
}
}
clicked => {
popup.show();
}
}
}
popup := PopupWindow {
x: 0;
y: 0;
width: root.width;
height: root.height + clamp(root.item_height * root.items.length, 3 * root.item_height, 6 * root.item_height);
close_policy: close_on_click_outside;
forward-focus: popup_input;
background_layer := Rectangle {
x: 0;
y: 0;
width: 100%;
height: root.height;
background: MaterialPalette.surface_container_high;
border_radius: MaterialStyleMetrics.border_radius_28;
clip: true;
VerticalLayout {
HorizontalLayout {
padding: MaterialStyleMetrics.padding_4;
spacing: MaterialStyleMetrics.spacing_4;
IconButton {
icon: Icons.arrow_back;
clicked => {
popup.close();
}
}
popup_input := SearchTextInput {
placeholder: root.placeholder;
text: root.text;
accepted => {
root.accepted(self.text);
}
edited => {
root.text = self.text;
root.edited(self.text);
}
key_pressed(event) => {
root.key_pressed(event)
}
key_released(event) => {
root.key_released(event)
}
changed text => {
root.text = text;
}
}
IconButton {
icon: Icons.close;
clicked => {
root.text = "";
popup.close();
}
}
}
HorizontalDivider {}
if root.items.length == 0 : MaterialText {
text: root.empty_text;
color: root.color;
horizontal_alignment: center;
style: Typography.label_small;
}
if root.items.length == 0 : Rectangle {}
if root.items.length > 0 : ListView {
horizontal_stretch: 1;
for item[index] in root.items : SearchListTile {
text: item.text;
supporting_text: item.supporting_text;
avatar_icon: item.avatar_icon;
avatar_text: item.avatar_text;
avatar_background: item.avatar_background;
avatar_foreground: item.avatar_foreground;
action_icon: item.action_icon;
clicked => {
root.text = self.text;
popup.close();
}
action => {
root.action(index);
}
}
}
}
animate height {
duration: Animations.standard_accelerate_duration;
easing: Animations.standard_easing;
}
}
Timer {
interval: 50ms;
triggered => {
background_layer.height = popup.height;
self.running = false;
}
}
}
}