From 31ccf5b16d0ae301abf29f3fb1a683d7e5dcaca3 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Thu, 3 Apr 2025 17:51:14 +0800 Subject: [PATCH 01/69] added highlight --- Cargo.lock | 1 + Cargo.toml | 1 + src/home/main_desktop_ui.rs | 2 +- src/home/mod.rs | 2 + src/home/new_message_context_menu.rs | 39 +- src/home/room_screen.rs | 175 ++++-- src/home/room_search_result.rs | 784 +++++++++++++++++++++++++++ src/shared/search_bar.rs | 1 + src/sliding_sync.rs | 36 +- 9 files changed, 1003 insertions(+), 38 deletions(-) create mode 100644 src/home/room_search_result.rs diff --git a/Cargo.lock b/Cargo.lock index eabcca56..e452184c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3459,6 +3459,7 @@ dependencies = [ "robius-location", "robius-open", "robius-use-makepad", + "ruma", "serde", "serde_json", "tokio", diff --git a/Cargo.toml b/Cargo.toml index aab6616f..6cebb795 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ bytesize = "1.3.0" bitflags = "2.6.0" indexmap = "2.6.0" blurhash = { version = "0.2.3", default-features = false } +ruma = { git = "https://github.com/ruma/ruma", rev = "7755c7cbc580f8d8aea30d78cc1a6850b1a6fd39", default-features = false, features = ["api", "client-api-c"] } [package.metadata.docs.rs] all-features = true diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index ff195bf6..f50bd25d 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -169,7 +169,7 @@ impl MainDesktopUI { // create a new tab for the room let (tab_bar, _pos) = dock.find_tab_bar_of_tab(live_id!(home_tab)).unwrap(); - let kind = live_id!(room_screen); + let kind: LiveId = live_id!(room_screen); let result = dock.create_and_select_tab( cx, diff --git a/src/home/mod.rs b/src/home/mod.rs index 8c1fdf79..771b6b9b 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -9,6 +9,7 @@ pub mod main_mobile_ui; pub mod room_preview; pub mod room_screen; pub mod room_read_receipt; +pub mod room_search_result; pub mod rooms_list; pub mod rooms_sidebar; pub mod spaces_dock; @@ -24,6 +25,7 @@ pub fn live_design(cx: &mut Cx) { editing_pane::live_design(cx); new_message_context_menu::live_design(cx); room_screen::live_design(cx); + room_search_result::live_design(cx); room_read_receipt::live_design(cx); rooms_sidebar::live_design(cx); main_mobile_ui::live_design(cx); diff --git a/src/home/new_message_context_menu.rs b/src/home/new_message_context_menu.rs index 0c8d6dfe..a1e555d9 100644 --- a/src/home/new_message_context_menu.rs +++ b/src/home/new_message_context_menu.rs @@ -5,10 +5,11 @@ use bitflags::bitflags; use makepad_widgets::*; use matrix_sdk::ruma::OwnedEventId; use matrix_sdk_ui::timeline::EventTimelineItem; +use ruma::events::{AnyMessageLikeEventContent, AnyTimelineEvent}; use crate::sliding_sync::UserPowerLevels; -use super::room_screen::{MessageAction, MessageOrSticker}; +use super::{room_screen::{MessageAction, MessageOrSticker}, room_search_result}; const BUTTON_HEIGHT: f64 = 30.0; // KEEP IN SYNC WITH BUTTON_HEIGHT BELOW const MENU_WIDTH: f64 = 215.0; // KEEP IN SYNC WITH MENU_WIDTH BELOW @@ -306,6 +307,42 @@ impl MessageAbilities { abilities } + pub fn from_user_power_and_any_event( + user_power_levels: &UserPowerLevels, + event_tl_item: &AnyTimelineEvent, + _message: &room_search_result::MessageOrSticker, + has_html: bool, + ) -> Self { + let (is_editable) = { + match event_tl_item { + AnyTimelineEvent::MessageLike(msg) => { + match msg.original_content() { + Some(AnyMessageLikeEventContent::RoomMessage(message)) => { + false + } + _ => false + } + } + AnyTimelineEvent::State(state) => false + } + }; + let mut abilities = Self::empty(); + abilities.set(Self::CanEdit, is_editable); + // abilities.set(Self::CanEdit, event_tl_item.is_editable()); + // // Currently we only support deleting one's own messages. + // if event_tl_item.is_own() { + // abilities.set(Self::CanDelete, user_power_levels.can_redact_own()); + // } + // abilities.set(Self::CanReplyTo, event_tl_item.can_be_replied_to()); + // abilities.set(Self::CanPin, user_power_levels.can_pin()); + // // TODO: currently we don't differentiate between pin and unpin, + // // but we should first check whether the given message is already pinned + // // before deciding which ability to set. + // // abilities.set(Self::CanUnPin, user_power_levels.can_pin_unpin()); + // abilities.set(Self::CanReact, user_power_levels.can_send_reaction()); + // abilities.set(Self::HasHtml, has_html); + abilities + } } /// Details about the message that define its context menu content. diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 7b1e9d73..594e259c 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1,7 +1,7 @@ //! A room screen is the UI page that displays a single Room's timeline of events/messages //! along with a message input bar at the bottom. -use std::{borrow::Cow, collections::BTreeMap, ops::{DerefMut, Range}, sync::{Arc, Mutex}, time::SystemTime}; +use std::{borrow::Cow, collections::BTreeMap, fs, ops::{DerefMut, Range}, sync::{Arc, Mutex}, time::SystemTime}; use bytesize::ByteSize; use imbl::Vector; @@ -19,21 +19,22 @@ use matrix_sdk_ui::timeline::{ self, EventTimelineItem, InReplyToDetails, MemberProfileChange, RepliedToInfo, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem }; use robius_location::Coordinates; +use ruma::{api::client::{filter::RoomEventFilter, search::search_events::v3::{Categories, Criteria, EventContext, OrderBy, ResultCategories}}, events::{AnyTimelineEvent, EventContent}}; use crate::{ avatar_cache, event_preview::{body_of_timeline_item, text_preview_of_member_profile_change, text_preview_of_other_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, location::{get_latest_location, init_location_subscriber, request_location_update, LocationAction, LocationRequest, LocationUpdate}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, shared::{ - avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::enqueue_popup_notification, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, typing_animation::TypingAnimationWidgetExt + avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::enqueue_popup_notification, search_bar::SearchBarAction, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, typing_animation::TypingAnimationWidgetExt }, sliding_sync::{get_client, submit_async_request, take_timeline_endpoints, BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineRequestSender, UserPowerLevels}, utils::{self, unix_time_millis_to_datetime, ImageFormat, MEDIA_THUMBNAIL_FORMAT} }; use crate::home::event_reaction_list::ReactionListWidgetRefExt; use crate::home::room_read_receipt::AvatarRowWidgetRefExt; use rangemap::RangeSet; -use super::{editing_pane::{EditingPaneAction, EditingPaneWidgetExt}, event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}}; - +use super::{editing_pane::{EditingPaneAction, EditingPaneWidgetExt}, event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}, room_search_result::{self, SearchTimelineItem}}; +use super::room_search_result::SearchResultWidgetExt; const GEO_URI_SCHEME: &str = "geo:"; const MESSAGE_NOTICE_TEXT_COLOR: Vec3 = Vec3 { x: 0.5, y: 0.5, z: 0.5 }; @@ -60,6 +61,7 @@ live_design! { use crate::home::loading_pane::*; use crate::home::event_reaction_list::*; use crate::home::editing_pane::*; + use crate::home::room_search_result::*; IMG_DEFAULT_AVATAR = dep("crate://self/resources/img/default_avatar.png") @@ -514,7 +516,7 @@ live_design! { // The view used for each day divider in a room's timeline. // The date text is centered between two horizontal lines. - DateDivider = { + pub DateDivider = { width: Fill, height: Fit, margin: {top: 7.0, bottom: 7.0} @@ -543,7 +545,7 @@ live_design! { // The view used for the divider indicating where the user's last-viewed message is. // This is implemented as a DateDivider with a different color and a fixed text label. - ReadMarker = { + pub ReadMarker = { left_line = { draw_bg: {color: (COLOR_READ_MARKER)} } @@ -711,11 +713,12 @@ live_design! { draw_bg: { color: (COLOR_SECONDARY) } - + room_screen_search_result = { + } room_screen_wrapper = { width: Fill, height: Fill, flow: Overlay, - show_bg: true + show_bg: true, draw_bg: { color: (COLOR_PRIMARY_DARKER) } @@ -937,15 +940,17 @@ live_design! { /// The main widget that displays a single Matrix room. #[derive(Live, LiveHook, Widget)] pub struct RoomScreen { - #[deref] view: View, + #[deref] pub view: View, #[animator] animator: Animator, /// The room ID of the currently-shown room. - #[rust] room_id: Option, + #[rust] pub room_id: Option, /// The display name of the currently-shown room. #[rust] room_name: String, /// The persistent UI-relevant states for the room that this widget is currently displaying. - #[rust] tl_state: Option, + #[rust] pub tl_state: Option, + /// By typing inside the Search input, search result + #[rust] pub other_display: RoomScreenOtherDisplay, } impl Drop for RoomScreen { fn drop(&mut self) { @@ -1043,7 +1048,6 @@ impl Widget for RoomScreen { self.handle_message_actions(cx, actions, &portal_list, &loading_pane); let message_input = self.text_input(id!(message_input)); - for action in actions { // Handle the highlight animation. let Some(tl) = self.tl_state.as_mut() else { return }; @@ -1084,12 +1088,14 @@ impl Widget for RoomScreen { message_action_bar_popup.close(cx); } */ + if let RoomScreenOtherDisplay::None = self.other_display { + // Set visibility of loading message banner based of pagination logic + self.send_pagination_request_based_on_scroll_pos(cx, actions, &portal_list); + // Handle sending any read receipts for the current logged-in user. + self.send_user_read_receipts_based_on_scroll_pos(cx, actions, &portal_list); + } - // Set visibility of loading message banner based of pagination logic - self.send_pagination_request_based_on_scroll_pos(cx, actions, &portal_list); - // Handle sending any read receipts for the current logged-in user. - self.send_user_read_receipts_based_on_scroll_pos(cx, actions, &portal_list); - + self.handle_search(cx, actions); // Clear the replying-to preview pane if the "cancel reply" button was clicked // or if the `Escape` key was pressed within the message input box. if self.button(id!(cancel_reply_button)).clicked(actions) @@ -1284,6 +1290,9 @@ impl Widget for RoomScreen { // If return DrawStep::done() inside self.view.draw_walk, turtle will misalign and panic. return DrawStep::done(); } + if let RoomScreenOtherDisplay::SearchResult = self.other_display { + return room_search_result::search_result_draw_walk(self, cx, scope, walk); + } while let Some(subview) = self.view.draw_walk(cx, scope, walk).step() { // We only care about drawing the portal list. let portal_list_ref = subview.as_portal_list(); @@ -1684,6 +1693,36 @@ impl RoomScreen { TimelineUpdate::OwnUserReadReceipt(receipt) => { tl.latest_own_user_receipt = Some(receipt); } + + TimelineUpdate::SearchResult(result) => { + tl.content_drawn_since_last_update.clear(); + tl.profile_drawn_since_last_update.clear(); + tl.fully_paginated = false; + // Set the portal list to the very bottom of the timeline. + let mut timeline_events= Vector::new(); + for item in result.room_events.results.iter() { + + let Some(event) = item.result.clone().and_then(|f|f.deserialize().ok()) else { continue }; + timeline_events.push_back(SearchTimelineItem::with_virtual(VirtualTimelineItem::DateDivider(event.origin_server_ts()))); + item.context.events_before.iter().for_each(|f| { + if let Ok(timeline_event) = f.deserialize() { + timeline_events.push_back(SearchTimelineItem::with_context_event(timeline_event)); + } + }); + timeline_events.push_back(SearchTimelineItem::with_event(event.clone())); + item.context.events_after.iter().for_each(|f| { + if let Ok(timeline_event) = f.deserialize() { + timeline_events.push_back(SearchTimelineItem::with_context_event(timeline_event)); + } + }); + } + portal_list.set_first_id_and_scroll(timeline_events.len().saturating_sub(1), 0.0); + portal_list.set_tail_range(true); + jump_to_bottom.update_visibility(cx, true); + tl.searched_results = timeline_events; + tl.searched_results_highlighted_strings = result.room_events.highlights; + done_loading = true; + } } } @@ -2280,6 +2319,8 @@ impl RoomScreen { prev_first_index: None, scrolled_past_read_marker: false, latest_own_user_receipt: None, + searched_results: Vector::new(), + searched_results_highlighted_strings: Vec::new() }; (new_tl_state, true) }; @@ -2536,6 +2577,47 @@ impl RoomScreen { } tl.last_scrolled_index = first_index; } + /// Handles any search-related actions received by this RoomScreen. + /// + /// See `SearchBarAction` for the possible actions. + fn handle_search( + &mut self, + cx: &mut Cx, + actions: &ActionsBuf + ) { + for action in actions { + match action.as_widget_action().cast() { + SearchBarAction::Search(keywords) => { + let Some(room_id) = self.room_id.clone() else { return }; + let mut search_categories = Categories::new(); + let mut room_filter = RoomEventFilter::empty(); + room_filter.rooms = Some(vec![self.tl_state.as_ref().unwrap().room_id.clone()]); + let mut criteria = Criteria::new(keywords); + criteria.filter = room_filter; + criteria.order_by = Some(OrderBy::Recent); + criteria.event_context = EventContext::new(); + criteria.event_context.after_limit = uint!(1); + criteria.event_context.before_limit = uint!(1); + criteria.event_context.include_profile = true; + + search_categories.room_events = Some(criteria); + submit_async_request(MatrixRequest::Search { + room_id, + search_categories + }); + self.other_display = RoomScreenOtherDisplay::SearchResult; + } + _ => { + } + } + match action.downcast_ref().cast() { + Some(SearchResultAction::Success(results, criteria )) => { + self.view.search_result(id!(room_screen_search_result)).set_list(cx, results.len(), criteria); + } + _ => {} + } + } + } } impl RoomScreenRef { @@ -2551,6 +2633,17 @@ impl RoomScreenRef { } } +/// Various display types other than timeline in the room screen +#[derive(Clone, Debug, DefaultNone)] +pub enum RoomScreenOtherDisplay{ + /// Display Search Result + SearchResult, + /// Display spinner animation + SearchPending, + /// Display timeline + None +} + /// Actions for the room screen's tooltip. #[derive(Clone, Debug, DefaultNone)] pub enum RoomScreenTooltipActions { @@ -2653,6 +2746,8 @@ pub enum TimelineUpdate { UserPowerLevels(UserPowerLevels), /// An update to the currently logged-in user's own read receipt for this room. OwnUserReadReceipt(Receipt), + /// Display Search result in Makepad's widget + SearchResult(ResultCategories), } /// The global set of all timeline states, one entry per room. @@ -2664,12 +2759,12 @@ static TIMELINE_STATES: Mutex> = Mutex::n /// across multiple `Hide`/`Show` cycles of that room's timeline within a RoomScreen. /// If a state is more temporary and shouldn't be persisted when the timeline is hidden, /// then it should be stored in the RoomScreen widget itself, not in this struct. -struct TimelineUiState { +pub struct TimelineUiState { /// The ID of the room that this timeline is for. - room_id: OwnedRoomId, + pub room_id: OwnedRoomId, /// The power levels of the currently logged-in user in this room. - user_power: UserPowerLevels, + pub user_power: UserPowerLevels, /// Whether this room's timeline has been fully paginated, which means /// that the oldest (first) event in the timeline is locally synced and available. @@ -2681,6 +2776,10 @@ struct TimelineUiState { /// The list of items (events) in this room's timeline that our client currently knows about. items: Vector>, + /// The list of searched events in this room + pub searched_results: Vector, + /// The list of strings that should be highlighted in the search results + pub searched_results_highlighted_strings: Vec, /// The range of items (indices in the above `items` list) whose event **contents** have been drawn /// since the last update and thus do not need to be re-populated on future draw events. /// @@ -2694,10 +2793,10 @@ struct TimelineUiState { /// /// Upon a background update, only item indices greater than or equal to the /// `index_of_first_change` are removed from this set. - content_drawn_since_last_update: RangeSet, + pub content_drawn_since_last_update: RangeSet, /// Same as `content_drawn_since_last_update`, but for the event **profiles** (avatar, username). - profile_drawn_since_last_update: RangeSet, + pub profile_drawn_since_last_update: RangeSet, /// The channel receiver for timeline updates for this room. /// @@ -2713,7 +2812,7 @@ struct TimelineUiState { /// The cache of media items (images, videos, etc.) that appear in this timeline. /// /// Currently this excludes avatars, as those are shared across multiple rooms. - media_cache: MediaCache, + pub media_cache: MediaCache, /// Info about the event currently being replied to, if any. replying_to: Option<(EventTimelineItem, RepliedToInfo)>, @@ -2841,11 +2940,11 @@ fn find_new_item_matching_current_item( } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -struct ItemDrawnStatus { +pub struct ItemDrawnStatus { /// Whether the profile info (avatar and displayable username) were drawn for this item. - profile_drawn: bool, + pub profile_drawn: bool, /// Whether the content of the item was drawn (e.g., the message text, image, video, sticker, etc). - content_drawn: bool, + pub content_drawn: bool, } impl ItemDrawnStatus { /// Returns a new `ItemDrawnStatus` with both `profile_drawn` and `content_drawn` set to `false`. @@ -2856,7 +2955,7 @@ impl ItemDrawnStatus { } } /// Returns a new `ItemDrawnStatus` with both `profile_drawn` and `content_drawn` set to `true`. - const fn both_drawn() -> Self { + pub const fn both_drawn() -> Self { Self { profile_drawn: true, content_drawn: true, @@ -3431,7 +3530,7 @@ fn populate_message_view( } /// Draws the Html or plaintext body of the given Text or Notice message into the `message_content_widget`. -fn populate_text_message_content( +pub fn populate_text_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, body: &str, @@ -3461,7 +3560,7 @@ fn populate_text_message_content( /// Draws the given image message's content into the `message_content_widget`. /// /// Returns whether the image message content was fully drawn. -fn populate_image_message_content( +pub fn populate_image_message_content( cx: &mut Cx2d, text_or_image_ref: &TextOrImageRef, image_info_source: Option<(Option, MediaSource)>, @@ -3578,7 +3677,7 @@ fn populate_image_message_content( /// Draws a file message's content into the given `message_content_widget`. /// /// Returns whether the file message content was fully drawn. -fn populate_file_message_content( +pub fn populate_file_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, file_content: &FileMessageEventContent, @@ -3608,7 +3707,7 @@ fn populate_file_message_content( /// Draws an audio message's content into the given `message_content_widget`. /// /// Returns whether the audio message content was fully drawn. -fn populate_audio_message_content( +pub fn populate_audio_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, audio: &AudioMessageEventContent, @@ -3649,7 +3748,7 @@ fn populate_audio_message_content( /// Draws a video message's content into the given `message_content_widget`. /// /// Returns whether the video message content was fully drawn. -fn populate_video_message_content( +pub fn populate_video_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, video: &VideoMessageEventContent, @@ -3694,7 +3793,7 @@ fn populate_video_message_content( /// Draws the given location message's content into the `message_content_widget`. /// /// Returns whether the location message content was fully drawn. -fn populate_location_message_content( +pub fn populate_location_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, location: &LocationMessageEventContent, @@ -4373,8 +4472,16 @@ impl Message { } impl MessageRef { - fn set_data(&self, details: MessageDetails) { + pub fn set_data(&self, details: MessageDetails) { let Some(mut inner) = self.borrow_mut() else { return }; inner.set_data(details); } } + +/// Actions related to a specific message within a room timeline. +#[derive(Clone, DefaultNone, Debug)] +pub enum SearchResultAction { + /// List of search result and the search criteria + Success(Vector, String), + None +} \ No newline at end of file diff --git a/src/home/room_search_result.rs b/src/home/room_search_result.rs new file mode 100644 index 00000000..a87e0401 --- /dev/null +++ b/src/home/room_search_result.rs @@ -0,0 +1,784 @@ +use std::{borrow::Cow, ops::DerefMut}; + +use imbl::Vector; +use makepad_widgets::*; +use matrix_sdk_ui::timeline::{Profile, TimelineDetails, VirtualTimelineItem}; +use rangemap::RangeSet; +use ruma::{events::{relation::InReplyTo, room::message::{EmoteMessageEventContent, FileMessageEventContent, FormattedBody, MessageFormat, MessageType, NoticeMessageEventContent, Relation, RoomMessageEventContent, TextMessageEventContent}, sticker::StickerEventContent, AnyMessageLikeEventContent, AnySyncTimelineEvent, AnyTimelineEvent}, uint, OwnedRoomId}; + +use crate::{media_cache::MediaCache, shared::{avatar::AvatarWidgetRefExt, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, text_or_image::TextOrImageWidgetRefExt}, sliding_sync::UserPowerLevels, utils::unix_time_millis_to_datetime}; + +use super::{new_message_context_menu::{MessageAbilities, MessageDetails}, room_screen::{populate_audio_message_content, populate_file_message_content, populate_image_message_content, populate_location_message_content, populate_text_message_content, populate_video_message_content, ItemDrawnStatus, MessageOrStickerType, MessageWidgetRefExt, RoomScreen}}; + +const MESSAGE_NOTICE_TEXT_COLOR: Vec3 = Vec3 { x: 0.5, y: 0.5, z: 0.5 }; +const COLOR_DANGER_RED: Vec3 = Vec3 { x: 0.862, y: 0.0, z: 0.02 }; +live_design! { + use link::theme::*; + use link::shaders::*; + use link::widgets::*; + use crate::shared::styles::*; + use crate::shared::helpers::*; + use crate::shared::icon_button::*; + COLOR_BUTTON_GREY = #B6BABF + ICON_SEARCH = dep("crate://self/resources/icons/search.svg") + pub SearchResult = {{SearchResult}} { + width: Fill, + height: 60, + show_bg: true, + draw_bg: { + color: (COLOR_SECONDARY) + } + flow: Down, + { + width: Fill, + height: 60, + { + align: {x: 0.0} // Align to top-right + spacing: 10, + margin: {top: 5}, + padding: {top: 10, bottom: 10, left: 8, right: 15} + width: Fit, + height: Fit, + draw_bg: { + instance color: (COLOR_BUTTON_GREY) + instance color_hover: #fef65b + instance border_width: 1.5 + instance radius: 3.0 + instance hover: 0.0 + fn get_color(self) -> vec4 { + return mix(self.color, mix(self.color, self.color_hover, 0.2), self.hover) + } + fn pixel(self) -> vec4 { + let sdf = Sdf2d::viewport(self.pos * self.rect_size); + sdf.box( + self.border_width, + self.border_width, + self.rect_size.x - self.border_width * 2.0, + self.rect_size.y - self.border_width * 2.0, + max(1.0, self.radius) + ) + sdf.fill(self.get_color()); + return sdf.result; + } + } + draw_icon: { + svg_file: (ICON_SEARCH), + fn get_color(self) -> vec4 { + return (COLOR_TEXT_INPUT_IDLE); + } + } + icon_walk: {width: 16, height: 16} + } +