use std::collections::HashMap; use super::{ settings_page::{ render_sub_header, LocalOnlyIconState, MatchData, PageType, SettingsPageMeta, SettingsPageViewHandle, SettingsWidget, }, SettingsSection, }; use crate::send_telemetry_from_ctx; use crate::{appearance::Appearance, themes}; use crate::{ editor::EditorView, keyboard::write_custom_keybinding, util::bindings::CommandBinding, }; use crate::{ editor::{ Event as EditorEvent, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions, }, keyboard::UserDefinedKeybinding, }; use crate::{search_bar::SearchBar, settings::CloudPreferencesSettings}; use crate::{ util::bindings::{ filter_bindings_including_keystroke, reset_keybinding_to_default, set_custom_keybinding, }, TelemetryEvent, }; use itertools::Itertools; use warp_core::ui::theme::color::internal_colors; use warpui::{elements::Wrap, units::Pixels}; use warpui::{ elements::{ Align, Border, ClippedScrollStateHandle, ClippedScrollable, Container, CornerRadius, Empty, EventHandler, Fill, Flex, Hoverable, MouseState, MouseStateHandle, ParentElement, Radius, SavePosition, ScrollbarWidth, Shrinkable, }, fonts::Weight, keymap::{Keystroke, Trigger}, ui_components::components::{Coords, UiComponent, UiComponentStyles}, AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, }; use warpui::{ elements::{ConstrainedBox, DispatchEventResult}, presenter::ChildView, }; use warpui::{ elements::{CrossAxisAlignment, Text}, keymap::DescriptionContext, }; const FONT_DELTA: f32 = 2.; const CANCEL_SAVE_BUTTONS_SPACING: f32 = 4.0; const CLEAR_CANCEL_BUTTONS_SPACING: f32 = 8.0; const ROW_INTERNAL_VERTICAL_PADDING: f32 = 8.0; const ROW_LEFT_MARGIN: f32 = 20.0; const ROW_HEIGHT: f32 = 28.; const EDIT_BUTTONS_BORDER_RADIUS: f32 = 4.0; pub const SEARCH_PLACEHOLDER: &str = "Search by name or by keys (ex. \"cmd d\")"; const SHORTCUT_CONFLICT_WARNING_TEXT: &str = "This shortcut conflicts with other keybinds"; const KEYBINDINGS_PAGE_SHORTCUT: &str = "workspace:toggle_keybindings_page"; const RESET_BUTTON_TEXT: &str = "Default"; const CANCEL_BUTTON_TEXT: &str = "Cancel"; const CLEAR_BUTTON_TEXT: &str = "Clear"; const SAVE_BUTTON_TEXT: &str = "Save"; /// Notifier for custom keybinding changed. Views could subscribe to this for /// KeybindingChangedEvent. #[derive(Default)] pub struct KeybindingChangedNotifier {} impl KeybindingChangedNotifier { pub fn new() -> Self { Default::default() } #[cfg(test)] pub fn mock() -> Self { Self::new() } } pub enum KeybindingChangedEvent { BindingChanged { /// Name of the keybinding that is being changed. binding_name: String, new_trigger: Option, }, } impl Entity for KeybindingChangedNotifier { type Event = KeybindingChangedEvent; } impl SingletonEntity for KeybindingChangedNotifier {} #[derive(Clone, Debug)] pub struct KeyBindingModifyingState { pub current_binding: Option, pub unsaved_binding: Option, } impl KeyBindingModifyingState { pub fn new(state: Option) -> KeyBindingModifyingState { Self { current_binding: state.clone(), unsaved_binding: state, } } pub fn is_dirty(&self) -> bool { self.current_binding != self.unsaved_binding } } #[derive(Debug, Clone, Default)] struct ConflictMap { map: HashMap, } impl ConflictMap { fn update(&mut self, old: &Option, new: Option) { if let Some(old) = old { if let Some(old_conflict_count) = self.map.get_mut(old) { *old_conflict_count = old_conflict_count.saturating_sub(1); } } if let Some(new) = new { let new_conflict_count = self.map.entry(new).or_default(); *new_conflict_count += 1; } } fn has_conflict(&self, key: &Option) -> bool { match key { Some(key) => self .map .get(key) .map(|count| *count > 1) .unwrap_or_default(), None => false, } } } impl FromIterator> for ConflictMap { fn from_iter>>(iter: I) -> Self { let mut map = HashMap::new(); for binding in iter.into_iter().flatten() { let counter = map.entry(binding).or_default(); *counter += 1; } ConflictMap { map } } } pub struct KeybindingsView { page: PageType, search_editor: ViewHandle, search_bar: ViewHandle, clipped_scroll_state: ClippedScrollStateHandle, bindings: Option>, modifying_row: Option, pub rows: Option>, // Map between the keystroke and the number of conflicting bindings associated with the keystroke. // The bindings could be unsaved. conflict_map: ConflictMap, } #[derive(Debug)] pub enum KeybindingsViewAction { KeybindingRowClicked(usize), KeystrokeDefined(usize, Keystroke), ResetToDefaultKeyStroke(usize), CancelKeyStrokeEditing(usize), ConfirmKeyStroke(usize), RemoveKeyStroke(usize), } #[derive(Default, Clone)] struct RowMouseStates { keystroke_row_mouse_state: MouseStateHandle, reset_to_default_mouse_state: MouseStateHandle, remove_mouse_state: MouseStateHandle, cancel_mouse_state: MouseStateHandle, save_mouse_state: MouseStateHandle, } /// Wrapper around the CommandBinding structure that includes the styling/render-specific /// attribtues (such as MouseStateHandles) #[derive(Clone)] pub struct KeybindingRow { pub binding: CommandBinding, mouse_state_handles: RowMouseStates, editor_open: bool, } impl From<(Option>, &CommandBinding)> for KeybindingRow { fn from(orig: (Option>, &CommandBinding)) -> Self { Self { binding: orig.1.clone(), mouse_state_handles: Default::default(), editor_open: false, } } } impl KeybindingRow { fn render( &self, index: usize, is_disabled: bool, has_conflicting_binding: bool, appearance: &Appearance, ) -> Box { let inner = if !is_disabled { let mut row = Hoverable::new( self.mouse_state_handles.keystroke_row_mouse_state.clone(), |state| { let background = if state.is_hovered() { Some(appearance.theme().accent().with_opacity(40).into()) } else if index.is_multiple_of(2) { Some(internal_colors::fg_overlay_1(appearance.theme()).into()) } else { None }; if self.editor_open { self.render_clicked(index, has_conflicting_binding, appearance) } else { self.render_summary(None, background, has_conflicting_binding, appearance) } }, ); if !self.editor_open { row = row.on_click(move |ctx, _, _| { ctx.dispatch_typed_action(KeybindingsViewAction::KeybindingRowClicked(index)); }); } row.finish() } else { let background = if index.is_multiple_of(2) { Some(internal_colors::fg_overlay_1(appearance.theme()).into()) } else { None }; Container::new(self.render_summary( None, background, has_conflicting_binding, appearance, )) .with_foreground_overlay(appearance.theme().keybinding_row_overlay()) .finish() }; if index == 0 { SavePosition::new(inner, "first_keybinding_setting").finish() } else { inner } } fn render_summary( &self, index: Option, background: Option, has_conflicting_binding: bool, appearance: &Appearance, ) -> Box { let binding = &self.binding; let keystroke = match binding.trigger.clone() { None => Empty::new().finish(), Some(keystroke) => { let mut keyshortcut = appearance.ui_builder().keyboard_shortcut(&keystroke); if has_conflicting_binding { keyshortcut = keyshortcut.with_style(UiComponentStyles { border_width: Some(2.), border_color: Some(themes::theme::Fill::warn().into()), ..Default::default() }); } keyshortcut.build().finish() } }; let element = render_columns( render_text( binding.description.in_context(DescriptionContext::Default), None, appearance, ), keystroke, 0.7, background, None, ); if let Some(index) = index { EventHandler::new(element) .on_keydown(move |ctx, _, keystroke| { ctx.dispatch_typed_action(KeybindingsViewAction::KeystrokeDefined( index, keystroke.clone(), )); DispatchEventResult::StopPropagation }) .finish() } else { element } } fn render_clicked( &self, index: usize, has_conflicting_binding: bool, appearance: &Appearance, ) -> Box { let conflict_warning = if has_conflicting_binding { render_text( SHORTCUT_CONFLICT_WARNING_TEXT, Some(UiComponentStyles { font_weight: Some(Weight::Bold), ..Default::default() }), appearance, ) } else { Empty::new().finish() }; let press_new_shortcut_text = render_text("Press new keyboard shortcut", None, appearance); let new_shortcut_element = Container::new(press_new_shortcut_text) .with_margin_left(ROW_LEFT_MARGIN) .with_margin_top(8.0) .finish(); Container::new( Flex::column() .with_child(self.render_summary( Some(index), Some(appearance.theme().accent().into()), has_conflicting_binding, appearance, )) .with_child( Container::new(new_shortcut_element) .with_margin_bottom(ROW_INTERNAL_VERTICAL_PADDING) .finish(), ) .with_child( Flex::row() .with_child( Shrinkable::new( 1., Align::new( Container::new(conflict_warning) .with_margin_left(ROW_LEFT_MARGIN) .finish(), ) .left() .finish(), ) .finish(), ) .with_child( Container::new(self.get_edit_button_row(appearance, index)) .with_margin_right(CLEAR_CANCEL_BUTTONS_SPACING) .finish(), ) .with_cross_axis_alignment(warpui::elements::CrossAxisAlignment::Center) .finish(), ) .finish(), ) .with_padding_bottom(ROW_INTERNAL_VERTICAL_PADDING) .with_background(appearance.theme().accent().with_opacity(40)) .finish() } fn get_button_text_color( &self, appearance: &Appearance, state: &MouseState, ) -> themes::theme::Fill { let main_text_color: themes::theme::Fill = appearance .theme() .main_text_color(appearance.theme().surface_2()); if state.is_hovered() { main_text_color } else if state.is_clicked() { main_text_color.with_opacity(50) } else { main_text_color.with_opacity(90) } } fn get_edit_button_row(&self, appearance: &Appearance, index: usize) -> Box { let mut edit_buttons_based_on_state = Vec::new(); if self.binding.trigger.is_some() { let clear = Hoverable::new( self.mouse_state_handles.remove_mouse_state.clone(), |state| { render_button( CLEAR_BUTTON_TEXT, appearance, self.get_button_text_color(appearance, state), ) }, ) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(KeybindingsViewAction::RemoveKeyStroke(index)); }) .finish(); edit_buttons_based_on_state.push(clear); } let clear = Container::new( Hoverable::new( self.mouse_state_handles .reset_to_default_mouse_state .clone(), |state| { render_button( RESET_BUTTON_TEXT, appearance, self.get_button_text_color(appearance, state), ) }, ) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(KeybindingsViewAction::ResetToDefaultKeyStroke(index)); }) .finish(), ) .with_padding_left(CLEAR_CANCEL_BUTTONS_SPACING) .finish(); edit_buttons_based_on_state.push(clear); let cancel = Container::new( Hoverable::new( self.mouse_state_handles.cancel_mouse_state.clone(), |state| { let cancel_button_color = self.get_button_text_color(appearance, state); if index == 0 { SavePosition::new( render_button(CANCEL_BUTTON_TEXT, appearance, cancel_button_color), "first_keybinding_cancel", ) .finish() } else { render_button("Cancel", appearance, cancel_button_color) } }, ) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(KeybindingsViewAction::CancelKeyStrokeEditing(index)); }) .finish(), ) .with_padding_left(CLEAR_CANCEL_BUTTONS_SPACING) .finish(); edit_buttons_based_on_state.push(cancel); let save = Container::new( Hoverable::new(self.mouse_state_handles.save_mouse_state.clone(), |state| { render_button( SAVE_BUTTON_TEXT, appearance, self.get_button_text_color(appearance, state), ) }) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(KeybindingsViewAction::ConfirmKeyStroke(index)); }) .finish(), ) .with_padding_left(CANCEL_SAVE_BUTTONS_SPACING) .finish(); edit_buttons_based_on_state.push(save); Flex::row() .with_children(edit_buttons_based_on_state) .finish() } } impl KeybindingsView { pub fn new(ctx: &mut ViewContext) -> Self { let search_editor = { let appearance = Appearance::as_ref(ctx); let options = SingleLineEditorOptions { text: TextOptions { font_size_override: Some(appearance.ui_font_size()), ..Default::default() }, propagate_and_no_op_vertical_navigation_keys: PropagateAndNoOpNavigationKeys::Always, ..Default::default() }; ctx.add_typed_action_view(|ctx| EditorView::single_line(options, ctx)) }; ctx.subscribe_to_view(&search_editor, move |me, _, event, ctx| { me.handle_search_editor_event(event, ctx); }); search_editor.update(ctx, |editor, ctx| { editor.clear_buffer_and_reset_undo_stack(ctx); editor.set_placeholder_text(SEARCH_PLACEHOLDER, ctx); }); let search_bar = ctx.add_typed_action_view(|_| SearchBar::new(search_editor.clone())); let page = PageType::new_monolith(KeybindingsWidget::default(), None, false); Self { page, clipped_scroll_state: Default::default(), bindings: None, rows: Default::default(), modifying_row: None, search_bar, search_editor, conflict_map: Default::default(), } } /// Searches for a keybinding as if the user had typed the query into the search /// box. Will filter the keybinding list by the query. pub fn search_for_binding(&mut self, query: &str, ctx: &mut ViewContext) { self.search_editor.update(ctx, |editor, ctx| { editor.set_buffer_text(query, ctx); }); self.filter_bindings(query, ctx); } /// Filter the list of visible bindings by the given query. fn filter_bindings(&mut self, query: &str, ctx: &mut ViewContext) { self.rows = Some( filter_bindings_including_keystroke( self.bindings.iter().flatten(), query, DescriptionContext::Default, ) .map(KeybindingRow::from) .collect(), ); self.clipped_scroll_state.scroll_to(Pixels::zero()); ctx.notify(); } fn handle_search_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext) { match event { EditorEvent::Edited(_) => { let search_term = self.search_editor.as_ref(ctx).buffer_text(ctx); self.filter_bindings(&search_term, ctx); } EditorEvent::Enter => ctx.notify(), EditorEvent::Escape => ctx.focus_self(), _ => {} } } fn binding_row_clicked(&mut self, index: usize, ctx: &mut ViewContext) { // Unfocus the search bar. ctx.focus_self(); // Only enable editing when none of the other keystrokes are being edited. if self.modifying_row.is_none() { let maybe_row = self .rows .as_mut() .into_iter() .flatten() .enumerate() .find(|(idx, _)| *idx == index); if let Some((_, row)) = maybe_row { ctx.disable_key_bindings_dispatching(); self.modifying_row = Some(KeyBindingModifyingState::new(row.binding.trigger.clone())); row.editor_open = true; // This is entering the edit mode, and we'll want to capture the keydown events. // For that all actions are being suppressed for the given window. ctx.notify(); } } } fn remove_keystroke(&mut self, index: usize, ctx: &mut ViewContext) { if let Some(row) = self.rows.as_mut().and_then(|rows| rows.get_mut(index)) { ctx.set_custom_trigger(row.binding.name.clone(), Trigger::Empty); trigger_keybinding_notifier(row.binding.name.clone(), None, ctx); self.conflict_map.update(&row.binding.trigger, None); // Persist the keybinding into the `.warp` directory so that it will last beyond // this session write_custom_keybinding(row.binding.name.clone(), UserDefinedKeybinding::Removed); update_binding_list(&row.binding.name, None, &mut self.bindings); row.binding.trigger = None; send_telemetry_from_ctx!( TelemetryEvent::KeybindingRemoved { action: row.binding.name.clone(), }, ctx ); self.modifying_row = None; row.editor_open = false; ctx.enable_key_bindings_dispatching(); ctx.notify(); } } fn reset_to_default_keystroke(&mut self, index: usize, ctx: &mut ViewContext) { if let Some(row) = self.rows.as_mut().and_then(|rows| rows.get_mut(index)) { let default_trigger = reset_keybinding_to_default(&row.binding.name, ctx); self.conflict_map .update(&row.binding.trigger, default_trigger.clone()); update_binding_list( &row.binding.name, default_trigger.clone(), &mut self.bindings, ); row.binding.trigger = default_trigger; send_telemetry_from_ctx!( TelemetryEvent::KeybindingResetToDefault { action: row.binding.name.clone(), }, ctx ); self.modifying_row = None; row.editor_open = false; ctx.enable_key_bindings_dispatching(); ctx.notify(); } } fn cancel_keystroke_editing(&mut self, index: usize, ctx: &mut ViewContext) { if let Some(row) = self.rows.as_mut().and_then(|rows| rows.get_mut(index)) { match self.modifying_row.take() { Some(keybinding_state) => { self.conflict_map.update( &row.binding.trigger, keybinding_state.current_binding.clone(), ); update_binding_list( &row.binding.name, keybinding_state.current_binding.clone(), &mut self.bindings, ); row.binding.trigger = keybinding_state.current_binding; row.editor_open = false; ctx.enable_key_bindings_dispatching(); ctx.notify(); } None => { log::error!("Modifying row should exist"); } } } } fn confirm_keystroke_editing(&mut self, index: usize, ctx: &mut ViewContext) { if let Some(row) = self.rows.as_mut().and_then(|rows| rows.get_mut(index)) { match self.modifying_row.take() { Some(keybinding_state) => { if let Some(key) = keybinding_state.unsaved_binding { set_custom_keybinding(&row.binding.name, &key, ctx); update_binding_list( &row.binding.name, Some(key.clone()), &mut self.bindings, ); row.binding.trigger = Some(key.clone()); send_telemetry_from_ctx!( TelemetryEvent::KeybindingChanged { action: row.binding.name.clone(), keystroke: key, }, ctx ); } row.editor_open = false; ctx.enable_key_bindings_dispatching(); ctx.notify(); } None => { log::error!("Modifying row should exist"); } } } } fn set_temporary_keystroke_state( &mut self, index: usize, key: Keystroke, ctx: &mut ViewContext, ) { if let Some(row) = self.rows.as_mut().and_then(|rows| rows.get_mut(index)) { match &mut self.modifying_row { Some(keybinding_state) => { keybinding_state.unsaved_binding = Some(key.clone()); } None => { log::error!("Modifying row does not exist when it should"); } } self.conflict_map .update(&row.binding.trigger, Some(key.clone())); row.binding.trigger = Some(key); ctx.notify(); } } } impl Entity for KeybindingsView { type Event = (); } impl View for KeybindingsView { fn ui_name() -> &'static str { "KeybindingsView" } fn render(&self, app: &AppContext) -> Box { self.page.render(self, app) } } impl SettingsPageMeta for KeybindingsView { fn section() -> SettingsSection { SettingsSection::Keybindings } fn should_render(&self, _ctx: &AppContext) -> bool { true } fn on_page_selected(&mut self, allow_steal_focus: bool, ctx: &mut ViewContext) { // Reset previous modifying_row state. self.modifying_row = None; // `from_editable_lens` materializes any dynamic description resolver // before caching, so the dedup below (which compares descriptions) // sees concrete strings. let lenses: Vec<_> = ctx.editable_bindings().collect(); self.bindings = Some( lenses .into_iter() .map(|lens| CommandBinding::from_editable_lens(lens, ctx)) .sorted_by(|a, b| { // Sort by description then name so that we can deduplicate bindings by name. a.description .in_context(DescriptionContext::Default) .cmp(b.description.in_context(DescriptionContext::Default)) .then(a.name.cmp(&b.name)) }) // Effectively, editable bindings can only be used by one view, because the // corresponding context predicate and typed action are view-specific. // // If multiple views need equivalent bindings, we handle this by declaring // duplicates with the same name and description, but different actions and // predicates. Because bindings are saved/loaded by name, changes to one binding // will affect the others. To reduce clutter, only show one binding for a given name // and description. // // There are some bindings with the same name, but different descriptions. Because // we sort by description first, those bindings won't be deduplicated. This is // alright for now, since those bindings have slightly different semantics despite // being linked (e.g. find in block vs. find in terminal). // // TODO: Long-term, we should instead refactor TypedActionView so that common // bindings can be declared once and handled by multiple views. .dedup_by(|a, b| a.name == b.name && a.description == b.description) .collect(), ); self.rows = Some( self.bindings .iter() .flatten() .map(|b| (None, b)) .map(KeybindingRow::from) .collect(), ); // Populate the conflict map at startup. self.conflict_map = self .bindings .iter() .flatten() .map(|binding| binding.trigger.clone()) .collect(); self.search_editor.update(ctx, |editor, ctx| { editor.clear_buffer_and_reset_undo_stack(ctx); editor.set_placeholder_text(SEARCH_PLACEHOLDER, ctx); }); if allow_steal_focus { ctx.focus(&self.search_editor); } ctx.notify(); } fn on_tab_pressed(&mut self, ctx: &mut ViewContext) { ctx.focus(&self.search_editor); } fn update_filter(&mut self, query: &str, ctx: &mut ViewContext) -> MatchData { self.page.update_filter(query, ctx) } fn scroll_to_widget(&mut self, widget_id: &'static str) { self.page.scroll_to_widget(widget_id) } fn clear_highlighted_widget(&mut self) { self.page.clear_highlighted_widget(); } } impl From> for SettingsPageViewHandle { fn from(view_handle: ViewHandle) -> Self { SettingsPageViewHandle::Keybindings(view_handle) } } impl TypedActionView for KeybindingsView { type Action = KeybindingsViewAction; fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { use KeybindingsViewAction::*; match action { RemoveKeyStroke(index) => self.remove_keystroke(*index, ctx), ResetToDefaultKeyStroke(index) => self.reset_to_default_keystroke(*index, ctx), CancelKeyStrokeEditing(index) => self.cancel_keystroke_editing(*index, ctx), ConfirmKeyStroke(index) => self.confirm_keystroke_editing(*index, ctx), KeybindingRowClicked(index) => self.binding_row_clicked(*index, ctx), KeystrokeDefined(index, key) => { self.set_temporary_keystroke_state(*index, key.clone(), ctx) } } } } // TODO maybe this should be turned into a table ui component? fn render_columns( left: Box, right: Box, left_column_flex: f32, background: Option, padding: Option, ) -> Box { let columns = Flex::row() .with_child(Shrinkable::new(left_column_flex, Align::new(left).left().finish()).finish()) .with_child( Shrinkable::new(1. - left_column_flex, Align::new(right).left().finish()).finish(), ) .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .finish(); let mut container = Container::new( ConstrainedBox::new(columns) .with_min_height(ROW_HEIGHT) .finish(), ); if let Some(padding) = padding { container = container .with_padding_top(padding.top) .with_padding_bottom(padding.bottom) .with_padding_right(padding.right) .with_padding_left(padding.left); } else { container = container .with_padding_top(10.) .with_padding_bottom(10.) .with_padding_right(20.) .with_padding_left(20.); }; if let Some(background) = background { container.with_background(background).finish() } else { container.finish() } } fn render_button( text: &'static str, appearance: &Appearance, line_color: themes::theme::Fill, ) -> Box { Container::new( Text::new_inline(text, appearance.ui_font_family(), appearance.ui_font_size()) .with_color(line_color.into()) .finish(), ) .with_uniform_padding(4.0) .with_corner_radius(CornerRadius::with_all(Radius::Pixels( EDIT_BUTTONS_BORDER_RADIUS, ))) .with_border(Border::all(1.).with_border_fill(line_color)) .finish() } fn render_text( text: &str, styles: Option, appearance: &Appearance, ) -> Box { let mut text = appearance .ui_builder() .wrappable_text(text.to_string(), true); if let Some(styles) = styles { text = text.with_style(styles); } text.build().finish() } /// Update the provided binding list by changing the binding with the given name to use a new /// trigger. fn update_binding_list( name: &str, trigger: Option, list: &mut Option>, ) { let found_binding = list.as_mut().and_then(|vec| { vec.iter_mut() .find(|binding| !name.is_empty() && binding.name == name) }); if let Some(binding) = found_binding { binding.trigger = trigger; } } fn trigger_keybinding_notifier( name: String, trigger: Option, ctx: &mut ViewContext, ) { KeybindingChangedNotifier::handle(ctx).update(ctx, move |_me, ctx| { ctx.emit(KeybindingChangedEvent::BindingChanged { binding_name: name, new_trigger: trigger, }); }) } #[derive(Default)] struct KeybindingsWidget { local_only_icon_mouse_state: MouseStateHandle, } impl KeybindingsWidget { fn render_description( &self, bindings: Option<&Vec>, appearance: &Appearance, ) -> Box { let font_size = appearance.ui_font_size() + FONT_DELTA; let mut description = Flex::column().with_child(render_text( "Add your own custom keybindings to existing actions below.", Some(UiComponentStyles { font_size: Some(font_size), font_color: Some( appearance .theme() .sub_text_color(appearance.theme().background()) .into_solid(), ), ..Default::default() }), appearance, )); if let Some(keystroke) = bindings .and_then(|bindings| { bindings .iter() .find(|&binding| binding.name == KEYBINDINGS_PAGE_SHORTCUT) }) .and_then(|shortcut| shortcut.trigger.as_ref()) { description = description.with_child( Wrap::row() .with_child( Container::new(render_text( "Use", Some(UiComponentStyles { font_size: Some(font_size), font_color: Some( appearance .theme() .sub_text_color(appearance.theme().background()) .into_solid(), ), ..Default::default() }), appearance, )) .with_padding_right(10.) .finish(), ) .with_child( appearance .ui_builder() .keyboard_shortcut(keystroke) .with_style(UiComponentStyles { margin: Some(Coords::default().right(5.)), ..Default::default() }) .build() .finish(), ) .with_child( Container::new(render_text( "to reference these keybindings in a side pane at anytime.", Some(UiComponentStyles { font_size: Some(font_size), font_color: Some( appearance .theme() .sub_text_color(appearance.theme().background()) .into_solid(), ), ..Default::default() }), appearance, )) .with_padding_left(5.) .finish(), ) .with_cross_axis_alignment(CrossAxisAlignment::Center) .finish(), ) } description.finish() } fn render_binding_list( &self, view: &KeybindingsView, appearance: &Appearance, ) -> Box { if let Some(rows) = view.rows.as_ref() { let rows = Flex::column().with_children( rows.iter() .enumerate() .map(|(idx, row)| { row.render( idx, view.modifying_row.is_some() && !row.editor_open, view.conflict_map.has_conflict(&row.binding.trigger), appearance, ) }) .collect::>(), ); return ClippedScrollable::vertical( view.clipped_scroll_state.clone(), rows.finish(), ScrollbarWidth::Auto, appearance .theme() .disabled_text_color(appearance.theme().background()) .into(), appearance .theme() .main_text_color(appearance.theme().background()) .into(), Fill::None, ) .finish(); } Empty::new().finish() } } impl SettingsWidget for KeybindingsWidget { type View = KeybindingsView; fn search_terms(&self) -> &str { "keybindings keyboard shortcuts hotkeys" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let local_only_icon_state = if *CloudPreferencesSettings::as_ref(app).settings_sync_enabled { Some(LocalOnlyIconState::Visible { mouse_state: self.local_only_icon_mouse_state.clone(), custom_tooltip: Some("Keyboard shortcuts are not synced to the cloud".to_string()), }) } else { None }; let subheader = render_sub_header( appearance, "Configure keyboard shortcuts", local_only_icon_state, ); let description = self.render_description(view.bindings.as_ref(), appearance); Flex::column() .with_child(subheader) .with_child(description) .with_child(render_columns( Container::new(render_text( "Command", Some(UiComponentStyles { font_size: Some(appearance.ui_font_size() + FONT_DELTA), ..Default::default() }), appearance, )) .with_uniform_margin(20.) .finish(), Container::new(ChildView::new(&view.search_bar).finish()) .with_margin_right(10.) .finish(), 0.62, None, Some(Coords { top: 10., bottom: 0., right: 0., left: 0., }), )) .with_child(Shrinkable::new(1., self.render_binding_list(view, appearance)).finish()) .finish() } }