From 378dd89bc7bbacb5f4cc38c8bfeee7b95ab2abb6 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Thu, 22 Oct 2026 22:14:29 +0200 Subject: [PATCH] Ensure that theme styles are kept across reloads/apply calls --- src/app.rs | 5 +- src/home/invite_modal.rs | 26 ++- src/home/room_screen.rs | 16 +- src/join_leave_room_modal.rs | 296 +++++++++++++++------------- src/logout/logout_confirm_modal.rs | 42 ++-- src/settings/app_preferences.rs | 20 +- src/settings/app_settings.rs | 4 +- src/shared/helpers.rs | 4 + src/shared/html_or_plaintext.rs | 18 +- src/shared/jump_to_bottom_button.rs | 20 +- src/shared/styles.rs | 60 ------ src/tsp/create_did_modal.rs | 68 ++++--- src/tsp/create_wallet_modal.rs | 72 ++++--- src/tsp/tsp_verification_modal.rs | 51 ++--- 14 files changed, 370 insertions(+), 332 deletions(-) diff --git a/src/app.rs b/src/app.rs index b80c852d6..b3021b426 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,7 +17,7 @@ use crate::{ event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, home_screen::{MainViewVariantChangedAction, ScreenRestoredAction}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt}, main_desktop_ui::MainDesktopUiAction, nav_history::{GoForwardAction, NavHistory, can_navigate_history}, nav_history_buttons::enable_nav_history_buttons, nav_history_menu::{ShowNavHistoryMenuAction, show_nav_history_menu}, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_context_menu::RoomContextMenuWidgetRefExt, room_screen::{DropDecodedImagesAction, InviteAction, MessageAction, clear_timeline_states, invalidate_single_timeline_state, drop_docked_pane_data}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update} }, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt - }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence::{self, WindowGeomTracker}, profile::user_profile_cache::{clear_user_profile_cache, process_user_profile_updates}, room::{BasicRoomDetails, room_pane::{self, PaneLayout, RoomPaneKind}}, settings::{app_preferences::{AppPreferences, Appearance, AppearanceGlobal, UiZoom}, encryption_settings::{EncryptionModalAction, EncryptionModalWidgetRefExt}}, shared::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, context_menu::{ContextMenuClosed, close_context_menus, is_context_menu_shown, menu_position_margin}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, speech_text_input::cancel_all_dictation}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RecoveryAction, TimelineKind, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ + }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence::{self, WindowGeomTracker}, profile::user_profile_cache::{clear_user_profile_cache, process_user_profile_updates}, room::{BasicRoomDetails, room_pane::{self, PaneLayout, RoomPaneKind}}, settings::{app_preferences::{AppPreferences, AppPreferencesGlobal, Appearance, AppearanceGlobal, UiZoom, set_img_msg_fit_max_height}, encryption_settings::{EncryptionModalAction, EncryptionModalWidgetRefExt}}, shared::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, context_menu::{ContextMenuClosed, close_context_menus, is_context_menu_shown, menu_position_margin}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, speech_text_input::cancel_all_dictation}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RecoveryAction, TimelineKind, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -838,6 +838,9 @@ impl AppMain for App { crate::verification_modal::script_mod(vm); crate::profile::script_mod(vm); crate::home::script_mod(vm); + // Re-running this script recreates `IMG_MSG_FIT` at its default size. + let thumbnail_max_height = vm.cx_mut().global::().0.thumbnail_max_height; + set_img_msg_fit_max_height(vm, thumbnail_max_height); crate::login::script_mod(vm); crate::logout::script_mod(vm); diff --git a/src/home/invite_modal.rs b/src/home/invite_modal.rs index 0ec24bc22..4142914fc 100644 --- a/src/home/invite_modal.rs +++ b/src/home/invite_modal.rs @@ -107,7 +107,7 @@ enum InviteModalState { } -#[derive(Script, ScriptHook, Widget)] +#[derive(Script, Widget)] pub struct InviteModal { #[deref] view: View, @@ -119,6 +119,12 @@ pub struct InviteModal { #[rust] room_name_id: Option, } +impl ScriptHook for InviteModal { + fn on_after_reload(&mut self, vm: &mut ScriptVm) { + vm.with_cx_mut(|cx| self.update_status_color(cx)); + } +} + impl Widget for InviteModal { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); @@ -168,7 +174,7 @@ impl WidgetMatchEvent for InviteModal { // Validate the user ID if user_id_str.is_empty() { status_label.set_text(cx, "Please enter a user ID."); - status_label.set_text_color(cx, self.status_color_error); + self.update_status_color(cx); status_view.set_visible(cx, true); self.view.redraw(cx); return; @@ -184,7 +190,7 @@ impl WidgetMatchEvent for InviteModal { }); self.state = InviteModalState::WaitingForInvite(user_id); status_label.set_text(cx, "Sending invite..."); - status_label.set_text_color(cx, self.status_color_pending); + self.update_status_color(cx); status_view.set_visible(cx, true); confirm_button.set_enabled(cx, false); user_id_input.set_is_read_only(cx, true); @@ -192,7 +198,7 @@ impl WidgetMatchEvent for InviteModal { } Err(_) => { status_label.set_text(cx, "Invalid User ID. Expected format: @user:server.xyz"); - status_label.set_text_color(cx, self.status_color_error); + self.update_status_color(cx); status_view.set_visible(cx, true); user_id_input.set_key_focus(cx); } @@ -210,7 +216,6 @@ impl WidgetMatchEvent for InviteModal { => { let status = format!("Successfully invited {user_id}!"); status_label.set_text(cx, &status); - status_label.set_text_color(cx, self.status_color_success); status_view.set_visible(cx, true); confirm_button.set_visible(cx, false); cancel_button.set_visible(cx, false); @@ -223,7 +228,6 @@ impl WidgetMatchEvent for InviteModal { => { let status = format!("Failed to send invite: {error}"); status_label.set_text(cx, &status); - status_label.set_text_color(cx, self.status_color_error); status_view.set_visible(cx, true); confirm_button.set_enabled(cx, true); user_id_input.set_is_read_only(cx, false); @@ -234,6 +238,7 @@ impl WidgetMatchEvent for InviteModal { }; if let Some(new_state) = new_state { self.state = new_state; + self.update_status_color(cx); self.view.redraw(cx); break; } @@ -243,6 +248,15 @@ impl WidgetMatchEvent for InviteModal { } impl InviteModal { + fn update_status_color(&self, cx: &mut Cx) { + let color = match self.state { + InviteModalState::WaitingForInvite(_) => self.status_color_pending, + InviteModalState::InviteSuccess => self.status_color_success, + InviteModalState::WaitingForUserInput | InviteModalState::InviteError => self.status_color_error, + }; + self.view.label(cx, ids!(status_label_view.status_label)).set_text_color(cx, color); + } + pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId) { self.view.label(cx, ids!(title)).set_text( cx, diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 0450036d1..c8f8f6177 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -927,10 +927,7 @@ impl ScriptHook for RoomScreen { // A script reload changes the RoomScreen's children; invalidate the ones we cached. self.cached_refs = None; vm.with_cx_mut(|cx| { - if let Some(tl_state) = &mut self.tl_state.as_mut() { - // Clear the timeline's drawn items caches and redraw it. - tl_state.content_drawn_since_last_update.clear(); - tl_state.profile_drawn_since_last_update.clear(); + if let Some(tl_state) = self.tl_state.as_ref() { // The reapply also resets the RoomInputBar, so we need to re-update its state. let room_input_bar = self.view.room_input_bar(cx, ids!(room_input_bar)); room_input_bar.update_room_state( @@ -1539,6 +1536,13 @@ impl Widget for RoomScreen { let Some(tl_state) = self.tl_state.as_mut() else { return DrawStep::done(); }; + // Re-populate all items after a reload, since it resets their widgets, + // even if it happened while this timeline state was parked. + if tl_state.drawn_reload_generation != cx.reload_generation { + tl_state.drawn_reload_generation = cx.reload_generation; + tl_state.content_drawn_since_last_update.clear(); + tl_state.profile_drawn_since_last_update.clear(); + } // Set the portal list's range based on the number of timeline items. let tl_items = &tl_state.items; @@ -3185,6 +3189,7 @@ impl RoomScreen { index_of_first_own_failed: None, content_drawn_since_last_update: RangeSet::new(), profile_drawn_since_last_update: RangeSet::new(), + drawn_reload_generation: cx.reload_generation, update_receiver, request_sender, media_cache: MediaCache::new(Some(update_sender.clone())), @@ -4331,6 +4336,9 @@ struct TimelineUiState { /// Same as `content_drawn_since_last_update`, but for the event **profiles** (avatar, username). profile_drawn_since_last_update: RangeSet, + /// The `cx.reload_generation` that the above drawn-item caches were filled in. + drawn_reload_generation: u64, + /// The channel receiver for timeline updates for this room. /// /// Here we use a synchronous (non-async) channel because the receiver runs diff --git a/src/join_leave_room_modal.rs b/src/join_leave_room_modal.rs index 0dc516314..8eb347529 100644 --- a/src/join_leave_room_modal.rs +++ b/src/join_leave_room_modal.rs @@ -8,7 +8,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::{OwnedRoomId, OwnedServerName}; use tokio::sync::mpsc::UnboundedSender; -use crate::{home::{invite_screen::{InviteDetails, JoinRoomResultAction, LeaveRoomResultAction}, rooms_list::{InviteState, set_invite_state}}, room::BasicRoomDetails, shared::{popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_negative_button_style, apply_neutral_button_style, apply_positive_button_style, apply_primary_button_style}}, sliding_sync::{MatrixRequest, submit_async_request}, space_service_sync::{SpaceRequest, SpaceRoomListAction}, utils::{self, RoomNameId}}; +use crate::{home::{invite_screen::{InviteDetails, JoinRoomResultAction, LeaveRoomResultAction}, rooms_list::{InviteState, set_invite_state}}, room::BasicRoomDetails, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::{MatrixRequest, submit_async_request}, space_service_sync::{SpaceRequest, SpaceRoomListAction}, utils::{self, RoomNameId}}; script_mod! { use mod.prelude.widgets.* @@ -22,6 +22,8 @@ script_mod! { body := ModalBody {} + // Only one cancel button and one accept button are shown at a time, + // depending on whether the action is positive (joining) or negative (leaving/rejecting). buttons_view := ModalButtonsRow { cancel_button := RobrixNegativeIconButton { width: Fit{min: FitBound.Abs(120.0)}, @@ -32,6 +34,16 @@ script_mod! { text: "Cancel" } + cancel_neutral_button := RobrixNeutralIconButton { + visible: false + width: Fit{min: FitBound.Abs(120.0)}, + align: Align{x: 0.5, y: 0.5} + padding: 15, + draw_icon.svg: (ICON_FORBIDDEN) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "Cancel" + } + accept_button := RobrixPositiveIconButton { width: Fit{min: FitBound.Abs(120.0)}, align: Align{x: 0.5, y: 0.5} @@ -40,6 +52,26 @@ script_mod! { icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } text: "Yes" } + + accept_negative_button := RobrixNegativeIconButton { + visible: false + width: Fit{min: FitBound.Abs(120.0)}, + align: Align{x: 0.5, y: 0.5} + padding: 15, + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "Yes" + } + + okay_button := RobrixIconButton { + visible: false + width: Fit{min: FitBound.Abs(120.0)}, + align: Align{x: 0.5, y: 0.5} + padding: 15, + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "Okay" + } } tip_view := View { @@ -70,12 +102,6 @@ script_mod! { pub struct JoinLeaveRoomModal { #[deref] view: View, #[rust] kind: Option, - /// Whether the modal is in a final state, meaning the user can only click "Okay" to close it. - /// - /// * Set to `Some(true)` after a successful action (e.g., joining or leaving a room). - /// * Set to `Some(false)` after a join/leave error occurs. - /// * Set to `None` when the user is still able to interact with the modal. - #[rust] final_success: Option, } /// Kinds of content that can be shown and handled by the [`JoinLeaveRoomModal`]. @@ -171,10 +197,11 @@ impl Widget for JoinLeaveRoomModal { impl WidgetMatchEvent for JoinLeaveRoomModal { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { - let mut accept_button = self.view.button(cx, ids!(accept_button)); - let cancel_button = self.view.button(cx, ids!(cancel_button)); + let accept_buttons = self.view.button_set(cx, ids_array!(accept_button, accept_negative_button)); + let cancel_buttons = self.view.button_set(cx, ids_array!(cancel_button, cancel_neutral_button)); + let okay_button = self.view.button(cx, ids!(okay_button)); - let cancel_clicked = cancel_button.clicked(actions); + let cancel_clicked = cancel_buttons.clicked(actions); if cancel_clicked || actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) { @@ -190,108 +217,112 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { } let Some(kind) = self.kind.as_ref() else { return }; + + if okay_button.clicked(actions) { + cx.action(JoinLeaveRoomModalAction::Close { + room_id: kind.room_id().clone(), + was_internal: true, + }); + self.reset_state(); + return; + } + let mut needs_redraw = false; - if accept_button.clicked(actions) { - if self.final_success.is_some() { - cx.action(JoinLeaveRoomModalAction::Close { - room_id: kind.room_id().clone(), - was_internal: true, - }); - self.reset_state(); - return; - } - else { - let title: Cow; - let description: String; - let accept_button_text: &str; - match kind { - JoinLeaveModalKind::AcceptInvite(invite) => { - title = "Accepting this invite...".into(); - description = format!( - "Accepting an invitation to join \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - invite.room_name_id(), - ); - accept_button_text = "Joining..."; - submit_async_request(MatrixRequest::JoinRoom { - room_id: invite.room_id().clone(), - alias: None, - via: Vec::new(), - }); - set_invite_state(cx, invite.room_id(), InviteState::WaitingForJoinResult); - } - JoinLeaveModalKind::RejectInvite(invite) => { - title = "Rejecting this invite...".into(); - description = format!( - "Rejecting an invitation to join \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - invite.room_name_id(), - ); - accept_button_text = "Rejecting..."; - submit_async_request(MatrixRequest::LeaveRoom { - room_id: invite.room_id().clone(), - }); - set_invite_state(cx, invite.room_id(), InviteState::WaitingForLeaveResult); - } - JoinLeaveModalKind::JoinRoom { details, is_space, via } => { - title = format!("Joining this {}...", if *is_space { "space" } else { "room" }).into(); - description = format!( - "Joining \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - details.room_name_id(), - ); - accept_button_text = "Joining..."; - submit_async_request(MatrixRequest::JoinRoom { - room_id: details.room_id().clone(), - alias: details.alias().cloned(), - via: via.clone(), - }); - } - JoinLeaveModalKind::LeaveRoom(room) => { - title = "Leaving this room...".into(); - description = format!( - "Leaving \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - room.room_name_id(), - ); - accept_button_text = "Leaving..."; - submit_async_request(MatrixRequest::LeaveRoom { - room_id: room.room_id().clone(), - }); - } - JoinLeaveModalKind::LeaveSpace { details, space_request_sender } => { - title = "Leaving this space...".into(); - description = format!( - "Leaving \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - details.room_name_id(), + if accept_buttons.clicked(actions) { + let title: Cow; + let description: String; + let accept_button_text: &str; + match kind { + JoinLeaveModalKind::AcceptInvite(invite) => { + title = "Accepting this invite...".into(); + description = format!( + "Accepting an invitation to join \"{}\".\n\n\ + Waiting for confirmation from the homeserver...", + invite.room_name_id(), + ); + accept_button_text = "Joining..."; + submit_async_request(MatrixRequest::JoinRoom { + room_id: invite.room_id().clone(), + alias: None, + via: Vec::new(), + }); + set_invite_state(cx, invite.room_id(), InviteState::WaitingForJoinResult); + } + JoinLeaveModalKind::RejectInvite(invite) => { + title = "Rejecting this invite...".into(); + description = format!( + "Rejecting an invitation to join \"{}\".\n\n\ + Waiting for confirmation from the homeserver...", + invite.room_name_id(), + ); + accept_button_text = "Rejecting..."; + submit_async_request(MatrixRequest::LeaveRoom { + room_id: invite.room_id().clone(), + }); + set_invite_state(cx, invite.room_id(), InviteState::WaitingForLeaveResult); + } + JoinLeaveModalKind::JoinRoom { details, is_space, via } => { + title = format!("Joining this {}...", if *is_space { "space" } else { "room" }).into(); + description = format!( + "Joining \"{}\".\n\n\ + Waiting for confirmation from the homeserver...", + details.room_name_id(), + ); + accept_button_text = "Joining..."; + submit_async_request(MatrixRequest::JoinRoom { + room_id: details.room_id().clone(), + alias: details.alias().cloned(), + via: via.clone(), + }); + } + JoinLeaveModalKind::LeaveRoom(room) => { + title = "Leaving this room...".into(); + description = format!( + "Leaving \"{}\".\n\n\ + Waiting for confirmation from the homeserver...", + room.room_name_id(), + ); + accept_button_text = "Leaving..."; + submit_async_request(MatrixRequest::LeaveRoom { + room_id: room.room_id().clone(), + }); + } + JoinLeaveModalKind::LeaveSpace { details, space_request_sender } => { + title = "Leaving this space...".into(); + description = format!( + "Leaving \"{}\".\n\n\ + Waiting for confirmation from the homeserver...", + details.room_name_id(), + ); + accept_button_text = "Leaving..."; + if space_request_sender.send( + SpaceRequest::LeaveSpace { space_name_id: details.room_name_id().clone() } + ).is_err() { + enqueue_popup_notification( + "Failed to send leave space request.\n\nPlease restart Robrix.", + PopupKind::Error, + None, ); - accept_button_text = "Leaving..."; - if space_request_sender.send( - SpaceRequest::LeaveSpace { space_name_id: details.room_name_id().clone() } - ).is_err() { - enqueue_popup_notification( - "Failed to send leave space request.\n\nPlease restart Robrix.", - PopupKind::Error, - None, - ); - } } } + } - self.view.label(cx, ids!(title)).set_text(cx, &title); - self.view.label(cx, ids!(body)).set_text(cx, &description); - self.view.view(cx, ids!(tip_view)).set_visible(cx, false); + self.view.label(cx, ids!(title)).set_text(cx, &title); + self.view.label(cx, ids!(body)).set_text(cx, &description); + self.view.view(cx, ids!(tip_view)).set_visible(cx, false); + for accept_button in accept_buttons.iter() { accept_button.set_text(cx, accept_button_text); - accept_button.set_enabled(cx, false); - // The request is already on its way, so "Cancel" would be a lie. + } + accept_buttons.set_enabled(cx, false); + // The request is already on its way, so "Cancel" would be a lie. + for cancel_button in cancel_buttons.iter() { cancel_button.set_text(cx, "Close"); - needs_redraw = true; } + needs_redraw = true; } - let mut new_final_success = None; + let mut is_final_state = false; for action in actions { match action.downcast_ref() { Some(JoinRoomResultAction::Joined { room_id }) if room_id == kind.room_id() => { @@ -300,14 +331,14 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { "Successfully joined \"{}\".", kind.room_name(), )); - new_final_success = Some(true); + is_final_state = true; } Some(JoinRoomResultAction::Failed { room_id, error }) if room_id == kind.room_id() => { self.view.label(cx, ids!(title)).set_text(cx, "Error joining room!"); let was_invite = matches!(kind, JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_)); let msg = utils::stringify_join_leave_error(error, kind.room_name(), true, was_invite); self.view.label(cx, ids!(body)).set_text(cx, &msg); - new_final_success = Some(false); + is_final_state = true; } _ => {} } @@ -331,7 +362,7 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { } self.view.label(cx, ids!(title)).set_text(cx, title); self.view.label(cx, ids!(body)).set_text(cx, &description); - new_final_success = Some(true); + is_final_state = true; } Some(LeaveRoomResultAction::Failed { room_id, error }) if room_id == kind.room_id() => { let title: &str; @@ -346,7 +377,7 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { self.view.label(cx, ids!(title)).set_text(cx, title); self.view.label(cx, ids!(body)).set_text(cx, &description); - new_final_success = Some(false); + is_final_state = true; } _ => {} } @@ -359,28 +390,25 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { Ok(()) => { title = "Left space!"; description = format!("Successfully left \"{space_name_id}\"."); - new_final_success = Some(true); } Err(e) => { title = "Error leaving space!"; description = format!("Failed to leave space \"{space_name_id}\".\n\nError: {e}"); - new_final_success = Some(false); } } self.view.label(cx, ids!(title)).set_text(cx, title); self.view.label(cx, ids!(body)).set_text(cx, &description); + is_final_state = true; } } } - if let Some(success) = new_final_success { - self.final_success = Some(success); + if is_final_state { needs_redraw = true; - accept_button.set_enabled(cx, true); - accept_button.set_text(cx, "Okay"); - apply_primary_button_style(cx, &mut accept_button); - accept_button.reset_hover(cx); - cancel_button.set_visible(cx, false); + accept_buttons.set_visible(cx, false); + cancel_buttons.set_visible(cx, false); + okay_button.set_visible(cx, true); + okay_button.reset_hover(cx); } if needs_redraw { self.redraw(cx); @@ -391,7 +419,6 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { impl JoinLeaveRoomModal { fn reset_state(&mut self) { self.kind = None; - self.final_success = None; } /// Populates this modal with the proper info based on @@ -472,36 +499,31 @@ impl JoinLeaveRoomModal { self.view.view(cx, ids!(tip_view)).set_visible(cx, false); } - let mut accept_button = self.button(cx, ids!(accept_button)); - let mut cancel_button = self.button(cx, ids!(cancel_button)); - accept_button.set_text(cx, "Yes"); - let is_negative = matches!(kind, JoinLeaveModalKind::RejectInvite(_) | JoinLeaveModalKind::LeaveRoom(_) | JoinLeaveModalKind::LeaveSpace { .. } ); - - if is_negative { - // Negative action: accept button is red, cancel button is gray/neutral. - apply_negative_button_style(cx, &mut accept_button); - apply_neutral_button_style(cx, &mut cancel_button); - } else { - // Positive action: accept button is green, cancel button is red/negative. - apply_positive_button_style(cx, &mut accept_button); - apply_negative_button_style(cx, &mut cancel_button); + // Negative action: accept button is red, cancel button is gray/neutral. + self.view.button_set(cx, ids_array!(accept_negative_button, cancel_neutral_button)).set_visible(cx, is_negative); + // Positive action: accept button is green, cancel button is red/negative. + self.view.button_set(cx, ids_array!(accept_button, cancel_button)).set_visible(cx, !is_negative); + self.view.button(cx, ids!(okay_button)).set_visible(cx, false); + + let accept_buttons = self.view.button_set(cx, ids_array!(accept_button, accept_negative_button)); + let cancel_buttons = self.view.button_set(cx, ids_array!(cancel_button, cancel_neutral_button)); + for accept_button in accept_buttons.iter() { + accept_button.set_text(cx, "Yes"); } - - accept_button.set_enabled(cx, true); - accept_button.set_visible(cx, true); - accept_button.reset_hover(cx); - cancel_button.set_text(cx, "Cancel"); - cancel_button.set_enabled(cx, true); - cancel_button.set_visible(cx, true); - cancel_button.reset_hover(cx); + for cancel_button in cancel_buttons.iter() { + cancel_button.set_text(cx, "Cancel"); + } + accept_buttons.set_enabled(cx, true); + cancel_buttons.set_enabled(cx, true); + accept_buttons.reset_hover(cx); + cancel_buttons.reset_hover(cx); self.kind = Some(kind); - self.final_success = None; } } diff --git a/src/logout/logout_confirm_modal.rs b/src/logout/logout_confirm_modal.rs index f8c40d28a..7f0deb5af 100644 --- a/src/logout/logout_confirm_modal.rs +++ b/src/logout/logout_confirm_modal.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use makepad_widgets::*; use tokio::sync::Notify; -use crate::{home::home_screen::effective_is_desktop, shared::styles::{apply_danger_button_style, apply_negative_button_style}, sliding_sync::{submit_async_request, MatrixRequest}}; +use crate::{home::home_screen::effective_is_desktop, sliding_sync::{submit_async_request, MatrixRequest}}; use super::logout_state_machine::is_logout_past_point_of_no_return; script_mod! { @@ -40,6 +40,22 @@ script_mod! { icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } text: "Log out now" } + + // Replaces the confirm button when a failed logout requires restarting Robrix. + restart_button := RobrixIconButton { + visible: false + width: Fit{min: FitBound.Abs(120.0)} + align: Align{x: 0.5, y: 0.5} + padding: 12, + draw_bg +: { + color: (COLOR_BG_DANGER_RED_SOLID) + color_hover: (COLOR_BG_DANGER_RED_SOLID_HOVER) + color_down: (COLOR_BG_DANGER_RED_SOLID_DOWN) + } + draw_icon.svg: (ICON_LOGOUT) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "Restart now" + } } } } @@ -142,7 +158,8 @@ impl Widget for LogoutConfirmModal { impl WidgetMatchEvent for LogoutConfirmModal { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { let cancel_button = self.button(cx, ids!(cancel_button)); - let mut confirm_button = self.button(cx, ids!(confirm_button)); + let confirm_button = self.button(cx, ids!(confirm_button)); + let restart_button = self.button(cx, ids!(restart_button)); let modal_dismissed = actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))); let cancel_clicked = cancel_button.clicked(actions); @@ -154,7 +171,7 @@ impl WidgetMatchEvent for LogoutConfirmModal { } let mut needs_redraw = false; - if confirm_button.clicked(actions) { + if confirm_button.clicked(actions) || restart_button.clicked(actions) { if let Some(successful) = self.final_success { if is_logout_past_point_of_no_return() && !successful { log!("User requested immediate restart after unrecoverable logout error"); @@ -195,9 +212,8 @@ impl WidgetMatchEvent for LogoutConfirmModal { self.label(cx, ids!(title)).set_text(cx, "Logout error, please restart Robrix."); self.set_message(cx, "The logout process encountered an error when communicating with the homeserver. Since your login session has been partially invalidated, Robrix must restart in order to continue to properly function."); - confirm_button.set_text(cx, "Restart now"); - apply_danger_button_style(cx, &mut confirm_button); - confirm_button.set_enabled(cx, true); + confirm_button.set_visible(cx, false); + restart_button.set_visible(cx, true); cancel_button.set_visible(cx, false); @@ -216,9 +232,8 @@ impl WidgetMatchEvent for LogoutConfirmModal { self.label(cx, ids!(title)).set_text(cx, "Logout error, please restart Robrix."); self.set_message(cx, "Application is in an inconsistent state and needs to be restarted to continue."); - confirm_button.set_text(cx, "Restart now"); - apply_danger_button_style(cx, &mut confirm_button); - confirm_button.set_enabled(cx, true); + confirm_button.set_visible(cx, false); + restart_button.set_visible(cx, true); cancel_button.set_visible(cx, false); self.final_success = Some(false); @@ -254,20 +269,21 @@ impl LogoutConfirmModal { fn reset_state(&mut self, cx: &mut Cx) { let cancel_button = self.button(cx, ids!(cancel_button)); - let mut confirm_button = self.button(cx, ids!(confirm_button)); - if self.final_success == Some(false) { - apply_negative_button_style(cx, &mut confirm_button); - } + let confirm_button = self.button(cx, ids!(confirm_button)); + let restart_button = self.button(cx, ids!(restart_button)); self.final_success = None; self.label(cx, ids!(title)).set_text(cx, "Confirm Logout"); self.set_message(cx, "Are you sure you want to logout?"); + confirm_button.set_visible(cx, true); confirm_button.set_enabled(cx, true); confirm_button.set_text(cx, "Log out now"); + restart_button.set_visible(cx, false); cancel_button.set_visible(cx, true); cancel_button.set_enabled(cx, true); cancel_button.set_text(cx, "Cancel"); cancel_button.reset_hover(cx); confirm_button.reset_hover(cx); + restart_button.reset_hover(cx); self.redraw(cx); } diff --git a/src/settings/app_preferences.rs b/src/settings/app_preferences.rs index ebd8f0d7c..9e38ba0d4 100644 --- a/src/settings/app_preferences.rs +++ b/src/settings/app_preferences.rs @@ -96,13 +96,7 @@ impl AppPreferences { /// object and updates the widget's `walk.height`. pub fn on_thumbnail_max_height_changed(&self, cx: &mut Cx) { cx.global::().0.thumbnail_max_height = self.thumbnail_max_height; - let px = self.thumbnail_max_height.to_pixels() as f64; - // The `use mod.prelude.widgets.*` is required so `FitBound` - // resolves in runtime script scope. - script_eval!(cx, { - use mod.prelude.widgets.* - mod.widgets.IMG_MSG_FIT.max = FitBound.Abs(#(px)) - }); + cx.with_vm(|vm| set_img_msg_fit_max_height(vm, self.thumbnail_max_height)); // Now that we've updated the `IMG_MSG_FIT.max` object in place, // we need to instruct every widget that uses this object to re-read @@ -275,6 +269,18 @@ impl ThumbnailMaxHeight { } } +/// Sets the max height of the shared `mod.widgets.IMG_MSG_FIT` object, +/// which sizes the thumbnail of every image message. +pub fn set_img_msg_fit_max_height(vm: &mut ScriptVm, max_height: ThumbnailMaxHeight) { + let px = max_height.to_pixels() as f64; + // The `use mod.prelude.widgets.*` is required so `FitBound` + // resolves in runtime script scope. + script_eval!(vm, { + use mod.prelude.widgets.* + mod.widgets.IMG_MSG_FIT.max = FitBound.Abs(#(px)) + }); +} + /// Whether the read receipts we send are visible to other users. #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum ReadReceiptsPrivacy { diff --git a/src/settings/app_settings.rs b/src/settings/app_settings.rs index bbdc1ea6a..c96b38990 100644 --- a/src/settings/app_settings.rs +++ b/src/settings/app_settings.rs @@ -929,9 +929,7 @@ impl AppSettings { } /// Sets `is_read_only` based on whether the custom radio is selected. - /// It's a plain `#[live] bool` the apply walk resets to the DSL default, - /// so we re-set it ourselves. Safe anywhere, since it's just a field - /// write plus a redraw. + /// Safe anywhere, since it's just a field write plus a redraw. fn set_thumb_custom_input_read_only(cx: &mut Cx, view: &View, enabled: bool) { view.text_input(cx, ids!(thumb_custom_input)) .set_is_read_only(cx, !enabled); diff --git a/src/shared/helpers.rs b/src/shared/helpers.rs index b9d86b946..9ad8f7280 100644 --- a/src/shared/helpers.rs +++ b/src/shared/helpers.rs @@ -122,6 +122,10 @@ script_mod! { scroll_bar_y.drag_scrolling: true } draw_bg +: { color: (mod.widgets.COLOR_TRANSPARENT) } + draw_selection +: { + color: (mod.widgets.COLOR_SELECT_TEXT) + color_focus: (mod.widgets.COLOR_SELECT_TEXT) + } // Light mode syntax highlighting (inspired by GitHub Light / VS Code Light+) token_colors +: { diff --git a/src/shared/html_or_plaintext.rs b/src/shared/html_or_plaintext.rs index 8206ef080..d6383a7ab 100644 --- a/src/shared/html_or_plaintext.rs +++ b/src/shared/html_or_plaintext.rs @@ -393,7 +393,7 @@ impl RobrixHtmlLink { /// A pill-shaped widget that shows a Matrix link as an avatar and a title. /// /// This can be a link to a user, a room, or an event in a room. -#[derive(Script, ScriptHook, Widget)] +#[derive(Script, Widget)] struct MatrixLinkPill { #[deref] view: View, @@ -418,6 +418,15 @@ struct PillBase { avatar_text_pt: f32, } +impl ScriptHook for MatrixLinkPill { + // A reapply or reload puts the DSL sizes back (a reload may even change them), + // so the next draw has to capture and scale them again. + fn on_after_reload(&mut self, _vm: &mut ScriptVm) { + self.base = None; + self.scaled_to = 0.0; + } +} + impl Widget for MatrixLinkPill { fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) { if matches!(event, Event::Signal) @@ -427,13 +436,6 @@ impl Widget for MatrixLinkPill { self.redraw(cx); } - // A reapply or reload puts the DSL sizes back (a reload may even change them), - // so the next draw has to capture and scale them again. - if matches!(event, Event::ScriptReapply | Event::LiveEdit) { - self.base = None; - self.scaled_to = 0.0; - } - // If this user's profile is now in the cache, redraw so we can pick up that new info. if let Event::Actions(actions) = event && self.is_waiting_for_data diff --git a/src/shared/jump_to_bottom_button.rs b/src/shared/jump_to_bottom_button.rs index ab060899f..8643b3bd4 100644 --- a/src/shared/jump_to_bottom_button.rs +++ b/src/shared/jump_to_bottom_button.rs @@ -104,10 +104,18 @@ script_mod! { } -#[derive(ScriptHook, Script, Widget)] +#[derive(Script, Widget)] pub struct JumpToBottomButton { #[source] source: ScriptObjectRef, #[deref] view: View, + /// The unread badge's border size, which is smaller for counts with more digits. + #[rust] unread_badge_border_size: Option, +} + +impl ScriptHook for JumpToBottomButton { + fn on_after_reload(&mut self, vm: &mut ScriptVm) { + vm.with_cx_mut(|cx| self.apply_unread_badge_border_size(cx)); + } } impl Widget for JumpToBottomButton { @@ -194,12 +202,18 @@ impl JumpToBottomButton { cx, &format!("{}{plus_sign}", std::cmp::min(unread_message_count, 99)) ); - self.view(cx, ids!(unread_message_badge.green_rounded_label)) - .set_uniform(cx, id!(border_size), &[border_size]); + self.unread_badge_border_size = Some(border_size); + self.apply_unread_badge_border_size(cx); } } } + fn apply_unread_badge_border_size(&self, cx: &mut Cx) { + let Some(border_size) = self.unread_badge_border_size else { return }; + self.view(cx, ids!(unread_message_badge.green_rounded_label)) + .set_uniform(cx, id!(border_size), &[border_size]); + } + /// Updates the visibility of the jump to bottom button and the unread message badge /// based on the given actions and the state of the portal list. /// diff --git a/src/shared/styles.rs b/src/shared/styles.rs index c748a89da..4f373922f 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -545,46 +545,6 @@ pub fn apply_positive_button_style(cx: &mut Cx, button: &mut ButtonRef) { }); } -/// Applies negative (red) button styling to the given button. -pub fn apply_negative_button_style(cx: &mut Cx, button: &mut ButtonRef) { - script_apply_eval!(cx, button, { - draw_bg +: { - border_color: mod.widgets.COLOR_FG_DANGER_RED, - color: mod.widgets.COLOR_BG_DANGER_RED, - color_hover: mod.widgets.COLOR_BG_DANGER_RED_HOVER, - color_down: mod.widgets.COLOR_BG_DANGER_RED_DOWN, - } - draw_text +: { - color: mod.widgets.COLOR_FG_DANGER_RED, - color_hover: mod.widgets.COLOR_FG_DANGER_RED, - color_down: mod.widgets.COLOR_FG_DANGER_RED, - } - draw_icon +: { - color: mod.widgets.COLOR_FG_DANGER_RED, - } - }); -} - -/// Applies neutral (gray) button styling to the given button. -pub fn apply_neutral_button_style(cx: &mut Cx, button: &mut ButtonRef) { - script_apply_eval!(cx, button, { - draw_bg +: { - border_color: mod.widgets.COLOR_BG_DISABLED, - color: mod.widgets.COLOR_BG_NEUTRAL, - color_hover: mod.widgets.COLOR_BG_NEUTRAL_HOVER, - color_down: mod.widgets.COLOR_BG_NEUTRAL_DOWN, - } - draw_text +: { - color: mod.widgets.COLOR_TEXT, - color_hover: mod.widgets.COLOR_TEXT, - color_down: mod.widgets.COLOR_TEXT, - } - draw_icon +: { - color: mod.widgets.COLOR_TEXT, - } - }); -} - /// Applies the primary (blue) button styling to the given button. pub fn apply_primary_button_style(cx: &mut Cx, button: &mut ButtonRef) { script_apply_eval!(cx, button, { @@ -607,26 +567,6 @@ pub fn apply_primary_button_style(cx: &mut Cx, button: &mut ButtonRef) { }); } -/// Applies danger (solid red) button styling to the given button. -pub fn apply_danger_button_style(cx: &mut Cx, button: &mut ButtonRef) { - script_apply_eval!(cx, button, { - draw_bg +: { - border_color: mod.widgets.COLOR_BG_DANGER_RED_SOLID, - color: mod.widgets.COLOR_BG_DANGER_RED_SOLID, - color_hover: mod.widgets.COLOR_BG_DANGER_RED_SOLID_HOVER, - color_down: mod.widgets.COLOR_BG_DANGER_RED_SOLID_DOWN, - } - draw_text +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - color_hover: mod.widgets.COLOR_TEXT_ON_ACCENT, - color_down: mod.widgets.COLOR_TEXT_ON_ACCENT, - } - draw_icon +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - } - }); -} - /// Sets the background color of the given view, updating it in place if it's already drawn. pub fn set_view_bg_color(cx: &mut Cx, view: &ViewRef, color: Vec4f) { let Some(mut view) = view.borrow_mut() else { return }; diff --git a/src/tsp/create_did_modal.rs b/src/tsp/create_did_modal.rs index 77cae218f..6c4907a2a 100644 --- a/src/tsp/create_did_modal.rs +++ b/src/tsp/create_did_modal.rs @@ -191,6 +191,16 @@ script_mod! { icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } text: "Create DID" } + + okay_button := RobrixIconButton { + visible: false + width: Fit{min: FitBound.Abs(140.0)} + align: Align{x: 0.5, y: 0.5} + padding: 15, + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "Okay" + } } status_label := ModalBody { @@ -229,7 +239,7 @@ enum CreateDidModalState { } -#[derive(Script, ScriptHook, Widget)] +#[derive(Script, Widget)] pub struct CreateDidModal { #[deref] view: View, @@ -241,6 +251,12 @@ pub struct CreateDidModal { #[rust] is_showing_error: bool, } +impl ScriptHook for CreateDidModal { + fn on_after_reload(&mut self, vm: &mut ScriptVm) { + vm.with_cx_mut(|cx| self.update_status_color(cx)); + } +} + impl Widget for CreateDidModal { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); @@ -254,8 +270,9 @@ impl Widget for CreateDidModal { impl WidgetMatchEvent for CreateDidModal { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { - let mut accept_button = self.view.button(cx, ids!(accept_button)); + let accept_button = self.view.button(cx, ids!(accept_button)); let cancel_button = self.view.button(cx, ids!(cancel_button)); + let okay_button = self.view.button(cx, ids!(okay_button)); // Handle canceling/closing the modal. let cancel_clicked = cancel_button.clicked(actions); @@ -282,7 +299,7 @@ impl WidgetMatchEvent for CreateDidModal { // Handle clicking the accept button. let mut needs_redraw = false; - if accept_button.clicked(actions) { + if accept_button.clicked(actions) || okay_button.clicked(actions) { match self.state { // If the modal is in the "final" state, just close the modal. CreateDidModalState::IdentityCreated => { @@ -298,7 +315,7 @@ impl WidgetMatchEvent for CreateDidModal { if username.is_empty() { self.is_showing_error = true; status_label.set_text(cx, "Please enter a DID username."); - status_label.set_text_color(cx, self.status_color_error); + self.update_status_color(cx); } else { let alias = match alias_input.text().trim() { "" => None, @@ -324,7 +341,7 @@ impl WidgetMatchEvent for CreateDidModal { self.state = CreateDidModalState::WaitingForIdentityCreation; self.is_showing_error = false; status_label.set_text(cx, "Waiting for identity to be created and published..."); - status_label.set_text_color(cx, self.status_color_pending); + self.update_status_color(cx); accept_button.set_enabled(cx, false); cancel_button.set_enabled(cx, false); // TODO: support canceling the identity creation request? username_input.set_is_read_only(cx, true); @@ -352,13 +369,7 @@ impl WidgetMatchEvent for CreateDidModal { self.is_showing_error = false; self.view.label(cx, ids!(status_label)).set_text(cx, ""); self.state = CreateDidModalState::WaitingForUserInput; - script_apply_eval!(cx, accept_button, { - text: "Create DID", - enabled: true, - draw_text +: { - color: mod.widgets.COLOR_FG_ACCEPT_GREEN, - }, - }); + accept_button.set_enabled(cx, true); needs_redraw = true; } } @@ -370,21 +381,10 @@ impl WidgetMatchEvent for CreateDidModal { self.is_showing_error = false; let message = format!("Successfully created and published DID: \"{}\"", did); status_label.set_text(cx, &message); - status_label.set_text_color(cx, self.status_color_success); - script_apply_eval!(cx, accept_button, { - enabled: true, - text: "Okay", - draw_bg +: { - color: mod.widgets.COLOR_ACTIVE_PRIMARY, - }, - draw_icon +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - } - draw_text +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - }, - }); + self.update_status_color(cx); + accept_button.set_visible(cx, false); cancel_button.set_visible(cx, false); + okay_button.set_visible(cx, true); needs_redraw = true; } @@ -395,7 +395,7 @@ impl WidgetMatchEvent for CreateDidModal { self.is_showing_error = true; let message = format!("Failed to create DID: {e}"); status_label.set_text(cx, &message); - status_label.set_text_color(cx, self.status_color_error); + self.update_status_color(cx); accept_button.set_enabled(cx, false); cancel_button.set_enabled(cx, true); username_input.set_is_read_only(cx, false); @@ -416,19 +416,29 @@ impl WidgetMatchEvent for CreateDidModal { } impl CreateDidModal { + fn update_status_color(&self, cx: &mut Cx) { + let color = match self.state { + CreateDidModalState::WaitingForIdentityCreation => self.status_color_pending, + CreateDidModalState::IdentityCreated => self.status_color_success, + CreateDidModalState::WaitingForUserInput | CreateDidModalState::IdentityCreationError => self.status_color_error, + }; + self.view.label(cx, ids!(status_label)).set_text_color(cx, color); + } + pub fn show(&mut self, cx: &mut Cx) { self.state = CreateDidModalState::WaitingForUserInput; let accept_button = self.view.button(cx, ids!(accept_button)); let cancel_button = self.view.button(cx, ids!(cancel_button)); - accept_button.set_text(cx, "Create DID"); + let okay_button = self.view.button(cx, ids!(okay_button)); cancel_button.set_text(cx, "Cancel"); accept_button.reset_hover(cx); cancel_button.reset_hover(cx); + okay_button.reset_hover(cx); accept_button.set_enabled(cx, true); cancel_button.set_enabled(cx, true); accept_button.set_visible(cx, true); cancel_button.set_visible(cx, true); - // TODO: return buttons to their default state/appearance + okay_button.set_visible(cx, false); self.view.text_input(cx, ids!(username_input)).set_is_read_only(cx, false); self.view.text_input(cx, ids!(alias_input)).set_is_read_only(cx, false); self.view.text_input(cx, ids!(server_input)).set_is_read_only(cx, false); diff --git a/src/tsp/create_wallet_modal.rs b/src/tsp/create_wallet_modal.rs index ad4bf47ff..99a8d90ce 100644 --- a/src/tsp/create_wallet_modal.rs +++ b/src/tsp/create_wallet_modal.rs @@ -133,6 +133,16 @@ script_mod! { icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } text: "Create Wallet" } + + okay_button := RobrixIconButton { + visible: false + width: Fit{min: FitBound.Abs(140.0)} + align: Align{x: 0.5, y: 0.5} + padding: 15, + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "Okay" + } } status_label := ModalBody { @@ -169,7 +179,7 @@ enum CreateWalletModalState { } -#[derive(Script, ScriptHook, Widget)] +#[derive(Script, Widget)] pub struct CreateWalletModal { #[deref] view: View, @@ -181,6 +191,12 @@ pub struct CreateWalletModal { #[rust] is_showing_error: bool, } +impl ScriptHook for CreateWalletModal { + fn on_after_reload(&mut self, vm: &mut ScriptVm) { + vm.with_cx_mut(|cx| self.update_status_color(cx)); + } +} + impl Widget for CreateWalletModal { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); @@ -194,8 +210,9 @@ impl Widget for CreateWalletModal { impl WidgetMatchEvent for CreateWalletModal { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { - let mut accept_button = self.view.button(cx, ids!(accept_button)); + let accept_button = self.view.button(cx, ids!(accept_button)); let cancel_button = self.view.button(cx, ids!(cancel_button)); + let okay_button = self.view.button(cx, ids!(okay_button)); // Handle canceling/closing the modal. let cancel_clicked = cancel_button.clicked(actions); @@ -222,7 +239,7 @@ impl WidgetMatchEvent for CreateWalletModal { // Handle clicking the accept button. let mut needs_redraw = false; - if accept_button.clicked(actions) { + if accept_button.clicked(actions) || okay_button.clicked(actions) { match self.state { // If the modal is in the "final" state, just close the modal. CreateWalletModalState::WalletCreated => { @@ -239,15 +256,15 @@ impl WidgetMatchEvent for CreateWalletModal { if password.is_empty() || confirm_password.is_empty() { self.is_showing_error = true; status_label.set_text(cx, "Please enter a wallet password."); - status_label.set_text_color(cx, self.status_color_error); + self.update_status_color(cx); } else if password != confirm_password { self.is_showing_error = true; status_label.set_text(cx, "Passwords do not match."); - status_label.set_text_color(cx, self.status_color_error); + self.update_status_color(cx); } else if wallet_name.is_empty() { self.is_showing_error = true; status_label.set_text(cx, "Please enter a wallet name."); - status_label.set_text_color(cx, self.status_color_error); + self.update_status_color(cx); } else { let url = tsp::TspWalletSqliteUrl::from_wallet_file_name( match wallet_file_name_input.text() { @@ -266,7 +283,7 @@ impl WidgetMatchEvent for CreateWalletModal { self.state = CreateWalletModalState::WaitingForWalletCreation; self.is_showing_error = false; status_label.set_text(cx, "Waiting for wallet to be created..."); - status_label.set_text_color(cx, self.status_color_pending); + self.update_status_color(cx); accept_button.set_enabled(cx, false); cancel_button.set_enabled(cx, false); // TODO: support canceling the wallet creation request? wallet_name_input.set_is_read_only(cx, true); @@ -293,13 +310,7 @@ impl WidgetMatchEvent for CreateWalletModal { self.is_showing_error = false; self.view.label(cx, ids!(status_label)).set_text(cx, ""); self.state = CreateWalletModalState::WaitingForUserInput; - script_apply_eval!(cx, accept_button, { - text: "Create Wallet", - enabled: true, - draw_text +: { - color: mod.widgets.COLOR_FG_ACCEPT_GREEN, - }, - }); + accept_button.set_enabled(cx, true); needs_redraw = true; } } @@ -322,21 +333,10 @@ impl WidgetMatchEvent for CreateWalletModal { format!("Wallet \"{}\" created successfully.", metadata.wallet_name) }; status_label.set_text(cx, &message); - status_label.set_text_color(cx, self.status_color_success); - script_apply_eval!(cx, accept_button, { - enabled: true, - text: "Okay", - draw_bg +: { - color: mod.widgets.COLOR_ACTIVE_PRIMARY, - }, - draw_icon +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - } - draw_text +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - }, - }); + self.update_status_color(cx); + accept_button.set_visible(cx, false); cancel_button.set_visible(cx, false); + okay_button.set_visible(cx, true); } // Handle the wallet creation error action. @@ -345,7 +345,7 @@ impl WidgetMatchEvent for CreateWalletModal { self.is_showing_error = true; let message = format!("Failed to create wallet: {error}."); status_label.set_text(cx, &message); - status_label.set_text_color(cx, self.status_color_error); + self.update_status_color(cx); accept_button.set_enabled(cx, false); cancel_button.set_enabled(cx, true); wallet_name_input.set_is_read_only(cx, false); @@ -365,19 +365,29 @@ impl WidgetMatchEvent for CreateWalletModal { } impl CreateWalletModal { + fn update_status_color(&self, cx: &mut Cx) { + let color = match self.state { + CreateWalletModalState::WaitingForWalletCreation => self.status_color_pending, + CreateWalletModalState::WalletCreated => self.status_color_success, + CreateWalletModalState::WaitingForUserInput | CreateWalletModalState::WalletCreationError => self.status_color_error, + }; + self.view.label(cx, ids!(status_label)).set_text_color(cx, color); + } + pub fn show(&mut self, cx: &mut Cx) { self.state = CreateWalletModalState::WaitingForUserInput; let accept_button = self.view.button(cx, ids!(accept_button)); let cancel_button = self.view.button(cx, ids!(cancel_button)); - accept_button.set_text(cx, "Create Wallet"); + let okay_button = self.view.button(cx, ids!(okay_button)); cancel_button.set_text(cx, "Cancel"); accept_button.reset_hover(cx); cancel_button.reset_hover(cx); + okay_button.reset_hover(cx); accept_button.set_enabled(cx, true); cancel_button.set_enabled(cx, true); accept_button.set_visible(cx, true); cancel_button.set_visible(cx, true); - // TODO: return buttons to their default state/appearance + okay_button.set_visible(cx, false); self.view.text_input(cx, ids!(wallet_name_input)).set_is_read_only(cx, false); self.view.text_input(cx, ids!(wallet_file_name_input)).set_is_read_only(cx, false); self.view.password_text_input(cx, ids!(password_input)).set_is_read_only(cx, false); diff --git a/src/tsp/tsp_verification_modal.rs b/src/tsp/tsp_verification_modal.rs index 65e56b978..c8cfbfab2 100644 --- a/src/tsp/tsp_verification_modal.rs +++ b/src/tsp/tsp_verification_modal.rs @@ -2,7 +2,7 @@ use makepad_widgets::*; use tsp_sdk::AsyncSecureStore; -use crate::{shared::styles::apply_positive_button_style, sliding_sync::current_user_id, tsp::{submit_tsp_request, TspRequest, TspVerificationDetails}}; +use crate::{sliding_sync::current_user_id, tsp::{submit_tsp_request, TspRequest, TspVerificationDetails}}; script_mod! { link tsp_enabled @@ -38,6 +38,15 @@ script_mod! { icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } text: "Accept Request" } + + okay_button := RobrixIconButton { + visible: false + align: Align{x: 0.5, y: 0.5} + padding: 15, + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "Okay" + } } } } @@ -104,8 +113,9 @@ impl Widget for TspVerificationModal { impl WidgetMatchEvent for TspVerificationModal { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { - let mut accept_button = self.button(cx, ids!(accept_button)); + let accept_button = self.button(cx, ids!(accept_button)); let cancel_button = self.button(cx, ids!(cancel_button)); + let okay_button = self.button(cx, ids!(okay_button)); let cancel_button_clicked = cancel_button.clicked(actions); let modal_dismissed = actions @@ -136,7 +146,7 @@ impl WidgetMatchEvent for TspVerificationModal { } let prompt_label = self.view.label(cx, ids!(body)); - if accept_button.clicked(actions) { + if accept_button.clicked(actions) || okay_button.clicked(actions) { let current_state = std::mem::take(&mut self.state); let new_state: TspVerificationModalState; match current_state { @@ -171,18 +181,8 @@ impl WidgetMatchEvent for TspVerificationModal { accepted: false, }); cancel_button.set_visible(cx, false); - script_apply_eval!(cx, accept_button, { - text: "Okay", - draw_bg +: { - color: mod.widgets.COLOR_ACTIVE_PRIMARY, - }, - draw_icon +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - } - draw_text +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - }, - }); + accept_button.set_visible(cx, false); + okay_button.set_visible(cx, true); new_state = TspVerificationModalState::RequestDeclined; } else { @@ -240,19 +240,8 @@ impl WidgetMatchEvent for TspVerificationModal { } } cancel_button.set_visible(cx, false); - script_apply_eval!(cx, accept_button, { - enabled: true, - text: "Okay", - draw_bg +: { - color: mod.widgets.COLOR_ACTIVE_PRIMARY, - }, - draw_icon +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - } - draw_text +: { - color: mod.widgets.COLOR_TEXT_ON_ACCENT, - } - }); + accept_button.set_visible(cx, false); + okay_button.set_visible(cx, true); self.redraw(cx); } _ => {} @@ -280,9 +269,9 @@ impl TspVerificationModal { ); self.label(cx, ids!(body)).set_text(cx, &prompt_text); - let mut accept_button = self.button(cx, ids!(accept_button)); + let accept_button = self.button(cx, ids!(accept_button)); let cancel_button = self.button(cx, ids!(cancel_button)); - apply_positive_button_style(cx, &mut accept_button); + let okay_button = self.button(cx, ids!(okay_button)); accept_button.set_text(cx, "Accept Request"); accept_button.set_enabled(cx, true); accept_button.set_visible(cx, true); @@ -291,6 +280,8 @@ impl TspVerificationModal { cancel_button.set_enabled(cx, true); cancel_button.set_visible(cx, true); cancel_button.reset_hover(cx); + okay_button.set_visible(cx, false); + okay_button.reset_hover(cx); self.state = TspVerificationModalState::ReceivedRequest { details,