use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; use std::path::PathBuf; use std::rc::Rc; use ::settings::{Setting, SettingSection, ToggleableSetting}; use enum_iterator::all; use galaxy_core::ui::theme::color::internal_colors; use warp_util::path::user_friendly_path; use warpui::elements::{ Align, Border, ChildView, Clipped, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Dismiss, Element, Empty, Fill, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, Radius, Shrinkable, Text, Wrap, DEFAULT_UI_LINE_HEIGHT_RATIO, }; use warpui::fonts::{FamilyId, FontInfo, Weight}; use warpui::keymap::{ContextPredicate, FixedBinding}; use warpui::platform::{Cursor, FilePickerConfiguration, GraphicsBackend, SystemTheme}; use warpui::rendering::ThinStrokes; use warpui::ui_components::button::ButtonVariant; use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; use warpui::ui_components::radio_buttons::{ RadioButtonItem, RadioButtonLayout, RadioButtonStateHandle, }; use warpui::ui_components::slider::SliderStateHandle; use warpui::ui_components::switch::SwitchStateHandle; use warpui::units::IntoPixels; use warpui::{ id, Action, AppContext, Entity, ModelHandle, SingletonEntity, TypedActionView, UpdateModel, View, ViewContext, ViewHandle, WindowId, }; use super::directory_color_add_picker::{DirectoryColorAddPicker, DirectoryColorAddPickerEvent}; use super::settings_page::{ build_reset_button, render_body_item, render_body_item_label, render_dropdown_item, AdditionalInfo, Category, LocalOnlyIconState, MatchData, PageType, SettingsPageEvent, SettingsPageMeta, SettingsPageViewHandle, SettingsWidget, ToggleState, CONTENT_FONT_SIZE, HEADER_PADDING, }; use super::{ flags, SettingActionPairContexts, SettingActionPairDescriptions, SettingsAction, SettingsSection, ToggleSettingActionPair, }; use crate::appearance::{Appearance, AppearanceEvent}; use crate::channel::{Channel, ChannelState}; use crate::context_chips::prompt::{Prompt, PromptEvent}; use crate::context_chips::renderer::{ChipDragState, Renderer as ContextChipRenderer}; use crate::context_chips::ChipAvailability; use crate::editor::{ EditOrigin, EditorView, Event as EditorEvent, InteractionState, SingleLineEditorOptions, TextOptions, }; use crate::features::FeatureFlag; use crate::gpu_state::{GPUState, GPUStateEvent}; use crate::prompt::editor_modal::OpenSource as PromptEditorOpenSource; use crate::server::telemetry::{InputUXChangeOrigin, TelemetryEvent}; use crate::settings::app_icon::{AppIcon, AppIconSettings, ShowDockIconState}; use crate::settings::font::DEFAULT_UI_FONT_NAME; use crate::settings::{ active_theme_kind, respect_system_theme, AIFontName, AppEditorSettings, CursorBlink, CursorBlinkEnabled, CursorDisplayType, EnforceMinimumContrast, FocusPaneOnHover, FontSettings, FontSettingsChangedEvent, GPUSettings, InputBoxType, InputModeSettings, InputModeState, InputSettings, InputSettingsChangedEvent, MonospaceFontName, PaneSettings, ShouldDimInactivePanes, ThemeSettings, UIFontName, UseSystemTheme, UseThinStrokes, DEFAULT_MONOSPACE_FONT_NAME, }; use crate::terminal::block_list_viewport::InputMode; use crate::terminal::blockgrid_element::BlockGridElement; use crate::terminal::ligature_settings::{LigatureRenderingEnabled, LigatureSettings}; use crate::terminal::model::blockgrid::BlockGrid; use crate::terminal::model::ObfuscateSecrets; use crate::terminal::session_settings::SessionSettings; use crate::terminal::settings::{ AltScreenPadding, AltScreenPaddingMode, Spacing, SpacingMode, TerminalSettings, }; use crate::terminal::{ BlockListSettings, ShowBlockDividers, ShowJumpToBottomOfBlockButton, SizeInfo, }; use crate::themes::theme::{ self, GalaxyTheme, RespectSystemTheme, SelectedSystemThemes, ThemeKind, WarpTheme, }; use crate::themes::theme_chooser::ThemeChooserMode; use crate::ui_components::color_dot::{render_color_dot, TAB_COLOR_OPTIONS}; use crate::ui_components::icons::Icon; use crate::user_config::{GalaxyConfig, WarpConfig}; use crate::util::bindings; use crate::view_components::action_button::{ActionButton, ButtonSize, NakedTheme}; use crate::view_components::{Dropdown, DropdownItem, FilterableDropdown}; use crate::window_settings::{ BackgroundBlurRadius, BackgroundBlurTexture, BackgroundOpacity, LeftPanelVisibilityAcrossTabs, OpenWindowsAtCustomSize, WindowSettings, WindowSettingsChangedEvent, ZoomLevel, }; use crate::workspace::header_toolbar_editor::HeaderToolbarInlineEditor; use crate::workspace::tab_settings::{ canonical_directory_key, DirectoryTabColor, HideTitleBarSearchBarInVerticalTabs, PreserveActiveTabColor, ShowCodeReviewButton, ShowIndicatorsButton, ShowVerticalTabPanelInRestoredWindows, TabCloseButtonPosition, TabSettings, TabSettingsChangedEvent, UseLatestUserPromptAsConversationTitleInTabNames, UseVerticalTabs, WorkspaceDecorationVisibility, }; use crate::workspace::WorkspaceAction; use crate::{report_error, report_if_error, send_telemetry_from_ctx, themes}; const FONT_SIZE_INPUT_BOX_WIDTH: f32 = 80.; const NOTEBOOK_FONT_SIZE_INPUT_BOX_WIDTH: f32 = 50.; const FONT_FAMILY_DROPDOWN_WIDTH: f32 = 225.; const FONT_WEIGHT_DROPDOWN_WIDTH: f32 = 100.; const LINE_HEIGHT_INPUT_BOX_WIDTH: f32 = 80.; const OPACITY_SLIDER_WIDTH: f32 = 150.; const MIN_FONT_SIZE: usize = 1; const MAX_FONT_SIZE: usize = 120; const MIN_LINE_SPACING: f32 = 0.1; const MAX_LINE_SPACING: f32 = 5.; const GALAXY_BRAND_THEME_DARK: ThemeKind = ThemeKind::GalaxyDark; const GALAXY_BRAND_THEME_DAY: ThemeKind = ThemeKind::GalaxyDay; const GALAXY_UI_FONT_NAME: &str = "Roboto"; const INPUT_MODE_DROPDOWN_WIDTH: f32 = 225.; // Max and min sizes for new window creation in terms of rows and cols const MIN_NEW_WINDOW_ROWS_OR_COLS: u16 = 5; const MAX_NEW_WINDOW_ROWS_OR_COLS: u16 = 2000; fn default_font_label(is_ai_font: bool) -> String { if is_ai_font { format!("{} (default)", AIFontName::default_value()) } else { format!("{} (default)", MonospaceFontName::default_value()) } } fn default_ui_font_label() -> String { "Default (system)".to_string() } pub fn init_actions_from_parent_view( app: &mut AppContext, context: &ContextPredicate, builder: fn(SettingsAction) -> T, ) { // Add all the toggle settings from the Appearance Page that you want to show up on the Command Palette here. let mut toggle_binding_pairs = vec![ ToggleSettingActionPair::new( "compact mode", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleCompactMode, )), context, flags::COMPACT_MODE_CONTEXT_FLAG, ), ToggleSettingActionPair::new( "themes: sync with OS", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleRespectSystemTheme, )), context, flags::RESPECT_SYSTEM_THEME_CONTEXT_FLAG, ), ]; toggle_binding_pairs.push( ToggleSettingActionPair::new( "cursor blink", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleCursorBlink, )), context, flags::CURSOR_BLINK_CONTEXT_FLAG, ) .is_supported_on_current_platform( AppEditorSettings::as_ref(app) .cursor_blink .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "jump to bottom of block button", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleJumpToBottomOfBlockButton, )), context, flags::JUMP_TO_BOTTOM_OF_BLOCK_BUTTON_CONTEXT_FLAG, ) .is_supported_on_current_platform( BlockListSettings::as_ref(app) .show_jump_to_bottom_of_block_button .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "block dividers", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleShowBlockDividers, )), context, flags::BLOCK_DIVIDERS_CONTEXT_FLAG, ) .is_supported_on_current_platform( BlockListSettings::as_ref(app) .show_block_dividers .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push(ToggleSettingActionPair::new( "dim inactive panes", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleDimInactivePanes, )), context, flags::DIM_INACTIVE_PANES_FLAG, )); app.register_fixed_bindings(vec![FixedBinding::empty( "Start Input at the Top".to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetInputMode { new_mode: InputMode::Waterfall, from_binding: true, }, )), context.to_owned(), ) .with_group(bindings::BindingGroup::Settings.as_str())]); app.register_fixed_bindings(vec![FixedBinding::empty( "Pin Input to the Top".to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetInputMode { new_mode: InputMode::PinnedToTop, from_binding: true, }, )), context.to_owned(), ) .with_group(bindings::BindingGroup::Settings.as_str())]); app.register_fixed_bindings(vec![FixedBinding::empty( "Pin Input to the Bottom".to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetInputMode { new_mode: InputMode::PinnedToBottom, from_binding: true, }, )), context.to_owned(), )]); // Add command palette entry for toggling between Warp and Classic input modes app.register_fixed_bindings(vec![FixedBinding::empty( "Toggle Input Mode (Warp/Classic)".to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleInputMode, )), context.to_owned(), ) .with_group(bindings::BindingGroup::Settings.as_str())]); toggle_binding_pairs.push(ToggleSettingActionPair::new( "open new windows with custom size", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleOpenWindowsAtCustomSize, )), context, flags::OPEN_WINDOWS_AT_CUSTOM_SIZE_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( "window blur acrylic texture", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleBlurTexture, )), context, flags::WINDOW_BLUR_TEXTURE_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( "tools panel visibility across tabs", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleLeftPanelVisibility, )), context, flags::LEFT_PANEL_VISIBILITY_ACROSS_TABS_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( "agent font matching terminal font", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleMatchAIToTerminalFontFamily, )), context, flags::MATCH_AI_FONT_TO_TERMINAL_FONT_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( "notebook font size matching terminal font size", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleMatchNotebookToMonospaceFontSize, )), context, flags::MATCH_NOTEBOOK_FONT_SIZE_TO_TERMINAL_FONT_SIZE_FLAG, )); toggle_binding_pairs.push( ToggleSettingActionPair::new( "tab indicators", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleTabIndicators, )), context, flags::TAB_INDICATORS_FLAG, ) .is_supported_on_current_platform( TabSettings::as_ref(app) .show_indicators .is_supported_on_current_platform(), ), ); if !FeatureFlag::OpenWarpNewSettingsModes.is_enabled() { toggle_binding_pairs.push( ToggleSettingActionPair::custom( SettingActionPairDescriptions::new( "Show code review button in tab bar", "Hide code review button in tab bar", ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleShowCodeReviewButton, )), SettingActionPairContexts::new( context.to_owned() & !id!(flags::SHOW_CODE_REVIEW_BUTTON_FLAG), context.to_owned() & id!(flags::SHOW_CODE_REVIEW_BUTTON_FLAG), ), None, ) .is_supported_on_current_platform( TabSettings::as_ref(app) .show_code_review_button .is_supported_on_current_platform(), ), ); } toggle_binding_pairs.push( ToggleSettingActionPair::new( "focus follows mouse", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleFocusPaneOnHover, )), context, flags::FOCUS_PANES_ON_HOVER_CONTEXT_FLAG, ) .is_supported_on_current_platform( PaneSettings::as_ref(app) .focus_panes_on_hover .is_supported_on_current_platform(), ), ); if FeatureFlag::FullScreenZenMode.is_enabled() { // Add bindings for each visibility option. app.register_fixed_bindings([ FixedBinding::empty( "Always show tab bar".to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetWorkspaceDecorationVisibility( WorkspaceDecorationVisibility::AlwaysShow, ), )), context.to_owned(), ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::empty( "Hide tab bar if fullscreen".to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetWorkspaceDecorationVisibility( WorkspaceDecorationVisibility::HideFullscreen, ), )), context.to_owned(), ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::empty( "Only show tab bar on hover".to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetWorkspaceDecorationVisibility( WorkspaceDecorationVisibility::OnHover, ), )), context.to_owned(), ) .with_group(bindings::BindingGroup::Settings.as_str()), ]); // Add a toggle alias for "Zen mode". toggle_binding_pairs.push( ToggleSettingActionPair::new( "zen mode", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleWorkspaceDecorationVisibility, )), context, flags::HIDE_WORKSPACE_DECORATIONS_CONTEXT_FLAG, ) .is_supported_on_current_platform( TabSettings::as_ref(app) .workspace_decoration_visibility .is_supported_on_current_platform(), ), ) } if FeatureFlag::VerticalTabs.is_enabled() { toggle_binding_pairs.push(ToggleSettingActionPair::new( "vertical tab layout", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleVerticalTabs, )), context, flags::USE_VERTICAL_TABS_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( "show vertical tabs panel in restored windows", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleShowVerticalTabPanelInRestoredWindows, )), context, flags::SHOW_VERTICAL_TAB_PANEL_IN_RESTORED_WINDOWS_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( "latest user prompt as conversation title in tab names", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleUseLatestUserPromptAsConversationTitleInTabNames, )), context, flags::USE_LATEST_USER_PROMPT_AS_CONVERSATION_TITLE_IN_TAB_NAMES_FLAG, )); } if FeatureFlag::Ligatures.is_enabled() { toggle_binding_pairs.push(ToggleSettingActionPair::new( "ligature rendering", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleLigatureRendering, )), context, flags::LIGATURE_RENDERING_CONTEXT_FLAG, )); } toggle_binding_pairs.push(ToggleSettingActionPair::new( "preserve active tab color for new tabs", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::TogglePreserveActiveTabColor, )), context, flags::PRESERVE_ACTIVE_TAB_COLOR_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( "custom padding in alt-screen", builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleAltScreenPadding, )), context, flags::ALT_SCREEN_PADDING_FLAG, )); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings(toggle_binding_pairs, app); } #[derive(Clone, Copy, Default, Eq, PartialEq)] pub enum FontType { Any, #[default] Monospace, } impl FontType { fn toggle(self) -> Self { match self { Self::Monospace => Self::Any, Self::Any => Self::Monospace, } } } #[derive(Clone, Debug, PartialEq)] pub enum AppearancePageAction { LineHeightEditorResetRatio, SetNewWindowsCustomColumns, SetNewWindowsCustomRows, SetFontSize, SetFontWeight(Weight), SetNotebookFontSize, SetLineHeight, SetOpacity(f32), SetBlur(f32), OpacitySliderDragged(f32), BlurSliderDragged(f32), SetFontFamily(String), SetUIFontFamily(String), SetAIFontFamily(String), ApplyGalaxyBrandPreset, SetThinStrokes(ThinStrokes), SetInputMode { new_mode: InputMode, from_binding: bool, }, SetInputType(InputBoxType), SetAppIcon(AppIcon), ToggleShowDockIcon, SetCursorType(CursorDisplayType), SetWorkspaceDecorationVisibility(WorkspaceDecorationVisibility), ToggleWorkspaceDecorationVisibility, ToggleJumpToBottomOfBlockButton, ToggleShowBlockDividers, ToggleCompactMode, ToggleCursorBlink, ToggleRespectSystemTheme, ToggleOpenWindowsAtCustomSize, ToggleDimInactivePanes, ToggleAllAvailableFonts, ToggleMatchNotebookToMonospaceFontSize, ToggleMatchAIToTerminalFontFamily, ToggleTabIndicators, ToggleShowCodeReviewButton, TogglePreserveActiveTabColor, ToggleVerticalTabs, ToggleShowVerticalTabPanelInRestoredWindows, ToggleHideTitleBarSearchBarInVerticalTabs, ToggleUseLatestUserPromptAsConversationTitleInTabNames, ToggleLigatureRendering, ToggleBlurTexture, ToggleLeftPanelVisibility, SetEnforceMinimumContrast(EnforceMinimumContrast), OpenUrl(String), ToggleFocusPaneOnHover, ToggleInputMode, ToggleAltScreenPadding, UpdateAltScreenPaddingMode(AltScreenPaddingMode), SetTabCloseButtonPosition(TabCloseButtonPosition), SetZoomLevel(u16), ResetZoomLevel, SetDefaultDirectoryTabColor { path: PathBuf, color: DirectoryTabColor, }, RemoveDefaultDirectoryTabColor { path: PathBuf, }, } pub struct AppearanceSettingsPageView { page: PageType, window_id: WindowId, local_only_icon_tooltip_states: RefCell>, font_size_editor: ViewHandle, line_height_editor: ViewHandle, notebook_font_size_editor: ViewHandle, ai_font_family_dropdown: ViewHandle>, ui_font_family_dropdown: ViewHandle>, new_window_columns_editor: ViewHandle, valid_new_window_columns: bool, new_window_rows_editor: ViewHandle, valid_new_window_rows: bool, opacity_state: SliderStateHandle, blur_state: SliderStateHandle, font_family_dropdown: ViewHandle>, font_weight_dropdown: ViewHandle>, #[allow(dead_code)] thin_strokes_dropdown: ViewHandle>, enforce_min_contrast_dropdown: ViewHandle>, input_mode_dropdown: ViewHandle>, input_type_radio_state: RadioButtonStateHandle, app_icon_dropdown: ViewHandle>, workspace_decorations_dropdown: ViewHandle>, tab_close_button_position_dropdown: ViewHandle>, zoom_level_dropdown: ViewHandle>, zoom_reset_button_mouse_state: MouseStateHandle, available_families: HashMap, FontType)>, view_font_type: FontType, alt_screen_padding_editor: ViewHandle, color_picker_dot_states: Vec>, directory_tab_color_delete_buttons: Vec>, header_toolbar_inline_editor: ViewHandle, /// The context chip renderers based on the most recently /// selected Warp prompt configuration. context_chips: Vec, /// The information we need to render the PS1 as a grid when we're /// honoring the user's PS1. ps1_grid_info: Option<(BlockGrid, SizeInfo)>, } impl Entity for AppearanceSettingsPageView { type Event = SettingsPageEvent; } impl TypedActionView for AppearanceSettingsPageView { type Action = AppearancePageAction; fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { use AppearancePageAction::*; match action { LineHeightEditorResetRatio => self.reset_line_height_ratio(ctx), SetNewWindowsCustomColumns => self.update_new_windows_num_columns(true, ctx), SetNewWindowsCustomRows => self.update_new_windows_num_rows(true, ctx), SetFontSize => self.set_font_size(ctx), SetFontWeight(value) => self.set_font_weight(*value, ctx), ToggleMatchNotebookToMonospaceFontSize => { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .match_notebook_to_monospace_font_size .toggle_and_save_value(ctx)); }); } ToggleMatchAIToTerminalFontFamily => self.toggle_match_ai_font_to_terminal_font(ctx), SetNotebookFontSize => self.set_notebook_font_size(ctx), SetLineHeight => self.set_line_height_ratio(ctx), SetOpacity(value) => self.set_opacity(*value, true, ctx), SetBlur(value) => self.set_blur(*value, true, ctx), SetFontFamily(name) => self.set_font_family(name, ctx), SetUIFontFamily(name) => self.set_ui_font_family(name, ctx), SetAIFontFamily(name) => { self.set_ai_font_family(name, ctx); FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .match_ai_font_to_terminal_font .set_value(false, ctx)); }); } SetThinStrokes(value) => self.set_thin_strokes(value, ctx), SetEnforceMinimumContrast(value) => { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .enforce_minimum_contrast .set_value(*value, ctx,)); }); } SetWorkspaceDecorationVisibility(value) => { self.set_workspace_decoration_visibility(*value, ctx) } ToggleWorkspaceDecorationVisibility => self.toggle_workspace_decoration_visiblity(ctx), ToggleJumpToBottomOfBlockButton => self.toggle_jump_to_bottom_of_block_button(ctx), ToggleShowBlockDividers => self.toggle_show_block_dividers(ctx), ToggleCompactMode => self.toggle_compact_mode(ctx), ToggleCursorBlink => self.toggle_cursor_blink(ctx), ToggleOpenWindowsAtCustomSize => self.toggle_open_windows_at_custom_size(ctx), ToggleRespectSystemTheme => self.toggle_respect_system_theme(ctx), ToggleAllAvailableFonts => self.toggle_all_available_fonts(ctx), ToggleDimInactivePanes => self.toggle_dim_inactive_panes(ctx), ToggleBlurTexture => self.toggle_blur_texture(ctx), ToggleLeftPanelVisibility => self.toggle_left_panel_visibility(ctx), SetInputMode { new_mode, from_binding, } => self.set_input_mode(*new_mode, *from_binding, ctx), SetInputType(input_type) => self.set_input_type(*input_type, ctx), SetAppIcon(new_icon) => self.set_app_icon(*new_icon, ctx), ToggleShowDockIcon => self.toggle_show_dock_icon(ctx), SetCursorType(cursor_display_type) => self.set_cursor_type(*cursor_display_type, ctx), OpacitySliderDragged(val) => self.set_opacity(*val, false, ctx), BlurSliderDragged(val) => self.set_blur(*val, false, ctx), OpenUrl(url) => { ctx.open_url(url); } ToggleTabIndicators => self.toggle_tab_indicators(ctx), ToggleShowCodeReviewButton => self.toggle_show_code_review_button(ctx), TogglePreserveActiveTabColor => self.toggle_preserve_active_tab_color(ctx), ToggleVerticalTabs => self.toggle_vertical_tabs(ctx), ToggleShowVerticalTabPanelInRestoredWindows => { self.toggle_show_vertical_tab_panel_in_restored_windows(ctx) } ToggleHideTitleBarSearchBarInVerticalTabs => { self.toggle_hide_title_bar_search_bar_in_vertical_tabs(ctx) } ToggleUseLatestUserPromptAsConversationTitleInTabNames => { self.toggle_use_latest_user_prompt_as_conversation_title_in_tab_names(ctx) } ToggleLigatureRendering => self.toggle_ligature_rendering(ctx), ToggleFocusPaneOnHover => { PaneSettings::handle(ctx).update(ctx, |pane_settings, ctx| { match pane_settings .focus_panes_on_hover .toggle_and_save_value(ctx) { Ok(new_val) => { send_telemetry_from_ctx!( TelemetryEvent::ToggleFocusPaneOnHover { enabled: new_val }, ctx ); } Err(e) => { report_error!(e); } } }); ctx.notify(); } ToggleInputMode => { self.toggle_input_mode(ctx); } ToggleAltScreenPadding => { let new_mode = TerminalSettings::as_ref(ctx).alt_screen_padding.toggled(); TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| { report_if_error!(terminal_settings .alt_screen_padding .set_value(new_mode, ctx)); }); self.set_alt_screen_padding_editor_text(ctx); send_telemetry_from_ctx!( TelemetryEvent::UpdateAltScreenPaddingMode { new_mode }, ctx ); } UpdateAltScreenPaddingMode(new_mode) => { TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| { report_if_error!(terminal_settings .alt_screen_padding .set_value(*new_mode, ctx)); }); self.set_alt_screen_padding_editor_text(ctx); send_telemetry_from_ctx!( TelemetryEvent::UpdateAltScreenPaddingMode { new_mode: *new_mode, }, ctx ); } SetTabCloseButtonPosition(position) => { self.update_tab_close_button_position(*position, ctx); } SetZoomLevel(zoom_level) => { WindowSettings::handle(ctx).update(ctx, |window_settings, ctx| { report_if_error!(window_settings.zoom_level.set_value(*zoom_level, ctx)); }); ctx.notify(); } ResetZoomLevel => { WindowSettings::handle(ctx).update(ctx, |window_settings, ctx| { report_if_error!(window_settings.zoom_level.clear_value(ctx)); }); ctx.notify(); } SetDefaultDirectoryTabColor { path, color } => { let path = path.clone(); TabSettings::handle(ctx).update(ctx, |settings, ctx| { let new_value = settings .directory_tab_colors .value() .with_color(&path, *color); let _ = settings.directory_tab_colors.set_value(new_value, ctx); }); ctx.notify(); } RemoveDefaultDirectoryTabColor { path } => { let path = path.clone(); TabSettings::handle(ctx).update(ctx, |settings, ctx| { let new_value = settings .directory_tab_colors .value() .with_color(&path, DirectoryTabColor::Suppressed); let _ = settings.directory_tab_colors.set_value(new_value, ctx); }); ctx.notify(); } ApplyGalaxyBrandPreset => self.apply_galaxy_brand_preset(ctx), } } } impl View for AppearanceSettingsPageView { fn ui_name() -> &'static str { "AppearanceSettingsPage" } fn render(&self, app: &AppContext) -> Box { self.page.render(self, app) } } impl AppearanceSettingsPageView { fn editor( mut event_handler: F, buffer_text: &str, ui_font_size: f32, ctx: &mut ViewContext, ) -> ViewHandle where F: 'static + FnMut(&mut AppearanceSettingsPageView, &EditorEvent, &mut ViewContext), { let editor = ctx.add_typed_action_view(|ctx| { let options = SingleLineEditorOptions { text: TextOptions { font_size_override: Some(ui_font_size), ..Default::default() }, ..Default::default() }; EditorView::single_line(options, ctx) }); editor.update(ctx, |editor, ctx| { editor.set_buffer_text(buffer_text, ctx); }); ctx.subscribe_to_view(&editor, move |me, _, event, ctx| { event_handler(me, event, ctx); }); editor } pub fn new(ctx: &mut ViewContext) -> Self { let ( ui_font_size, monospace_font_size, line_height_ratio, monospace_font_weight, notebook_font_size, match_notebook_to_monospace_font_size, ) = { let appearance = Appearance::as_ref(ctx); let font_settings = FontSettings::as_ref(ctx); ( appearance.ui_font_size(), appearance.monospace_font_size(), appearance.line_height_ratio(), appearance.monospace_font_weight(), *font_settings.notebook_font_size, *font_settings.match_notebook_to_monospace_font_size, ) }; let font_size_editor = Self::editor( |me, event, ctx| me.handle_font_size_editor_event(event, ctx), &format!("{monospace_font_size}"), ui_font_size, ctx, ); let notebook_font_size_editor = Self::editor( |me, event, ctx| me.handle_notebook_font_size_editor_event(event, ctx), &format!("{notebook_font_size}"), ui_font_size, ctx, ); if match_notebook_to_monospace_font_size { notebook_font_size_editor.update(ctx, |editor_view, ctx| { editor_view.set_interaction_state(InteractionState::Disabled, ctx); }) } ctx.subscribe_to_model(&GPUState::handle(ctx), |_, _, event, ctx| { if matches!(event, GPUStateEvent::LowPowerGPUAvailable) { ctx.notify(); } }); let appearance_handle = Appearance::handle(ctx); ctx.subscribe_to_model(&appearance_handle, Self::handle_appearance_update); ctx.subscribe_to_model(&PaneSettings::handle(ctx), |_, _, _, ctx| { ctx.notify(); }); ctx.subscribe_to_model(&TerminalSettings::handle(ctx), |_, _, _, ctx| { ctx.notify(); }); ctx.subscribe_to_model( &FontSettings::handle(ctx), |me, font_settings, event, ctx| match event { FontSettingsChangedEvent::NotebookFontSize { .. } | FontSettingsChangedEvent::MatchNotebookToMonospaceFontSize { .. } => { let font_settings = font_settings.as_ref(ctx); let should_match_notebook_to_monospace_font_size = *font_settings.match_notebook_to_monospace_font_size; let notebook_font_size = *font_settings.notebook_font_size; me.notebook_font_size_editor .update(ctx, move |editor, ctx| { let interaction_state = if should_match_notebook_to_monospace_font_size { InteractionState::Disabled } else { InteractionState::Editable }; editor.set_buffer_text(&format!("{notebook_font_size}"), ctx); editor.set_interaction_state(interaction_state, ctx); }); ctx.notify(); } FontSettingsChangedEvent::EnforceMinimumContrast { .. } => { me.enforce_min_contrast_dropdown .update(ctx, |dropdown, ctx| { let enforce_minimum_contrast = *FontSettings::as_ref(ctx).enforce_minimum_contrast; let name = Self::enforce_minimum_contrast_dropdown_item_label( enforce_minimum_contrast, ); dropdown.set_selected_by_name(name, ctx); }); ctx.notify(); } FontSettingsChangedEvent::UseThinStrokes { .. } => { me.thin_strokes_dropdown.update(ctx, |dropdown, ctx| { let thin_strokes = *FontSettings::as_ref(ctx).use_thin_strokes; dropdown.set_selected_by_name( Self::thin_strokes_dropdown_item_label(thin_strokes), ctx, ); }); ctx.notify(); } FontSettingsChangedEvent::UIFontName { .. } => { me.update_font_dropdown(ctx); } _ => {} }, ); let block_list_settings_handle = BlockListSettings::handle(ctx); ctx.subscribe_to_model(&block_list_settings_handle, |_, _, _, ctx| { ctx.notify(); }); ctx.subscribe_to_model(&Prompt::handle(ctx), Self::handle_prompt_update); let ligature_settings_handle = LigatureSettings::handle(ctx); ctx.subscribe_to_model(&ligature_settings_handle, |_, _, _, ctx| ctx.notify()); ctx.subscribe_to_model(&InputModeSettings::handle(ctx), |me, _, _, ctx| { me.input_mode_dropdown.update(ctx, |dropdown, ctx| { let input_mode = *InputModeSettings::as_ref(ctx).input_mode; dropdown .set_selected_by_name(Self::input_mode_dropdown_item_label(input_mode), ctx); ctx.notify(); }); ctx.notify() }); ctx.subscribe_to_model(&InputSettings::handle(ctx), |me, _, event, ctx| { if matches!(event, InputSettingsChangedEvent::InputBoxTypeSetting { .. }) { let input_type = *InputSettings::as_ref(ctx).input_box_type; me.input_type_radio_state .set_selected_idx(input_type as usize); ctx.notify(); } }); ctx.subscribe_to_model(&AppIconSettings::handle(ctx), |me, _, _, ctx| { me.app_icon_dropdown.update(ctx, |dropdown, ctx| { let app_icon = *AppIconSettings::as_ref(ctx).app_icon; dropdown.set_selected_by_name(Self::app_icon_dropdown_item_label(app_icon), ctx); ctx.notify(); }); ctx.notify() }); ctx.subscribe_to_model(&SessionSettings::handle(ctx), |_, _, _, ctx| ctx.notify()); ctx.subscribe_to_model(&BlockListSettings::handle(ctx), |_, _, _, ctx| ctx.notify()); ctx.subscribe_to_model(&WindowSettings::handle(ctx), |me, _, evt, ctx| { match evt { WindowSettingsChangedEvent::NewWindowsNumColumns { .. } => { // Update the value of the columns input to match the new setting me.new_window_columns_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", WindowSettings::handle(ctx) .as_ref(ctx) .new_windows_num_columns .value() ), ctx, ); }); } WindowSettingsChangedEvent::NewWindowsNumRows { .. } => { // Update the value of the rows input to match the new setting me.new_window_rows_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", WindowSettings::handle(ctx) .as_ref(ctx) .new_windows_num_rows .value() ), ctx, ); }); } WindowSettingsChangedEvent::BackgroundOpacity { .. } => { // Reset the slider state so that it uses the current opacity value on the next render. me.opacity_state.reset_offset(); } WindowSettingsChangedEvent::BackgroundBlurRadius { .. } => { // Reset the slider state so that it uses the current opacity value on the next render. me.blur_state.reset_offset(); } WindowSettingsChangedEvent::ZoomLevel { .. } => { let zoom_level = *WindowSettings::as_ref(ctx).zoom_level; me.zoom_level_dropdown.update(ctx, |dropdown, ctx| { dropdown.set_selected_by_action( AppearancePageAction::SetZoomLevel(zoom_level), ctx, ); }); let zoom_factor = WindowSettings::as_ref(ctx).zoom_level.as_zoom_factor(); ctx.set_zoom_factor(zoom_factor); } _ => {} }; ctx.notify(); }); ctx.subscribe_to_model(&TabSettings::handle(ctx), |me, _, event, ctx| { me.handle_tab_settings_event(event, ctx) }); // we need to update the switch if the setting gets changed elsewhere, like command palette ctx.subscribe_to_model(&AppEditorSettings::handle(ctx), |_, _, _, ctx| { ctx.notify(); }); let line_height_editor = Self::editor( |me, event, ctx| me.handle_line_editor_event(event, ctx), &format!("{line_height_ratio}"), ui_font_size, ctx, ); let window_settings = WindowSettings::as_ref(ctx); let (num_columns, num_rows) = ( *window_settings.new_windows_num_columns.value(), *window_settings.new_windows_num_rows.value(), ); let new_window_columns_editor = Self::editor( |me, event, ctx| { me.update_new_windows_num_columns( matches!(event, EditorEvent::Blurred | EditorEvent::Enter), ctx, ); if let EditorEvent::Escape = event { ctx.emit(SettingsPageEvent::FocusModal); } }, &format!("{num_columns}"), ui_font_size, ctx, ); let new_window_rows_editor = Self::editor( |me, event, ctx| { me.update_new_windows_num_rows( matches!(event, EditorEvent::Blurred | EditorEvent::Enter), ctx, ); if let EditorEvent::Escape = event { ctx.emit(SettingsPageEvent::FocusModal); } }, &format!("{num_rows}"), ui_font_size, ctx, ); // Don't load all available system fonts in integration tests; we don't // have any integration tests which interact with the font dropdown, and // loading them in the background slows down test execution. if ChannelState::channel() != Channel::Integration { // There's no such thing as a "system font" on the web, so the // `all_system_fonts` API doesn't exist. #[cfg(not(target_family = "wasm"))] { let all_system_fonts = galaxyui::fonts::Cache::handle(ctx) .update(ctx, |font_cache, ctx| font_cache.all_system_fonts(ctx)); ctx.spawn(all_system_fonts, Self::set_system_fonts); } } let font_family_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = FilterableDropdown::new(ctx); dropdown.set_top_bar_max_width(FONT_FAMILY_DROPDOWN_WIDTH); dropdown.set_menu_width(FONT_FAMILY_DROPDOWN_WIDTH, ctx); // Initialize dropdown with the default font in case system fonts failed to load. dropdown.add_items(vec![Self::default_font_item(ctx, false)], ctx); dropdown.set_selected_by_index(0, ctx); dropdown }); let ui_font_family_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = FilterableDropdown::new(ctx); dropdown.set_top_bar_max_width(FONT_FAMILY_DROPDOWN_WIDTH); dropdown.set_menu_width(FONT_FAMILY_DROPDOWN_WIDTH, ctx); dropdown.add_items(vec![Self::default_ui_font_item(ctx)], ctx); dropdown.set_selected_by_index(0, ctx); dropdown }); let ai_font_family_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = FilterableDropdown::new(ctx); dropdown.set_top_bar_max_width(FONT_FAMILY_DROPDOWN_WIDTH); dropdown.set_menu_width(FONT_FAMILY_DROPDOWN_WIDTH, ctx); // Initialize dropdown with the default font in case system fonts failed to load. dropdown.add_items(vec![Self::default_font_item(ctx, true)], ctx); dropdown.set_selected_by_index(0, ctx); dropdown }); let font_weight_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); dropdown.set_top_bar_max_width(FONT_WEIGHT_DROPDOWN_WIDTH); dropdown.set_menu_width(FONT_WEIGHT_DROPDOWN_WIDTH, ctx); let selectable_weights = [Weight::Normal, Weight::Bold]; let items = selectable_weights .iter() .map(|weight| { DropdownItem::new( weight.to_string(), AppearancePageAction::SetFontWeight(*weight), ) }) .collect(); dropdown.add_items(items, ctx); dropdown.set_selected_by_name(monospace_font_weight.to_string(), ctx); dropdown }); let thin_strokes_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); let values = vec![ ThinStrokes::Never, ThinStrokes::OnLowDpiDisplays, ThinStrokes::OnHighDpiDisplays, ThinStrokes::Always, ]; let current_value = ctx.rendering_config().glyphs.use_thin_strokes; let selected_index = values .iter() .position(|val| *val == current_value) .unwrap_or_else(|| { log::error!("Could not find current ThinStrokes value in dropdown option list"); 0 }); dropdown.add_items( values .into_iter() .map(|val| { DropdownItem::new( Self::thin_strokes_dropdown_item_label(val), AppearancePageAction::SetThinStrokes(val), ) }) .collect(), ctx, ); dropdown.set_selected_by_index(selected_index, ctx); dropdown }); let input_mode_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); dropdown.set_top_bar_max_width(INPUT_MODE_DROPDOWN_WIDTH); dropdown.set_menu_width(INPUT_MODE_DROPDOWN_WIDTH, ctx); let values = vec![ InputMode::PinnedToBottom, InputMode::Waterfall, InputMode::PinnedToTop, ]; let current_value = *InputModeSettings::as_ref(ctx).input_mode.value(); let selected_index: usize = values .iter() .position(|val| *val == current_value) .unwrap_or_else(|| { log::error!("Could not find current InputMode value in dropdown option list"); 0 }); dropdown.add_items( values .into_iter() .map(|val| { DropdownItem::new( Self::input_mode_dropdown_item_label(val), AppearancePageAction::SetInputMode { new_mode: val, from_binding: false, }, ) }) .collect(), ctx, ); dropdown.set_selected_by_index(selected_index, ctx); dropdown }); let app_icon_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); dropdown.set_top_bar_max_width(INPUT_MODE_DROPDOWN_WIDTH); dropdown.set_menu_width(INPUT_MODE_DROPDOWN_WIDTH, ctx); let values: Vec = all::().collect(); let current_value = *AppIconSettings::as_ref(ctx).app_icon; let selected_index = values .iter() .position(|val| *val == current_value) .unwrap_or_else(|| { log::error!("Could not find current AppIcon value in dropdown option list"); 0 }); dropdown.add_items( values .into_iter() .map(|val| { DropdownItem::new( Self::app_icon_dropdown_item_label(val), AppearancePageAction::SetAppIcon(val), ) }) .collect(), ctx, ); dropdown.set_selected_by_index(selected_index, ctx); dropdown }); let enforce_min_contrast_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); let values = vec![ EnforceMinimumContrast::Always, EnforceMinimumContrast::OnlyNamedColors, EnforceMinimumContrast::Never, ]; let current_value = *FontSettings::as_ref(ctx) .enforce_minimum_contrast; let selected_index = values.iter().position(|val| *val == current_value).unwrap_or_else(|| { log::error!("Could not find current EnforceMinimumContrast value in dropdown option list"); 0 }); dropdown.add_items( values.into_iter().map(|val| { DropdownItem::new( Self::enforce_minimum_contrast_dropdown_item_label(val), AppearancePageAction::SetEnforceMinimumContrast(val), ) }).collect(), ctx, ); dropdown.set_selected_by_index(selected_index, ctx); dropdown }); let context_chips = Self::get_context_chip_renderers(ctx); let alt_screen_padding_editor = { let width_and_height_editor_options = SingleLineEditorOptions { text: TextOptions::ui_font_size(appearance_handle.as_ref(ctx)), ..Default::default() }; ctx.add_typed_action_view(|ctx| { let mut editor = EditorView::single_line(width_and_height_editor_options.clone(), ctx); if let AltScreenPaddingMode::Custom { uniform_padding } = *TerminalSettings::as_ref(ctx).alt_screen_padding { let val = format!("{:.1}", uniform_padding.as_f32()); // Do a system edit to avoid counting this update as part of telemetry. editor.system_reset_buffer_text(val.trim_end_matches(".0"), ctx); } editor }) }; ctx.subscribe_to_view(&alt_screen_padding_editor, |me, _, event, ctx| { me.handle_alt_screen_padding_editor_event(event, ctx); }); // Initialize the input type radio state let input_type = InputSettings::as_ref(ctx).input_type(ctx); let input_type_radio_state = RadioButtonStateHandle::default(); input_type_radio_state.set_selected_idx(input_type as usize); let header_toolbar_inline_editor = ctx.add_typed_action_view(HeaderToolbarInlineEditor::new); AppearanceSettingsPageView { page: Self::build_page(ctx), window_id: ctx.window_id(), local_only_icon_tooltip_states: Default::default(), ai_font_family_dropdown, ui_font_family_dropdown, notebook_font_size_editor, font_size_editor, line_height_editor, new_window_columns_editor, valid_new_window_columns: true, new_window_rows_editor, valid_new_window_rows: true, opacity_state: Default::default(), blur_state: Default::default(), font_family_dropdown, font_weight_dropdown, thin_strokes_dropdown, input_mode_dropdown, input_type_radio_state, app_icon_dropdown, enforce_min_contrast_dropdown, workspace_decorations_dropdown: Self::build_workspace_decoration_visibility_dropdown( ctx, ), tab_close_button_position_dropdown: Self::build_tab_close_button_position_dropdown(ctx), zoom_level_dropdown: Self::build_zoom_level_dropdown(ctx), zoom_reset_button_mouse_state: MouseStateHandle::default(), available_families: Default::default(), view_font_type: Default::default(), color_picker_dot_states: (0..directory_tab_colors(ctx).len()) .map(|_| { (0..TAB_COLOR_OPTIONS.len() + 1) .map(|_| MouseStateHandle::default()) .collect() }) .collect(), directory_tab_color_delete_buttons: build_directory_delete_buttons(ctx), header_toolbar_inline_editor, alt_screen_padding_editor, context_chips, ps1_grid_info: None, } } fn build_page(ctx: &mut ViewContext) -> PageType { let mut categories = vec![Category::new( "Themes", vec![ Box::new(CreateCustomThemeWidget::default()), Box::new(ThemeSelectWidget::default()), Box::new(BrandPresetWidget::default()), ], )]; if AppIconSettings::as_ref(ctx).is_supported_on_current_platform() { categories.push(Category::new( "Icon", vec![Box::new(CustomAppIconWidget::default())], )); } let window_settings = WindowSettings::as_ref(ctx); let mut window_settings_widgets: Vec>> = vec![]; if window_settings .open_windows_at_custom_size .is_supported_on_current_platform() { window_settings_widgets.push(Box::new(CustomWindowSizeWidget::default())); } if window_settings .background_opacity .is_supported_on_current_platform() { window_settings_widgets.push(Box::new(WindowOpacityWidget::default())); } if window_settings .background_blur_radius .is_supported_on_current_platform() { window_settings_widgets.push(Box::new(WindowBlurWidget::default())); } if window_settings .background_blur_texture .is_supported_on_current_platform() { window_settings_widgets.push(Box::new(WindowBlurTextureWidget::default())); } if FeatureFlag::UIZoom.is_enabled() { window_settings_widgets.push(Box::new(ZoomLevelWidget)); } if window_settings .left_panel_visibility_across_tabs .is_supported_on_current_platform() { window_settings_widgets.push(Box::new(ToolsPanelStateScopeWidget::default())); } if !window_settings_widgets.is_empty() { categories.push(Category::new("Window", window_settings_widgets)); } // Create the Input category with all widgets // The PromptWidget and InputModeWidget will handle their own visibility let category_widgets: Vec>> = vec![ Box::new(InputTypeWidget::default()), Box::new(PromptWidget::default()), Box::new(InputModeWidget::default()), ]; categories.push(Category::new("Input", category_widgets)); categories.push(Category::new( "Panes", vec![ Box::new(DimInactivePanesWidget::default()), Box::new(FocusFollowsMouseWidget::default()), ], )); let mut block_settings_widgets: Vec>> = vec![ Box::new(CompactModeWidget::default()), Box::new(JumpToBottomOfBlockWidget::default()), ]; if FeatureFlag::MinimalistUI.is_enabled() { block_settings_widgets.push(Box::new(ShowBlockDividersWidget::default())); } categories.push(Category::new("Blocks", block_settings_widgets)); let font_settings = FontSettings::as_ref(ctx); let mut text_settings_widgets: Vec>> = vec![ Box::new(TerminalFontWidget::default()), Box::new(UIFontWidget::default()), Box::new(AIFontWidget::default()), Box::new(NotebookFontSizeWidget::default()), ]; if font_settings .use_thin_strokes .is_supported_on_current_platform() { text_settings_widgets.push(Box::new(ThinStrokesWidget::default())); } if font_settings .enforce_minimum_contrast .is_supported_on_current_platform() { text_settings_widgets.push(Box::new(MinimumContrastWidget::default())); } let ligature_settings = LigatureSettings::as_ref(ctx); if FeatureFlag::Ligatures.is_enabled() && ligature_settings .ligature_rendering_enabled .is_supported_on_current_platform() { text_settings_widgets.push(Box::new(LigaturesWidget::default())); } categories.push(Category::new("Text", text_settings_widgets)); categories.push(Category::new( "Cursor", vec![ Box::new(CursorTypeWidget::default()), Box::new(BlinkingCursorWidget::default()), ], )); let tab_settings = TabSettings::as_ref(ctx); let mut tab_settings_widgets: Vec>> = vec![Box::new(TabIndicatorWidget::default())]; if !FeatureFlag::OpenWarpNewSettingsModes.is_enabled() { tab_settings_widgets.push(Box::new(CodeReviewButtonWidget::default())); } if FeatureFlag::FullScreenZenMode.is_enabled() && tab_settings .workspace_decoration_visibility .is_supported_on_current_platform() { tab_settings_widgets.push(Box::new(ZenModeWidget::default())); } if FeatureFlag::TabCloseButtonOnLeft.is_enabled() { tab_settings_widgets.push(Box::new(TabCloseButtonPositionWidget::default())); } tab_settings_widgets.push(Box::new(PreserveActiveTabColorWidget::default())); if FeatureFlag::VerticalTabs.is_enabled() { tab_settings_widgets.push(Box::new(VerticalTabsWidget::default())); tab_settings_widgets.push(Box::new( ShowVerticalTabPanelInRestoredWindowsWidget::default(), )); tab_settings_widgets.push(Box::new( HideTitleBarSearchBarInVerticalTabsWidget::default(), )); tab_settings_widgets.push(Box::new( UseLatestUserPromptAsConversationTitleInTabNamesWidget::default(), )); if FeatureFlag::ConfigurableToolbar.is_enabled() { tab_settings_widgets.push(Box::new(EditToolbarWidget)); } } if FeatureFlag::DirectoryTabColors.is_enabled() { let add_picker = ctx.add_typed_action_view(DirectoryColorAddPicker::new); ctx.subscribe_to_view(&add_picker, |me, _, event, ctx| { me.handle_directory_color_add_picker_event(event, ctx); }); tab_settings_widgets.push(Box::new(DirectoryTabColorsWidget { add_picker })); } categories.push(Category::new("Tabs", tab_settings_widgets)); categories.push(Category::new( "Full-screen Apps", vec![Box::new(AltScreenPaddingWidget::default())], )); PageType::new_categorized(categories, None) } fn set_alt_screen_padding_editor_text(&mut self, ctx: &mut ViewContext) { if let AltScreenPaddingMode::Custom { uniform_padding } = *TerminalSettings::as_ref(ctx).alt_screen_padding { self.alt_screen_padding_editor.update(ctx, |editor, ctx| { let val = format!("{:.1}", uniform_padding.as_f32()); editor.set_buffer_text(val.trim_end_matches(".0"), ctx); }); ctx.notify(); } } fn handle_appearance_update( &mut self, handle: ModelHandle, event: &AppearanceEvent, ctx: &mut ViewContext, ) { match event { AppearanceEvent::MonospaceFontFamilyChanged { .. } => { self.update_font_dropdown(ctx); } AppearanceEvent::MonospaceFontSizeChanged { .. } => { let font_size = handle.as_ref(ctx).monospace_font_size(); self.font_size_editor.update(ctx, move |editor, ctx| { editor.set_buffer_text(&format!("{font_size}"), ctx); }); } AppearanceEvent::MonospaceFontWeightChanged { .. } => { let font_weight = handle.as_ref(ctx).monospace_font_weight(); self.font_weight_dropdown.update(ctx, |dropdown, ctx| { dropdown.set_selected_by_name(font_weight.to_string(), ctx); }); } AppearanceEvent::LineHeightRatioChanged { .. } => { let line_height_ratio = handle.as_ref(ctx).line_height_ratio(); self.line_height_editor.update(ctx, move |editor, ctx| { editor.set_buffer_text(&format!("{line_height_ratio}"), ctx); }); } AppearanceEvent::ThemeChanged => { // Context-chip colors are theme-derived, so rebuild the Input // preview chips when the theme changes to keep them in sync. self.context_chips = Self::get_context_chip_renderers(ctx); } _ => {} } ctx.notify(); } fn get_context_chip_renderers(app: &AppContext) -> Vec { let appearance = Appearance::as_ref(app); let prompt = Prompt::as_ref(app); prompt .chip_kinds() .into_iter() .filter_map(|kind| { ContextChipRenderer::default_from_kind(kind, ChipAvailability::Enabled, appearance) }) .collect() } fn handle_prompt_update( &mut self, _prompt: ModelHandle, _event: &PromptEvent, ctx: &mut ViewContext, ) { self.context_chips = Self::get_context_chip_renderers(ctx); } fn default_font_item( ctx: &mut ViewContext, is_ai_font: bool, ) -> DropdownItem where V: View, { let font_name = if is_ai_font { AIFontName::default_value() } else { MonospaceFontName::default_value() }; let mut initial_dropdown_item = DropdownItem::new( default_font_label(is_ai_font), if is_ai_font { AppearancePageAction::SetAIFontFamily(font_name.clone()) } else { AppearancePageAction::SetFontFamily(font_name.clone()) }, ); // If we're on a non-Linux platform, render the dropdown item in the // actual font. We currently don't do this on Linux because // pre-loading all of the fonts is too expensive. if cfg!(not(any(target_os = "linux", target_os = "freebsd"))) { if let Some(family_id) = ctx.font_cache().family_id_for_name(&font_name) { initial_dropdown_item = initial_dropdown_item.with_font_override(family_id); } } initial_dropdown_item } fn default_ui_font_item(_: &mut ViewContext) -> DropdownItem where V: View, { DropdownItem::new( default_ui_font_label(), AppearancePageAction::SetUIFontFamily(DEFAULT_UI_FONT_NAME.to_string()), ) } fn input_mode_dropdown_item_label(val: InputMode) -> &'static str { match val { InputMode::PinnedToBottom => "Pin to the bottom (Warp mode)", InputMode::PinnedToTop => "Pin to the top (Reverse mode)", InputMode::Waterfall => "Start at the top (Classic mode)", } } fn app_icon_dropdown_item_label(val: AppIcon) -> &'static str { match val { AppIcon::Galaxy => "Galaxy", AppIcon::DotMatrix => "Dot Matrix", AppIcon::Explorer => "Explorer", AppIcon::Rainbow => "Rainbow", AppIcon::Wormhole => "Wormhole", } } fn thin_strokes_dropdown_item_label(val: ThinStrokes) -> &'static str { match val { ThinStrokes::Never => "Never", ThinStrokes::OnLowDpiDisplays => "On low-DPI displays", ThinStrokes::OnHighDpiDisplays => "On high-DPI displays", ThinStrokes::Always => "Always", } } fn enforce_minimum_contrast_dropdown_item_label(val: EnforceMinimumContrast) -> &'static str { match val { EnforceMinimumContrast::Always => "Always", EnforceMinimumContrast::OnlyNamedColors => "Only for named colors", EnforceMinimumContrast::Never => "Never", } } fn workspace_decoration_visibility_dropdown_item_label( value: WorkspaceDecorationVisibility, ) -> &'static str { match value { WorkspaceDecorationVisibility::AlwaysShow => "Always", WorkspaceDecorationVisibility::HideFullscreen => "When windowed", WorkspaceDecorationVisibility::OnHover => "Only on hover", } } fn tab_close_button_position_dropdown_item_label( value: TabCloseButtonPosition, ) -> &'static str { match value { TabCloseButtonPosition::Right => "Right", TabCloseButtonPosition::Left => "Left", } } fn handle_alt_screen_padding_editor_event( &mut self, event: &EditorEvent, ctx: &mut ViewContext, ) { match event { EditorEvent::Edited(EditOrigin::UserTyped | EditOrigin::UserInitiated) => { let buffer_text = self.alt_screen_padding_editor.as_ref(ctx).buffer_text(ctx); if let Ok(padding) = buffer_text.parse::() { if padding >= 0. { TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| { let new_mode = AltScreenPaddingMode::Custom { uniform_padding: padding.into_pixels(), }; report_if_error!(terminal_settings .alt_screen_padding .set_value(new_mode, ctx)); send_telemetry_from_ctx!( TelemetryEvent::UpdateAltScreenPaddingMode { new_mode }, ctx ); }); } } ctx.notify(); } EditorEvent::Escape | EditorEvent::Blurred | EditorEvent::Enter => { self.set_alt_screen_padding_editor_text(ctx); ctx.emit(SettingsPageEvent::FocusModal) } _ => {} } } pub fn handle_font_size_editor_event( &mut self, event: &EditorEvent, ctx: &mut ViewContext, ) { match event { EditorEvent::Blurred | EditorEvent::Enter => self.set_font_size(ctx), EditorEvent::Escape => ctx.emit(SettingsPageEvent::FocusModal), _ => {} } } pub fn handle_notebook_font_size_editor_event( &mut self, event: &EditorEvent, ctx: &mut ViewContext, ) { match event { EditorEvent::Blurred | EditorEvent::Enter => self.set_notebook_font_size(ctx), EditorEvent::Escape => ctx.emit(SettingsPageEvent::FocusModal), _ => {} } } pub fn handle_line_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext) { match event { EditorEvent::Blurred | EditorEvent::Enter => self.set_line_height_ratio(ctx), EditorEvent::Escape => ctx.emit(SettingsPageEvent::FocusModal), _ => {} } } /// Updates the prompt that is shown when PS1 is selected, in the 'Prompt' section. pub(super) fn set_ps1_info( &mut self, ps1_grid_info: Option<(BlockGrid, SizeInfo)>, ctx: &mut ViewContext, ) { self.ps1_grid_info = ps1_grid_info; ctx.notify(); } pub(super) fn get_ps1_info(&self) -> Option<&(BlockGrid, SizeInfo)> { self.ps1_grid_info.as_ref() } fn update_new_windows_num_columns(&mut self, blurred: bool, ctx: &mut ViewContext) { let user_input = self.new_window_columns_editor.as_ref(ctx).buffer_text(ctx); if let Some(columns) = Self::parse_new_window_size(user_input) { self.valid_new_window_columns = true; if blurred { self.set_new_windows_num_columns(columns, ctx); } } else { self.valid_new_window_columns = false; if blurred { let window_settings: &WindowSettings = WindowSettings::as_ref(ctx); // Revert to the saved value self.set_new_windows_num_columns( *window_settings.new_windows_num_columns.value(), ctx, ); } } if blurred { ctx.focus_self(); } ctx.notify(); } fn update_new_windows_num_rows(&mut self, blurred: bool, ctx: &mut ViewContext) { let user_input = self.new_window_rows_editor.as_ref(ctx).buffer_text(ctx); if let Some(rows) = Self::parse_new_window_size(user_input) { self.valid_new_window_rows = true; if blurred { self.set_new_windows_num_rows(rows, ctx); } } else { self.valid_new_window_rows = false; if blurred { let window_settings: &WindowSettings = WindowSettings::as_ref(ctx); // Revert to the saved value self.set_new_windows_num_columns( *window_settings.new_windows_num_columns.value(), ctx, ); } } if blurred { ctx.focus_self(); } ctx.notify(); } fn parse_new_window_size(user_input: String) -> Option { user_input.parse::().ok().filter(|parsed| { (MIN_NEW_WINDOW_ROWS_OR_COLS..=MAX_NEW_WINDOW_ROWS_OR_COLS).contains(parsed) }) } fn set_font_size(&mut self, ctx: &mut ViewContext) { let user_input = self.font_size_editor.as_ref(ctx).buffer_text(ctx); if let Ok(num) = user_input.parse::() { if (MIN_FONT_SIZE..=MAX_FONT_SIZE).contains(&num) { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .monospace_font_size .set_value(num as f32, ctx,)); }); } } } pub fn set_font_weight(&mut self, value: Weight, ctx: &mut ViewContext) { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings.monospace_font_weight.set_value(value, ctx)) }); } fn set_notebook_font_size(&mut self, ctx: &mut ViewContext) { let user_input = self.notebook_font_size_editor.as_ref(ctx).buffer_text(ctx); if let Ok(num) = user_input.parse::() { if (MIN_FONT_SIZE..=MAX_FONT_SIZE).contains(&num) { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings.notebook_font_size.set_value(num as f32, ctx,)); }); } } } fn set_opacity( &mut self, opacity_value: f32, should_set_defaults: bool, ctx: &mut ViewContext, ) { if should_set_defaults { send_telemetry_from_ctx!( TelemetryEvent::SetOpacity { opacity: opacity_value as u8 }, ctx ); } WindowSettings::handle(ctx).update(ctx, |window_settings, ctx| { report_if_error!(window_settings .background_opacity .set_value(opacity_value as u8, ctx)); }); ctx.notify(); } fn set_blur( &mut self, blur_value: f32, should_set_defaults: bool, ctx: &mut ViewContext, ) { if should_set_defaults { send_telemetry_from_ctx!( TelemetryEvent::SetBlurRadius { blur_radius: blur_value as u8 }, ctx ); } ctx.windows() .set_all_windows_background_blur_radius(blur_value as u8); WindowSettings::handle(ctx).update(ctx, |window_settings, ctx| { report_if_error!(window_settings .background_blur_radius .set_value(blur_value as u8, ctx)); }); ctx.notify() } fn reset_line_height_ratio(&mut self, ctx: &mut ViewContext) { self.line_height_editor.update(ctx, |editor, ctx| { editor.set_buffer_text(&format!("{DEFAULT_UI_LINE_HEIGHT_RATIO}"), ctx); }); send_telemetry_from_ctx!( TelemetryEvent::SetLineHeight { new_value: DEFAULT_UI_LINE_HEIGHT_RATIO }, ctx ); FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .line_height_ratio .set_value(DEFAULT_UI_LINE_HEIGHT_RATIO, ctx)); }); } fn set_line_height_ratio(&mut self, ctx: &mut ViewContext) { let user_input = self.line_height_editor.as_ref(ctx).buffer_text(ctx); let Ok(new_line_height) = user_input.parse::() else { return; }; let appearance = Appearance::as_ref(ctx); let current_line_height = appearance.ui_builder().line_height_ratio(); if (current_line_height - new_line_height).abs() > f32::EPSILON { send_telemetry_from_ctx!( TelemetryEvent::SetLineHeight { new_value: new_line_height }, ctx ); if (MIN_LINE_SPACING..=MAX_LINE_SPACING).contains(&new_line_height) { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .line_height_ratio .set_value(new_line_height, ctx)); }); } } } pub fn toggle_open_windows_at_custom_size(&mut self, ctx: &mut ViewContext) { WindowSettings::handle(ctx).update(ctx, |window_settings, ctx| { let current_val = window_settings.open_windows_at_custom_size.value(); let new_val: bool = !current_val; send_telemetry_from_ctx!( TelemetryEvent::ToggleNewWindowsAtCustomSize { enabled: new_val }, ctx ); report_if_error!(window_settings .open_windows_at_custom_size .set_value(new_val, ctx)); }); ctx.notify(); } fn set_new_windows_num_columns(&mut self, columns: u16, ctx: &mut ViewContext) { WindowSettings::handle(ctx).update(ctx, |window_settings, ctx| { send_telemetry_from_ctx!(TelemetryEvent::SetNewWindowsAtCustomSize, ctx); report_if_error!(window_settings .new_windows_num_columns .set_value(columns, ctx)); }); } fn set_new_windows_num_rows(&mut self, rows: u16, ctx: &mut ViewContext) { WindowSettings::handle(ctx).update(ctx, |window_settings, ctx| { send_telemetry_from_ctx!(TelemetryEvent::SetNewWindowsAtCustomSize, ctx); report_if_error!(window_settings.new_windows_num_rows.set_value(rows, ctx)); }); } fn update_font_dropdown(&mut self, ctx: &mut ViewContext) { let monospace_font_family = Appearance::as_ref(ctx).monospace_font_family(); let ai_font_family = Appearance::as_ref(ctx).ai_font_family(); self.font_family_dropdown.update(ctx, |dropdown, ctx| { // Get the family name of the current monospace font. // We check the font_cache for the current monospace family. // We also make sure that // - If the current monospace family is in our available_families map, // we update its entry to ensure it has the correct family_id // - Otherwise, we add a new entry for the current monospace family let font_name = ctx .font_cache() .load_family_name_from_id(monospace_font_family); if let Some(font_name) = &font_name { self.available_families .entry(font_name.clone()) .and_modify(|entry| entry.0 = Some(monospace_font_family)) .or_insert((Some(monospace_font_family), FontType::Monospace)); } let font_name = font_name.unwrap_or_default(); let mut items = self .available_families .iter() .filter_map(|(name, (family, font_type))| { // Only add the item if it's not a default font and then check whether we want // to see it depending on it being monospace and user's preference. We also // want to include the currently selected font, even if it's non-monospace and // user hasn't chosen to view all system fonts. let include_in_dropdown = name != &MonospaceFontName::default_value() && (matches!(self.view_font_type, FontType::Any) || matches!(font_type, FontType::Monospace) || *name == font_name); if include_in_dropdown { let name_move = name.clone(); let mut dropdown = DropdownItem::new(name, AppearancePageAction::SetFontFamily(name_move)); // If we're on a non-Linux platform, render the dropdown item in the // actual font. We currently don't do this on Linux because // pre-loading all of the fonts is too expensive. if cfg!(not(any(target_os = "linux", target_os = "freebsd"))) { if let Some(family_id) = family { dropdown = dropdown.with_font_override(*family_id) } } Some(dropdown) } else { None } }) .collect::>(); // Sort the font names by alphabetical order. items.sort_by(|a, b| a.display_text.cmp(&b.display_text)); // Prepend the default item items.insert(0, Self::default_font_item(ctx, false)); dropdown.set_items(items, ctx); if !font_name.is_empty() { let label = if font_name == MonospaceFontName::default_value() { &default_font_label(false) } else { &font_name }; dropdown.set_selected_by_name(label, ctx); } }); self.ai_font_family_dropdown.update(ctx, |dropdown, ctx| { // Get the family name of the current agent mode font. // We check the font_cache for the current agent mode family. // We also make sure that // - If the current family is in our available_families map, // we update its entry to ensure it has the correct family_id // - Otherwise, we add a new entry for the current agent mode family let font_name = ctx.font_cache().load_family_name_from_id(ai_font_family); if let Some(font_name) = &font_name { self.available_families .entry(font_name.clone()) .and_modify(|entry| entry.0 = Some(ai_font_family)) .or_insert((Some(ai_font_family), FontType::Any)); } let font_name = font_name.unwrap_or_default(); let mut items = self .available_families .iter() .filter_map(|(name, (family, _font_type))| { if name == &AIFontName::default_value() { return None; } let name_move = name.clone(); let mut dropdown = DropdownItem::new(name, AppearancePageAction::SetAIFontFamily(name_move)); // If we're on a non-Linux platform, render the dropdown item in the // actual font. We currently don't do this on Linux because // pre-loading all of the fonts is too expensive. if cfg!(not(any(target_os = "linux", target_os = "freebsd"))) { if let Some(family_id) = family { dropdown = dropdown.with_font_override(*family_id) } } Some(dropdown) }) .collect::>(); // Sort the font names by alphabetical order. items.sort_by(|a, b| a.display_text.cmp(&b.display_text)); // Prepend the default item items.insert(0, Self::default_font_item(ctx, true)); dropdown.set_items(items, ctx); if !font_name.is_empty() { let label = if font_name == AIFontName::default_value() { &default_font_label(true) } else { &font_name }; dropdown.set_selected_by_name(label, ctx); } }); self.ui_font_family_dropdown.update(ctx, |dropdown, ctx| { let selected_ui_font = FontSettings::as_ref(ctx).ui_font_name.value().clone(); let mut items = self .available_families .iter() .map(|(name, (family, _font_type))| { let mut item = DropdownItem::new( name, AppearancePageAction::SetUIFontFamily(name.clone()), ); if cfg!(not(target_os = "linux")) { if let Some(family_id) = family { item = item.with_font_override(*family_id); } } item }) .collect::>(); items.sort_by(|a, b| a.display_text.cmp(&b.display_text)); items.insert(0, Self::default_ui_font_item(ctx)); dropdown.set_items(items, ctx); if selected_ui_font.is_empty() { dropdown.set_selected_by_name(default_ui_font_label(), ctx); } else { dropdown.set_selected_by_name(selected_ui_font, ctx); } }); ctx.notify(); } #[cfg_attr(target_family = "wasm", allow(dead_code))] pub fn set_system_fonts( &mut self, available_families: Vec<(Option, FontInfo)>, ctx: &mut ViewContext, ) { self.available_families = available_families .into_iter() .map(|(family_id, font_info)| { let font_type = if font_info.is_monospace { FontType::Monospace } else { FontType::Any }; (font_info.family_name, (family_id, font_type)) }) .collect(); // Add Hack font to the available monospace families so user could switch back to it. if let Some(family_id) = ctx .font_cache() .family_id_for_name(DEFAULT_MONOSPACE_FONT_NAME) { self.available_families.insert( String::from(DEFAULT_MONOSPACE_FONT_NAME), (Some(family_id), FontType::Monospace), ); } // Roboto is bundled with Galaxy and is the UI face used by the Galaxy preset. if let Some(family_id) = ctx.font_cache().family_id_for_name(GALAXY_UI_FONT_NAME) { self.available_families.insert( String::from(GALAXY_UI_FONT_NAME), (Some(family_id), FontType::Any), ); } self.update_font_dropdown(ctx); } pub fn set_font_family(&mut self, name: &str, ctx: &mut ViewContext) { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .monospace_font_name .set_value(name.to_string(), ctx)); if *font_settings.match_ai_font_to_terminal_font.value() { report_if_error!(font_settings.ai_font_name.set_value(name.to_string(), ctx)) } }); } pub fn toggle_match_ai_font_to_terminal_font(&mut self, ctx: &mut ViewContext) { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .match_ai_font_to_terminal_font .toggle_and_save_value(ctx)); if *font_settings.match_ai_font_to_terminal_font.value() { let font_name = font_settings.monospace_font_name.value().clone(); self.ai_font_family_dropdown.update(ctx, |dropdown, ctx| { dropdown.clear_filter(ctx); }); report_if_error!(font_settings.ai_font_name.set_value(font_name, ctx)) } }); } pub fn set_ai_font_family(&mut self, name: &str, ctx: &mut ViewContext) { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings.ai_font_name.set_value(name.to_string(), ctx)) }); } pub fn set_ui_font_family(&mut self, name: &str, ctx: &mut ViewContext) { let ui_font_name = if name == default_ui_font_label() { DEFAULT_UI_FONT_NAME.to_string() } else { name.to_string() }; FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .ui_font_name .set_value(ui_font_name.clone(), ctx)); }); } pub fn apply_galaxy_brand_preset(&mut self, ctx: &mut ViewContext) { ThemeSettings::handle(ctx).update(ctx, |theme_settings, ctx| { report_if_error!(theme_settings .theme_kind .set_value(GALAXY_BRAND_THEME_DARK, ctx)); report_if_error!(theme_settings.selected_system_themes.set_value( SelectedSystemThemes { light: GALAXY_BRAND_THEME_DAY, dark: GALAXY_BRAND_THEME_DARK, }, ctx, )); report_if_error!(theme_settings.use_system_theme.set_value(true, ctx)); }); FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { report_if_error!(font_settings .monospace_font_name .set_value(DEFAULT_MONOSPACE_FONT_NAME.to_string(), ctx)); report_if_error!(font_settings .ai_font_name .set_value(DEFAULT_MONOSPACE_FONT_NAME.to_string(), ctx)); report_if_error!(font_settings .match_ai_font_to_terminal_font .set_value(true, ctx)); report_if_error!(font_settings .ui_font_name .set_value(GALAXY_UI_FONT_NAME.to_string(), ctx)); }); self.update_font_dropdown(ctx); ctx.notify(); } fn set_thin_strokes(&mut self, value: &ThinStrokes, ctx: &mut ViewContext) { FontSettings::handle(ctx).update(ctx, |font_settings, ctx| { match font_settings.use_thin_strokes.set_value(*value, ctx) { Ok(_) => { send_telemetry_from_ctx!( TelemetryEvent::ThinStrokesSettingChanged { new_value: *value }, ctx ); } Err(e) => { report_error!(e); } } }); } pub fn toggle_jump_to_bottom_of_block_button(&mut self, ctx: &mut ViewContext) { let block_list_settings = BlockListSettings::handle(ctx); let new_value = { !*block_list_settings .as_ref(ctx) .show_jump_to_bottom_of_block_button .value() }; send_telemetry_from_ctx!( TelemetryEvent::ToggleJumpToBottomofBlockButton { enabled: new_value }, ctx ); ctx.update_model(&block_list_settings, move |block_list_settings, ctx| { report_if_error!(block_list_settings .show_jump_to_bottom_of_block_button .set_value(new_value, ctx)); }); } pub fn toggle_show_block_dividers(&mut self, ctx: &mut ViewContext) { let block_list_settings = BlockListSettings::handle(ctx); let new_value = { !*block_list_settings.as_ref(ctx).show_block_dividers.value() }; send_telemetry_from_ctx!( TelemetryEvent::ToggleShowBlockDividers { enabled: new_value }, ctx ); ctx.update_model(&block_list_settings, move |block_list_settings, ctx| { report_if_error!(block_list_settings .show_block_dividers .set_value(new_value, ctx)); }); } pub fn toggle_compact_mode(&mut self, ctx: &mut ViewContext) { TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| { let current_value = *terminal_settings.spacing_mode.value(); report_if_error!(terminal_settings .spacing_mode .set_value(current_value.other_mode(), ctx)); }); } pub fn toggle_cursor_blink(&mut self, ctx: &mut ViewContext) { AppEditorSettings::handle(ctx).update(ctx, |me, ctx| { me.toggle_cursor_blink(ctx); }) } pub fn toggle_respect_system_theme(&mut self, ctx: &mut ViewContext) { ThemeSettings::handle(ctx).update(ctx, |theme_settings, ctx| { report_if_error!(theme_settings.use_system_theme.toggle_and_save_value(ctx)); }); ctx.notify(); } pub fn toggle_dim_inactive_panes(&mut self, ctx: &mut ViewContext) { PaneSettings::handle(ctx).update(ctx, |pane_settings, ctx| { match pane_settings .should_dim_inactive_panes .toggle_and_save_value(ctx) { Ok(new_value) => { send_telemetry_from_ctx!( TelemetryEvent::ToggleDimInactivePanes { enabled: new_value }, ctx ); } Err(e) => { report_error!(e); } } }); } pub fn toggle_blur_texture(&mut self, ctx: &mut ViewContext) { let blur_enabled = WindowSettings::handle(ctx).read(ctx, |window_settings, _ctx| { *window_settings.background_blur_texture.value() }); ctx.windows() .set_all_windows_background_blur_texture(!blur_enabled); WindowSettings::handle(ctx).update(ctx, |window_settings, ctx| { report_if_error!(window_settings .background_blur_texture .toggle_and_save_value(ctx)); }); ctx.notify(); } pub fn toggle_left_panel_visibility(&mut self, ctx: &mut ViewContext) { WindowSettings::handle(ctx).update(ctx, |window_settings, ctx| { report_if_error!(window_settings .left_panel_visibility_across_tabs .toggle_and_save_value(ctx)); }); ctx.notify(); } pub fn set_input_mode( &mut self, new_mode: InputMode, from_binding: bool, ctx: &mut ViewContext, ) { let old_mode = *InputModeSettings::as_ref(ctx).input_mode.value(); send_telemetry_from_ctx!(TelemetryEvent::InputModeChanged { old_mode, new_mode }, ctx); InputModeSettings::handle(ctx).update(ctx, |input_mode, ctx| { report_if_error!(input_mode.input_mode.set_value(new_mode, ctx)); }); let item_name = Self::input_mode_dropdown_item_label(new_mode); if from_binding { // If this update is from a command palette action, we need to update the dropdown // If not, we can't update it because there is a circular view reference, but the dropdown // will update it itself. Not great state management - I think ideally the dropdowns would have // a model they are listening to. self.input_mode_dropdown.update(ctx, |input_dropdown, ctx| { input_dropdown.set_selected_by_name(item_name, ctx); ctx.notify(); }); } } fn set_input_type(&mut self, new_type: InputBoxType, ctx: &mut ViewContext) { let old_type = InputSettings::as_ref(ctx).input_type(ctx); if old_type != new_type { InputSettings::handle(ctx).update(ctx, |input_type_settings, ctx| { report_if_error!(input_type_settings.input_box_type.set_value(new_type, ctx)); }); self.input_type_radio_state .set_selected_idx(new_type as usize); let is_udi_enabled = new_type == InputBoxType::Universal; send_telemetry_from_ctx!( TelemetryEvent::InputUXModeChanged { is_udi_enabled, origin: InputUXChangeOrigin::Settings }, ctx ); // Selecting classic mode must also enable honor_ps1 so the mode takes // effect immediately (input_type() requires honor_ps1 to return classic). SessionSettings::handle(ctx).update(ctx, |session_settings, ctx| { report_if_error!(session_settings .honor_ps1 .set_value(new_type == InputBoxType::Classic, ctx)); }); ctx.notify(); } } fn set_app_icon(&mut self, new_icon: AppIcon, ctx: &mut ViewContext) { AppIconSettings::handle(ctx).update(ctx, |app_icon_settings, ctx| { report_if_error!(app_icon_settings.app_icon.set_value(new_icon, ctx)); send_telemetry_from_ctx!( TelemetryEvent::AppIconSelection { icon: new_icon.to_string(), }, ctx ); }); } fn toggle_show_dock_icon(&mut self, ctx: &mut ViewContext) { AppIconSettings::handle(ctx).update(ctx, |app_icon_settings, ctx| { report_if_error!(app_icon_settings.show_dock_icon.toggle_and_save_value(ctx)); }); } fn set_cursor_type(&mut self, new_cursor_type: CursorDisplayType, ctx: &mut ViewContext) { AppEditorSettings::handle(ctx).update(ctx, |app_editor_settings, ctx| { report_if_error!(app_editor_settings .cursor_display_type .set_value(new_cursor_type, ctx)); send_telemetry_from_ctx!( TelemetryEvent::CursorDisplayType { cursor: new_cursor_type.to_string(), }, ctx ); }); } fn toggle_all_available_fonts(&mut self, ctx: &mut ViewContext) { self.view_font_type = self.view_font_type.toggle(); self.update_font_dropdown(ctx); } fn toggle_tab_indicators(&mut self, ctx: &mut ViewContext) { let tab_settings = TabSettings::handle(ctx); let new_value = { !*tab_settings.as_ref(ctx).show_indicators.value() }; ctx.update_model(&tab_settings, move |tab_settings, ctx| { report_if_error!(tab_settings.show_indicators.set_value(new_value, ctx)); }); send_telemetry_from_ctx!( TelemetryEvent::ToggleTabIndicators { enabled: new_value }, ctx ); } fn toggle_show_code_review_button(&mut self, ctx: &mut ViewContext) { let tab_settings = TabSettings::handle(ctx); let new_value = !*tab_settings.as_ref(ctx).show_code_review_button.value(); ctx.update_model(&tab_settings, move |tab_settings, ctx| { report_if_error!(tab_settings .show_code_review_button .set_value(new_value, ctx)); }); } fn toggle_preserve_active_tab_color(&mut self, ctx: &mut ViewContext) { let tab_settings = TabSettings::handle(ctx); let new_value = !*tab_settings.as_ref(ctx).preserve_active_tab_color.value(); ctx.update_model(&tab_settings, move |tab_settings, ctx| { report_if_error!(tab_settings .preserve_active_tab_color .set_value(new_value, ctx)); }); send_telemetry_from_ctx!( TelemetryEvent::TogglePreserveActiveTabColor { enabled: new_value }, ctx ); } fn toggle_vertical_tabs(&mut self, ctx: &mut ViewContext) { let tab_settings = TabSettings::handle(ctx); let new_value = !*tab_settings.as_ref(ctx).use_vertical_tabs.value(); ctx.update_model(&tab_settings, move |tab_settings, ctx| { report_if_error!(tab_settings.use_vertical_tabs.set_value(new_value, ctx)); }); } fn toggle_show_vertical_tab_panel_in_restored_windows(&mut self, ctx: &mut ViewContext) { TabSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings .show_vertical_tab_panel_in_restored_windows .toggle_and_save_value(ctx)); }); } fn toggle_hide_title_bar_search_bar_in_vertical_tabs(&mut self, ctx: &mut ViewContext) { TabSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings .hide_title_bar_search_bar_in_vertical_tabs .toggle_and_save_value(ctx)); }); } fn toggle_use_latest_user_prompt_as_conversation_title_in_tab_names( &mut self, ctx: &mut ViewContext, ) { TabSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings .use_latest_user_prompt_as_conversation_title_in_tab_names .toggle_and_save_value(ctx)); }); } /// Set the workspace decoration visibility to a particular value. fn set_workspace_decoration_visibility( &mut self, new_value: WorkspaceDecorationVisibility, ctx: &mut ViewContext, ) { let previous_value = TabSettings::handle(ctx).update(ctx, |tab_settings, ctx| { let prev_value = *tab_settings.workspace_decoration_visibility.value(); report_if_error!(tab_settings .workspace_decoration_visibility .set_value(new_value, ctx)); prev_value }); send_telemetry_from_ctx!( TelemetryEvent::ToggleWorkspaceDecorationVisibility { previous_value, new_value }, ctx ); } /// Toggle among the supported workspace decoration visibility values. fn toggle_workspace_decoration_visiblity(&mut self, ctx: &mut ViewContext) { let (new_value, previous_value) = TabSettings::handle(ctx).update(ctx, |tab_settings, ctx| { let previous_value = *tab_settings.workspace_decoration_visibility.value(); let new_value = previous_value.toggled(); report_if_error!(tab_settings .workspace_decoration_visibility .set_value(new_value, ctx)); (new_value, previous_value) }); send_telemetry_from_ctx!( TelemetryEvent::ToggleWorkspaceDecorationVisibility { previous_value, new_value }, ctx ); } fn build_workspace_decoration_visibility_dropdown( ctx: &mut ViewContext, ) -> ViewHandle> { ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); let values = [ WorkspaceDecorationVisibility::AlwaysShow, WorkspaceDecorationVisibility::OnHover, WorkspaceDecorationVisibility::HideFullscreen, ]; let current_value = TabSettings::as_ref(ctx).workspace_decoration_visibility; let selected_index = values.iter().position(|val| *val == current_value).unwrap_or_else(|| { log::error!("Could not find current WorkspaceDecorationVisibility value in dropdown option list"); 0 }); dropdown.set_items(values.into_iter().map(|value| { DropdownItem::new(Self::workspace_decoration_visibility_dropdown_item_label(value), AppearancePageAction::SetWorkspaceDecorationVisibility(value)) }).collect(), ctx); dropdown.set_selected_by_index(selected_index, ctx); dropdown }) } fn build_tab_close_button_position_dropdown( ctx: &mut ViewContext, ) -> ViewHandle> { ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); let values = [ TabCloseButtonPosition::Right, TabCloseButtonPosition::Left, ]; let current_value = TabSettings::as_ref(ctx).close_button_position; let selected_index = values.iter().position(|val| *val == current_value).unwrap_or_else(|| { log::error!("Could not find current TabCloseButtonPosition value in dropdown option list"); 0 }); dropdown.set_items(values.into_iter().map(|value| { DropdownItem::new(Self::tab_close_button_position_dropdown_item_label(value), AppearancePageAction::SetTabCloseButtonPosition(value)) }).collect(), ctx); dropdown.set_selected_by_index(selected_index, ctx); dropdown }) } fn build_zoom_level_dropdown( ctx: &mut ViewContext, ) -> ViewHandle> { ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); dropdown.set_items( crate::window_settings::ZoomLevel::VALUES .iter() .map(|&value| { DropdownItem::new( format!("{value}%"), AppearancePageAction::SetZoomLevel(value), ) }) .collect(), ctx, ); let current_value = *WindowSettings::as_ref(ctx).zoom_level.value(); dropdown.set_selected_by_action(AppearancePageAction::SetZoomLevel(current_value), ctx); dropdown }) } fn handle_directory_color_add_picker_event( &mut self, event: &DirectoryColorAddPickerEvent, ctx: &mut ViewContext, ) { match event { DirectoryColorAddPickerEvent::Selected(path) => { add_directory_tab_color_path(path.clone(), ctx); } DirectoryColorAddPickerEvent::RequestAddFromFilePicker => { open_directory_tab_color_folder_picker(ctx); } } } fn handle_tab_settings_event( &mut self, event: &TabSettingsChangedEvent, ctx: &mut ViewContext, ) { if let TabSettingsChangedEvent::WorkspaceDecorationVisibility { .. } = event { let value = TabSettings::as_ref(ctx).workspace_decoration_visibility; let name = Self::workspace_decoration_visibility_dropdown_item_label(value); self.workspace_decorations_dropdown .update(ctx, |dropdown, ctx| { dropdown.set_selected_by_name(name, ctx); }); } if let TabSettingsChangedEvent::DirectoryTabColors { .. } = event { let count = directory_tab_colors(ctx).len(); self.color_picker_dot_states.resize_with(count, || { (0..TAB_COLOR_OPTIONS.len() + 1) .map(|_| MouseStateHandle::default()) .collect() }); self.directory_tab_color_delete_buttons = build_directory_delete_buttons(ctx); } ctx.notify(); } fn toggle_ligature_rendering(&mut self, ctx: &mut ViewContext) { if FeatureFlag::Ligatures.is_enabled() { let ligature_settings = LigatureSettings::handle(ctx); let new_value = !*ligature_settings .as_ref(ctx) .ligature_rendering_enabled .value(); ligature_settings.update(ctx, |settings, ctx| { report_if_error!(settings .ligature_rendering_enabled .set_value(new_value, ctx)); }); send_telemetry_from_ctx!( TelemetryEvent::ToggleLigatureRendering { enabled: new_value }, ctx ); } } pub fn toggle_input_mode(&mut self, ctx: &mut ViewContext) { // Get the current input type let current_type = InputSettings::as_ref(ctx).input_type(ctx); // Toggle between Universal and Classic let new_type = match current_type { InputBoxType::Universal => InputBoxType::Classic, InputBoxType::Classic => InputBoxType::Universal, }; // Update the setting self.set_input_type(new_type, ctx); } pub fn update_tab_close_button_position( &mut self, position: TabCloseButtonPosition, ctx: &mut ViewContext, ) { TabSettings::handle(ctx).update(ctx, |tab_settings, ctx| { report_if_error!(tab_settings.close_button_position.set_value(position, ctx)); }); send_telemetry_from_ctx!( TelemetryEvent::TabCloseButtonPositionUpdated { position }, ctx ); ctx.notify(); } } fn render_group( children: impl IntoIterator>, appearance: &Appearance, ) -> Box { let bar = Container::new( ConstrainedBox::new(Empty::new().finish()) .with_width(4.) .finish(), ) .with_background(appearance.theme().outline()) .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.))) .with_margin_right(8.) .with_margin_left(8.) .finish(); Container::new( Flex::row() .with_main_axis_size(MainAxisSize::Max) .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child(bar) .with_child( Shrinkable::new(1., Flex::column().with_children(children).finish()).finish(), ) .finish(), ) .with_margin_top(-4.) .with_margin_bottom(HEADER_PADDING) .finish() } #[derive(Default)] struct CreateCustomThemeWidget { mouse_state: MouseStateHandle, } impl SettingsWidget for CreateCustomThemeWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "create theme create custom theme" } fn render( &self, _view: &Self::View, appearance: &Appearance, _app: &AppContext, ) -> Box { Align::new( appearance .ui_builder() .link( "Create your own custom theme".to_string(), Some("https://docs.warp.dev/terminal/appearance/custom-themes".to_string()), None, self.mouse_state.clone(), ) .soft_wrap(false) .build() .with_margin_bottom(10.) .finish(), ) .left() .finish() } } #[derive(Default)] struct ThemeSelectWidget { sync_os_switch_state: SwitchStateHandle, open_theme_chooser_button_mouse_state: MouseStateHandle, open_theme_chooser_button_mouse_state_light: MouseStateHandle, open_theme_chooser_button_mouse_state_dark: MouseStateHandle, } impl ThemeSelectWidget { fn render_theme_option( &self, appearance: &Appearance, theme_kind: ThemeKind, theme_chooser_mode: ThemeChooserMode, state: MouseStateHandle, is_selected: bool, app: &AppContext, ) -> Box { let theme: GalaxyTheme = GalaxyConfig::as_ref(app).theme_config().theme(&theme_kind); let mode_ui_label = match theme_chooser_mode { ThemeChooserMode::SystemLight => "Light", ThemeChooserMode::SystemDark => "Dark", ThemeChooserMode::SystemAgnostic => "Current theme", }; ConstrainedBox::new( Hoverable::new(state, |hover_state| { let (border_color, border_width) = match hover_state.is_hovered() { true => (theme.accent(), 1.0), false => (theme.accent(), 0.0), }; let mut container = Container::new( Flex::row() .with_child( appearance .ui_builder() .span(mode_ui_label.to_owned()) .with_style( UiComponentStyles::default() .set_font_weight(Weight::Bold) .set_margin(Coords { top: 20., bottom: 20., left: 10., right: 20., }), ) .build() .finish(), ) .with_child(theme::render_preview( &theme, appearance.monospace_font_family(), Some(0.6), )) .with_child( appearance .ui_builder() .span(theme_kind.to_string()) .with_style(UiComponentStyles::default().set_margin(Coords { top: 20., bottom: 20., left: 20., right: 10., })) .build() .finish(), ) .finish(), ) .with_border(Border::all(border_width).with_border_fill(border_color)) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(5.))) .with_padding_left(10. - border_width) .with_padding_top(5. - border_width) .with_padding_bottom(5. - border_width); if is_selected { container = container .with_background(internal_colors::fg_overlay_1(appearance.theme())); } container.finish() }) .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(WorkspaceAction::ShowThemeChooser(theme_chooser_mode)); }) .finish(), ) .with_min_height(70.) .with_min_width(450.) .finish() } } impl SettingsWidget for ThemeSelectWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "sync with os theme themes background backgrounds color colors customize" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let theme_settings = ThemeSettings::handle(app); let theme_picker = match respect_system_theme(theme_settings.as_ref(app)) { RespectSystemTheme::Off => self.render_theme_option( appearance, active_theme_kind(theme_settings.as_ref(app), app), ThemeChooserMode::SystemAgnostic, self.open_theme_chooser_button_mouse_state.clone(), true, // is selected app, ), RespectSystemTheme::On(SelectedSystemThemes { light, dark }) => Flex::column() .with_child(self.render_theme_option( appearance, light.clone(), ThemeChooserMode::SystemLight, self.open_theme_chooser_button_mouse_state_light.clone(), app.system_theme() == SystemTheme::Light, app, )) .with_child(self.render_theme_option( appearance, dark.clone(), ThemeChooserMode::SystemDark, self.open_theme_chooser_button_mouse_state_dark.clone(), app.system_theme() == SystemTheme::Dark, app, )) .finish(), }; Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child(render_body_item::( "Sync with OS".into(), None, LocalOnlyIconState::for_setting( UseSystemTheme::storage_key(), UseSystemTheme::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.sync_os_switch_state.clone()) .check(matches!( respect_system_theme(ThemeSettings::as_ref(app)), RespectSystemTheme::On { .. } )) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleRespectSystemTheme); }) .finish(), None, )) .with_child( appearance .ui_builder() .span( "Automatically switch between light and dark themes when your system does." .to_string(), ) .with_style( UiComponentStyles::default().set_margin(Coords::default().bottom(10.)), ) .build() .finish(), ) .with_child( Container::new(theme_picker) .with_margin_bottom(25.) .finish(), ) .finish() } } #[derive(Default)] struct CustomAppIconWidget { show_dock_icon_switch_state: SwitchStateHandle, } impl SettingsWidget for CustomAppIconWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "customize custom app icon icons dock cmd tab app switcher" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { #[allow(unused_mut)] let show_bundle_warning = { #[cfg(target_os = "macos")] { use objc2_app_kit::NSRunningApplication; NSRunningApplication::currentApplication() .bundleIdentifier() .is_none() } #[cfg(not(target_os = "macos"))] { false } }; let dropdown = render_dropdown_item( appearance, "Customize your app icon", show_bundle_warning.then_some("Changing the app icon requires the app to be bundled."), None, LocalOnlyIconState::Hidden, None, &view.app_icon_dropdown, ); let show_dock_icon_toggle = render_body_item::( "Show Warp in Dock".into(), None, LocalOnlyIconState::for_setting( ShowDockIconState::storage_key(), ShowDockIconState::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.show_dock_icon_switch_state.clone()) .check(*AppIconSettings::as_ref(app).show_dock_icon) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleShowDockIcon); }) .finish(), None, ); let show_dock_icon_is_supported = AppIconSettings::as_ref(app) .show_dock_icon .is_supported_on_current_platform(); #[cfg(target_os = "macos")] { use crate::appearance::AppearanceManager; let app_icon_at_startup = AppearanceManager::as_ref(app).app_icon_at_startup(); let current_icon = *AppIconSettings::as_ref(app).app_icon; if current_icon == AppIcon::Galaxy && ChannelState::channel() != Channel::Local && app_icon_at_startup != AppIcon::Galaxy { let theme = appearance.theme(); let column = Flex::column().with_child(dropdown).with_child( appearance .ui_builder() .wrappable_text( "You may need to restart Warp for MacOS to apply the preferred icon style.", true, ) .with_style(UiComponentStyles { font_color: Some( theme.sub_text_color(theme.background()).into_solid(), ), margin: Some(Coords::default().bottom(8.)), ..Default::default() }) .build() .finish(), ); let column = if show_dock_icon_is_supported { column.with_child(show_dock_icon_toggle) } else { column }; return column.finish(); } } let column = Flex::column().with_child(dropdown); let column = if show_dock_icon_is_supported { column.with_child(show_dock_icon_toggle) } else { column }; column.finish() } } #[derive(Default)] struct CustomWindowSizeWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for CustomWindowSizeWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "open windows with custom size" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let window_settings = WindowSettings::as_ref(app); let column_border_color: Option = (!view.valid_new_window_columns).then(|| themes::theme::Fill::error().into()); let row_border_color: Option = (!view.valid_new_window_rows).then(|| themes::theme::Fill::error().into()); let mut column = Flex::column().with_child(render_body_item::( "Open new windows with custom size".into(), None, LocalOnlyIconState::for_setting( OpenWindowsAtCustomSize::storage_key(), OpenWindowsAtCustomSize::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*window_settings.open_windows_at_custom_size.value()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleOpenWindowsAtCustomSize); }) .finish(), None, )); if *window_settings.open_windows_at_custom_size.value() { column.add_child( Container::new(render_body_item::( "Columns".into(), None, // We show the local-only icon for this with the toggle, not the individual inputs. LocalOnlyIconState::Hidden, ToggleState::Enabled, appearance, Dismiss::new( appearance .ui_builder() .text_input(view.new_window_columns_editor.clone()) .with_style(UiComponentStyles { width: Some(60.), padding: Some(Coords { top: 4., bottom: 4., left: 6., right: 6., }), background: Some(appearance.theme().surface_2().into()), border_color: column_border_color, ..Default::default() }) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action(AppearancePageAction::SetNewWindowsCustomColumns) }) .finish(), None, )) .with_margin_left(10.) .finish(), ); column.add_child( Container::new(render_body_item::( "Rows".into(), None, // We show the local-only icon for this with the toggle, not the individual inputs. LocalOnlyIconState::Hidden, ToggleState::Enabled, appearance, Dismiss::new( appearance .ui_builder() .text_input(view.new_window_rows_editor.clone()) .with_style(UiComponentStyles { width: Some(60.), padding: Some(Coords { top: 4., bottom: 4., left: 6., right: 6., }), background: Some(appearance.theme().surface_2().into()), border_color: row_border_color, ..Default::default() }) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action(AppearancePageAction::SetNewWindowsCustomRows) }) .finish(), None, )) .with_margin_left(10.) .finish(), ); } column.finish() } } #[derive(Default)] struct WindowOpacityWidget { slider_state: SliderStateHandle, } impl SettingsWidget for WindowOpacityWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "window opacity transparency" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let window_settings = WindowSettings::as_ref(app); if !window_settings .background_opacity .is_configurable(view.window_id, app) { return Flex::column() .with_child( Container::new(render_body_item_label::( "Window Opacity:".to_owned(), None, None, LocalOnlyIconState::Hidden, ToggleState::Disabled, appearance, )) .finish(), ) .with_child( Container::new( FormattedTextElement::from_str( "Transparency is not supported with your graphics drivers.", appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(appearance.theme().disabled_ui_text_color().into_solid()) .finish(), ) .with_margin_bottom(8.0) .finish(), ) .finish(); } let opacity_value = *window_settings.background_opacity; let mut col = Flex::column().with_child(render_body_item::( format!("Window Opacity: {opacity_value}"), // TODO(CORE-3384) add AdditionalInfo here. None, LocalOnlyIconState::for_setting( BackgroundOpacity::storage_key(), BackgroundOpacity::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .slider(self.slider_state.clone()) .with_range(BackgroundOpacity::MIN as f32..BackgroundOpacity::MAX as f32) .with_default_value(opacity_value as f32) .with_style(UiComponentStyles { width: Some(OPACITY_SLIDER_WIDTH), // Margin is 3. to add up with 7. padding on slider for a total of 10. margin: Some(Coords::default().top(3.).bottom(3.)), ..Default::default() }) .on_drag(|ctx, _, val| { ctx.dispatch_typed_action(AppearancePageAction::OpacitySliderDragged(val)) }) .on_change(|ctx, _, val| { ctx.dispatch_typed_action(AppearancePageAction::SetOpacity(val)) }) .build() .finish(), None, )); if let Some(window) = app.windows().platform_window(view.window_id) { // Skip showing the warning for OpenGL since WGPU often incorrectly reports it as not // supporting alpha. if !window.supports_transparency() && window.graphics_backend() != GraphicsBackend::Gl { let mut message = Cow::Borrowed( "The selected graphics settings may not support rendering transparent windows.", ); let gpu_settings = GPUSettings::as_ref(app); if (gpu_settings .prefer_low_power_gpu .is_supported_on_current_platform() && GPUState::as_ref(app).is_low_power_gpu_available()) || gpu_settings .preferred_backend .is_supported_on_current_platform() { message.to_mut().push_str( " Try changing the settings for the graphics backend or integrated GPU in \ Features > System.", ); } col.add_child( Container::new( FormattedTextElement::from_str( message, appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(appearance.theme().disabled_ui_text_color().into_solid()) .finish(), ) .with_margin_bottom(8.0) .finish(), ); } } col.finish() } } #[derive(Default)] struct WindowBlurWidget { slider_state: SliderStateHandle, info_button: MouseStateHandle, } impl SettingsWidget for WindowBlurWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "window blur radius" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let window_settings = WindowSettings::as_ref(app); let blur_value = *window_settings.background_blur_radius; let label_info = AdditionalInfo { mouse_state: self.info_button.clone(), on_click_action: Some(AppearancePageAction::OpenUrl( "https://docs.warp.dev/terminal/appearance/size-opacity-blurring".into(), )), secondary_text: None, tooltip_override_text: None, }; Flex::column() .with_child(render_body_item::( format!("Window Blur Radius: {blur_value}"), Some(label_info), LocalOnlyIconState::for_setting( BackgroundBlurRadius::storage_key(), BackgroundBlurRadius::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .slider(self.slider_state.clone()) .with_range(BackgroundBlurRadius::MIN as f32..BackgroundBlurRadius::MAX as f32) .with_default_value(blur_value as f32) .with_style(UiComponentStyles { width: Some(OPACITY_SLIDER_WIDTH), // Margin is 3. to add up with 7. padding on slider // for a total of 10. margin: Some(Coords::default().top(3.).bottom(3.)), ..Default::default() }) .on_drag(|ctx, _, val| { ctx.dispatch_typed_action(AppearancePageAction::BlurSliderDragged(val)) }) .on_change(|ctx, _, val| { ctx.dispatch_typed_action(AppearancePageAction::SetBlur(val)) }) .build() .finish(), None, )) .finish() } } #[derive(Default)] struct WindowBlurTextureWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for WindowBlurTextureWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "window blur texture acrylic" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let window_settings = WindowSettings::as_ref(app); let use_blur_texture = *window_settings.background_blur_texture; let mut col = Flex::column().with_child(render_body_item::( "Use Window Blur (Acrylic texture)".to_string(), None, LocalOnlyIconState::for_setting( BackgroundBlurTexture::storage_key(), BackgroundBlurTexture::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(use_blur_texture) .build() .on_click(|evt_ctx, _app, _v2f| { evt_ctx.dispatch_typed_action(AppearancePageAction::ToggleBlurTexture); }) .finish(), None, )); if let Some(window) = app.windows().platform_window(view.window_id) { if !window.supports_transparency() && window.graphics_backend() != GraphicsBackend::Gl { col.add_child( Container::new( FormattedTextElement::from_str( "The selected hardware may not support rendering transparent windows.", appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(appearance.theme().disabled_ui_text_color().into_solid()) .finish(), ) .with_margin_bottom(8.0) .finish(), ); } } col.finish() } } #[derive(Default)] struct ToolsPanelStateScopeWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for ToolsPanelStateScopeWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "left tools panel open closed across tabs file tree project explorer global search warp drive conversation list" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let window_settings = WindowSettings::as_ref(app); let is_enabled = *window_settings.left_panel_visibility_across_tabs; render_body_item::( "Tools panel visibility is consistent across tabs".to_string(), None, LocalOnlyIconState::for_setting( LeftPanelVisibilityAcrossTabs::storage_key(), LeftPanelVisibilityAcrossTabs::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(is_enabled) .build() .on_click(|evt_ctx, _app, _v2f| { evt_ctx.dispatch_typed_action(AppearancePageAction::ToggleLeftPanelVisibility); }) .finish(), None, ) } } struct InputTypeWidget { radio_buttons_states: Vec, } impl Default for InputTypeWidget { fn default() -> Self { Self { radio_buttons_states: vec![MouseStateHandle::default(), MouseStateHandle::default()], } } } impl SettingsWidget for InputTypeWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "input type warp universal classic style prompt terminal ai developer mode interface shell chips ps1" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let input_type = InputSettings::as_ref(app).input_type(app); let radio_buttons = appearance .ui_builder() .radio_buttons( self.radio_buttons_states.clone(), vec![ RadioButtonItem::text("Warp"), RadioButtonItem::text("Shell (PS1)"), ], view.input_type_radio_state.clone(), Some(input_type as usize), appearance.ui_font_size(), RadioButtonLayout::Row, ) .on_change(Rc::new(move |ctx, _, index| { if let Some(index) = index { let input_type = match index { 0 => InputBoxType::Universal, _ => InputBoxType::Classic, }; ctx.dispatch_typed_action(AppearancePageAction::SetInputType(input_type)); } })) .build() .finish(); render_body_item::( "Input type".into(), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, appearance, radio_buttons, None, ) } } #[derive(Default)] struct InputModeWidget {} impl SettingsWidget for InputModeWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "input mode input position pinned top bottom classic waterfall reverse" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_dropdown_item( appearance, "Input position", None, None, LocalOnlyIconState::for_setting( InputModeState::storage_key(), InputModeState::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), None, &view.input_mode_dropdown, ) } } #[derive(Default)] struct PromptWidget { button_mouse_state: MouseStateHandle, } impl SettingsWidget for PromptWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "prompt ps1 terminal warp shell custom" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let session_settings = SessionSettings::as_ref(app); let honor_ps1 = *session_settings.honor_ps1; let background = internal_colors::fg_overlay_1(appearance.theme()); let body = if honor_ps1 { // TODO: we should render something else when the grid info isn't available. if let Some((grid, size_info)) = &view.ps1_grid_info { let left_padding = size_info.padding_x_px(); let prompt_grid = BlockGridElement::new( grid, appearance, *FontSettings::as_ref(app).enforce_minimum_contrast, ObfuscateSecrets::No, *size_info, ) .finish(); Flex::row() .with_main_axis_size(MainAxisSize::Max) .with_child( Clipped::new( Container::new(prompt_grid) // Remove any left-padding built into the prompt to make sure it's // left-aligned with the title. .with_padding_left(-left_padding.as_f32()) .finish(), ) .finish(), ) .finish() } else { Empty::new().finish() } } else { Wrap::row() .with_children(view.context_chips.iter().map(|renderer| { Container::new(renderer.render_unused(ChipDragState::Undraggable, appearance)) .with_margin_right(4.) .finish() })) .with_run_spacing(4.) .finish() }; Hoverable::new(self.button_mouse_state.clone(), |hover_state| { let (border_color, border_width) = match hover_state.is_hovered() { true => (appearance.theme().accent(), 1.0), false => (appearance.theme().accent(), 0.0), }; Container::new(body) .with_background(background) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.))) .with_border(Border::all(border_width).with_border_fill(border_color)) .with_horizontal_padding(24. - border_width) .with_vertical_padding(12. - border_width) .with_margin_right(4.) .with_margin_bottom(16.) .finish() }) .with_cursor(Cursor::PointingHand) .on_click(|ctx, _, _| { ctx.dispatch_typed_action(WorkspaceAction::OpenPromptEditor { open_source: PromptEditorOpenSource::AppearancePage, }) }) .finish() } } #[derive(Default)] struct DimInactivePanesWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for DimInactivePanesWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "dim inactive panes" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_body_item::( "Dim inactive panes".into(), None, LocalOnlyIconState::for_setting( ShouldDimInactivePanes::storage_key(), ShouldDimInactivePanes::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*PaneSettings::as_ref(app).should_dim_inactive_panes) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleDimInactivePanes); }) .finish(), None, ) } } #[derive(Default)] struct FocusFollowsMouseWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for FocusFollowsMouseWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "focus follows mouse" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_body_item::( "Focus follows mouse".into(), None, LocalOnlyIconState::for_setting( FocusPaneOnHover::storage_key(), FocusPaneOnHover::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*PaneSettings::as_ref(app).focus_panes_on_hover) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleFocusPaneOnHover); }) .finish(), None, ) } } #[derive(Default)] struct CompactModeWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for CompactModeWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "compact mode spacing padding" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let is_compact_mode = matches!( TerminalSettings::as_ref(app).spacing_mode.value(), SpacingMode::Compact ); render_body_item::( "Compact mode".into(), None, LocalOnlyIconState::for_setting( Spacing::storage_key(), Spacing::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(is_compact_mode) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleCompactMode); }) .finish(), None, ) } } #[derive(Default)] struct JumpToBottomOfBlockWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for JumpToBottomOfBlockWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "jump to bottom of block button" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let block_list_settings = BlockListSettings::as_ref(app); let enabled = block_list_settings .show_jump_to_bottom_of_block_button .value(); render_body_item::( "Show Jump to Bottom of Block button".into(), None, LocalOnlyIconState::for_setting( ShowJumpToBottomOfBlockButton::storage_key(), ShowJumpToBottomOfBlockButton::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( AppearancePageAction::ToggleJumpToBottomOfBlockButton, ); }) .finish(), None, ) } } #[derive(Default)] struct ShowBlockDividersWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for ShowBlockDividersWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "show block dividers" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let block_list_settings = BlockListSettings::as_ref(app); let enabled = block_list_settings.show_block_dividers.value(); render_body_item::( "Show block dividers".into(), None, LocalOnlyIconState::for_setting( ShowBlockDividers::storage_key(), ShowBlockDividers::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleShowBlockDividers); }) .finish(), None, ) } } #[derive(Default)] struct AIFontWidget { checkbox_state: MouseStateHandle, } impl SettingsWidget for AIFontWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "text agent ai font family font size monospace" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let font_settings = FontSettings::as_ref(app); let mut ai_font_row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center); let mut ai_font = Flex::column(); ai_font.add_child(render_body_item_label::( "Agent font".to_string(), None, None, LocalOnlyIconState::for_setting( AIFontName::storage_key(), AIFontName::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, )); ai_font.add_child( Container::new(ChildView::new(&view.ai_font_family_dropdown).finish()) .with_margin_bottom(10.) .finish(), ); ai_font_row .add_child(Shrinkable::new(1., Align::new(ai_font.finish()).left().finish()).finish()); ai_font_row.add_child( appearance .ui_builder() .checkbox(self.checkbox_state.clone(), None) .check(*font_settings.match_ai_font_to_terminal_font) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( AppearancePageAction::ToggleMatchAIToTerminalFontFamily, ) }) .finish(), ); ai_font_row.add_child( appearance .ui_builder() .span("Match terminal".to_string()) .build() .with_margin_left(2.) .with_margin_right(16.) .finish(), ); ai_font_row.finish() } } #[derive(Default)] struct UIFontWidget {} impl SettingsWidget for UIFontWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "text ui font family" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { Flex::column() .with_child(render_body_item_label::( "UI font".to_string(), None, None, LocalOnlyIconState::for_setting( UIFontName::storage_key(), UIFontName::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, )) .with_child( Container::new(ChildView::new(&view.ui_font_family_dropdown).finish()) .with_margin_bottom(10.) .finish(), ) .finish() } } #[derive(Default)] struct BrandPresetWidget { button_mouse_state: MouseStateHandle, } impl SettingsWidget for BrandPresetWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "galaxy brand preset one-click" } fn render( &self, _view: &Self::View, appearance: &Appearance, _app: &AppContext, ) -> Box { let button = Hoverable::new(self.button_mouse_state.clone(), |hover_state| { let border_width = if hover_state.is_hovered() { 1.0 } else { 0.0 }; Container::new( Text::new( "Apply Galaxy preset", appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(appearance.theme().active_ui_text_color().into()) .finish(), ) .with_background(appearance.theme().surface_2()) .with_border(Border::all(border_width).with_border_fill(appearance.theme().accent())) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.))) .with_padding_top(8. - border_width) .with_padding_bottom(8. - border_width) .with_padding_left(12. - border_width) .with_padding_right(12. - border_width) .finish() }) .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ApplyGalaxyBrandPreset); }) .finish(); render_body_item::( "Brand preset".into(), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, appearance, button, Some("Apply Galaxy Dark and Galaxy Day with the bundled Roboto UI font.".to_string()), ) } } #[derive(Default)] struct TerminalFontWidget { line_height_button_state: MouseStateHandle, fonts_checkbox_state: MouseStateHandle, } impl TerminalFontWidget { fn render_line_height_editor( &self, view: &AppearanceSettingsPageView, appearance: &Appearance, row: &mut Flex, ) { let mut line_height = Flex::column(); line_height.add_child( appearance .ui_builder() .label("Line height".to_string()) .with_style(UiComponentStyles { margin: Some(Coords { left: 12., ..Default::default() }), font_size: Some(CONTENT_FONT_SIZE), ..Default::default() }) .build() .finish(), ); line_height.add_child( Container::new( Dismiss::new( appearance .ui_builder() .text_input(view.line_height_editor.clone()) .with_style(UiComponentStyles { width: Some(LINE_HEIGHT_INPUT_BOX_WIDTH), padding: Some(Coords { top: 7., bottom: 7., left: 12., right: 12., }), margin: Some(Coords { top: 2., left: 12., ..Default::default() }), background: Some(appearance.theme().surface_2().into()), ..Default::default() }) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action(AppearancePageAction::SetLineHeight) }) .finish(), ) .with_padding_top(4.) .finish(), ); line_height.add_child({ let button = appearance .ui_builder() .reset_button( ButtonVariant::Text, self.line_height_button_state.clone(), appearance.line_height_ratio() != DEFAULT_UI_LINE_HEIGHT_RATIO, appearance .theme() .disabled_text_color(appearance.theme().surface_2()) .into(), ) .with_style(UiComponentStyles { padding: Some(Coords::default().bottom(HEADER_PADDING).top(4.)), margin: Some(Coords { top: 2., left: 8., ..Default::default() }), font_size: Some(appearance.ui_font_size() * 0.8), ..Default::default() }) .with_text_label("Reset to default".to_string()); button .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::LineHeightEditorResetRatio); }) .finish() }); row.add_child(line_height.finish()); } } impl SettingsWidget for TerminalFontWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "text terminal font family font size line height monospace" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let mut terminal_font_row = Flex::row(); // Terminal Font let mut terminal_font = Flex::column(); terminal_font.add_child(render_body_item_label::( "Terminal font".to_string(), None, None, LocalOnlyIconState::for_setting( MonospaceFontName::storage_key(), MonospaceFontName::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, )); terminal_font.add_child( Container::new(ChildView::new(&view.font_family_dropdown).finish()) .with_margin_bottom(10.) .finish(), ); terminal_font.add_child( Container::new( Flex::row() .with_child( Container::new( appearance .ui_builder() .checkbox(self.fonts_checkbox_state.clone(), None) .check(view.view_font_type == FontType::Any) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( AppearancePageAction::ToggleAllAvailableFonts, ) }) .finish(), ) .with_margin_left(-7.) .finish(), ) .with_child( Shrinkable::new( 1., appearance .ui_builder() .span("View all available system fonts".to_string()) .build() .with_margin_left(2.) .finish(), ) .finish(), ) .with_cross_axis_alignment(CrossAxisAlignment::Center) .finish(), ) .with_margin_bottom(16.) .finish(), ); terminal_font_row.add_child(Shrinkable::new(1., terminal_font.finish()).finish()); // Font Weight let mut font_weight = Flex::column(); font_weight.add_child( appearance .ui_builder() .label("Font weight".to_string()) .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE), ..Default::default() }) .build() .with_margin_left(12.) .finish(), ); font_weight.add_child( Container::new(ChildView::new(&view.font_weight_dropdown).finish()) .with_margin_left(12.) .finish(), ); terminal_font_row.add_child(Container::new(font_weight.finish()).finish()); // Font Size let mut font_size = Flex::column(); font_size.add_child( appearance .ui_builder() .label("Font size (px)".to_string()) .with_style(UiComponentStyles { margin: Some(Coords { left: 2., ..Default::default() }), font_size: Some(CONTENT_FONT_SIZE), ..Default::default() }) .build() .finish(), ); font_size.add_child( Container::new( Dismiss::new( appearance .ui_builder() .text_input(view.font_size_editor.clone()) .with_style(UiComponentStyles { width: Some(FONT_SIZE_INPUT_BOX_WIDTH), padding: Some(Coords { top: 7., bottom: 7., left: 12., right: 12., }), margin: Some(Coords { top: 2., left: 2., ..Default::default() }), background: Some(appearance.theme().surface_2().into()), ..Default::default() }) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action(AppearancePageAction::SetFontSize) }) .finish(), ) .with_padding_top(4.) .finish(), ); terminal_font_row.add_child( Container::new(font_size.finish()) .with_margin_left(12.) .finish(), ); self.render_line_height_editor(view, appearance, &mut terminal_font_row); terminal_font_row.finish() } } #[derive(Default)] struct NotebookFontSizeWidget { checkbox_state: MouseStateHandle, } impl SettingsWidget for NotebookFontSizeWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "text notebook font size" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let font_settings = FontSettings::as_ref(app); Container::new( Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Shrinkable::new( 1.0, Align::new( appearance .ui_builder() .span("Notebook font size".to_string()) .build() .with_margin_right(16.) .finish(), ) .left() .finish(), ) .finish(), ) .with_child( appearance .ui_builder() .checkbox(self.checkbox_state.clone(), None) .check(*font_settings.match_notebook_to_monospace_font_size) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( AppearancePageAction::ToggleMatchNotebookToMonospaceFontSize, ) }) .finish(), ) .with_child( appearance .ui_builder() .span("Match terminal".to_string()) .build() .with_margin_left(2.) .with_margin_right(16.) .finish(), ) .with_child( Container::new( Dismiss::new( appearance .ui_builder() .text_input(view.notebook_font_size_editor.clone()) .with_style(UiComponentStyles { width: Some(NOTEBOOK_FONT_SIZE_INPUT_BOX_WIDTH), padding: Some(Coords { top: 7., bottom: 7., left: 16., right: 16., }), background: Some(appearance.theme().surface_2().into()), ..Default::default() }) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action(AppearancePageAction::SetNotebookFontSize) }) .finish(), ) .finish(), ) .finish(), ) .with_margin_bottom(10.) .finish() } } #[derive(Default)] struct ThinStrokesWidget {} impl SettingsWidget for ThinStrokesWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "text thin strokes high dpi" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_dropdown_item( appearance, "Use thin strokes", None, None, LocalOnlyIconState::for_setting( UseThinStrokes::storage_key(), UseThinStrokes::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), None, &view.thin_strokes_dropdown, ) } } #[derive(Default)] struct MinimumContrastWidget {} impl SettingsWidget for MinimumContrastWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "text minimum contrast high" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_dropdown_item( appearance, "Enforce minimum contrast", None, None, LocalOnlyIconState::for_setting( crate::settings::font::EnforceMinimumContrast::storage_key(), crate::settings::font::EnforceMinimumContrast::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), None, &view.enforce_min_contrast_dropdown, ) } } #[derive(Default)] struct LigaturesWidget { switch_state: SwitchStateHandle, info_mouse_state: MouseStateHandle, } impl SettingsWidget for LigaturesWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "text font ligatures" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ligature_rendering = &LigatureSettings::as_ref(app).ligature_rendering_enabled; let ligature_rendering_enabled = ligature_rendering.value(); render_body_item::( "Show ligatures in terminal".into(), Some(AdditionalInfo { mouse_state: self.info_mouse_state.clone(), on_click_action: None, secondary_text: None, tooltip_override_text: Some("Ligatures may reduce performance".to_string()), }), LocalOnlyIconState::for_setting( LigatureRenderingEnabled::storage_key(), LigatureRenderingEnabled::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*ligature_rendering_enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleLigatureRendering); }) .finish(), None, ) } } struct CursorTypeWidget { radio_state: RadioButtonStateHandle, radio_buttons_states: Vec, } impl Default for CursorTypeWidget { fn default() -> Self { Self { radio_state: Default::default(), radio_buttons_states: all::() .map(|_| Default::default()) .collect(), } } } impl SettingsWidget for CursorTypeWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "cursor shape cursor type block bar beam underline" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let settings = AppEditorSettings::as_ref(app); let cursor_display_type = &settings.cursor_display_type; let is_vim_mode_enabled = *settings.vim_mode.value(); let cursor_display_types: Vec = all::().collect(); render_body_item::( "Cursor type".into(), None, LocalOnlyIconState::for_setting( CursorBlinkEnabled::storage_key(), CursorBlinkEnabled::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, match is_vim_mode_enabled { true => Flex::column() .with_child( appearance .ui_builder() .span("Cursor type is disabled in Vim mode".to_string()) .build() .finish(), ) .finish(), false => appearance .ui_builder() .radio_buttons( self.radio_buttons_states.clone(), cursor_display_types .iter() .map(|x| RadioButtonItem::text(x.to_string())) .collect(), self.radio_state.clone(), Some(cursor_display_type.value().to_index()), appearance.ui_font_size(), RadioButtonLayout::Row, ) .on_change(Rc::new(move |ctx, _, index| { if let Some(index) = index { ctx.dispatch_typed_action(AppearancePageAction::SetCursorType( CursorDisplayType::nth(index).expect("Cursor does not exist"), )); } })) .build() .finish(), }, None, ) } } #[derive(Default)] struct BlinkingCursorWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for BlinkingCursorWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "blinking cursor" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let settings = AppEditorSettings::as_ref(app); let cursor_blink = &settings.cursor_blink; render_body_item::( "Blinking cursor".into(), None, LocalOnlyIconState::for_setting( CursorBlinkEnabled::storage_key(), CursorBlinkEnabled::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(cursor_blink.value() == &CursorBlink::Enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleCursorBlink); }) .finish(), None, ) } } #[derive(Default)] struct TabCloseButtonPositionWidget {} impl SettingsWidget for TabCloseButtonPositionWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "tab bar close button position left right" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_dropdown_item( appearance, "Tab close button position", None, None, LocalOnlyIconState::for_setting( TabCloseButtonPosition::storage_key(), TabCloseButtonPosition::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), None, &view.tab_close_button_position_dropdown, ) } } #[derive(Default)] struct TabIndicatorWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for TabIndicatorWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "tab indicator" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let tab_settings = TabSettings::as_ref(app); render_body_item::( "Show tab indicators".into(), None, LocalOnlyIconState::for_setting( ShowIndicatorsButton::storage_key(), ShowIndicatorsButton::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*tab_settings.show_indicators) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleTabIndicators); }) .finish(), None, ) } } #[derive(Default)] struct CodeReviewButtonWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for CodeReviewButtonWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "code review button tab bar" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let tab_settings = TabSettings::as_ref(app); render_body_item::( "Show code review button".into(), None, LocalOnlyIconState::for_setting( ShowCodeReviewButton::storage_key(), ShowCodeReviewButton::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*tab_settings.show_code_review_button) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleShowCodeReviewButton); }) .finish(), None, ) } } #[derive(Default)] struct PreserveActiveTabColorWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for PreserveActiveTabColorWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "tab color preserve new inherit active" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let tab_settings = TabSettings::as_ref(app); render_body_item::( "Preserve active tab color for new tabs".into(), None, LocalOnlyIconState::for_setting( PreserveActiveTabColor::storage_key(), PreserveActiveTabColor::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*tab_settings.preserve_active_tab_color) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::TogglePreserveActiveTabColor); }) .finish(), None, ) } } #[derive(Default)] struct VerticalTabsWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for VerticalTabsWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "vertical tabs sidebar layout" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let tab_settings = TabSettings::as_ref(app); render_body_item::( "Use vertical tab layout".into(), None, LocalOnlyIconState::for_setting( UseVerticalTabs::storage_key(), UseVerticalTabs::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*tab_settings.use_vertical_tabs) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ToggleVerticalTabs); }) .finish(), None, ) } } #[derive(Default)] struct ShowVerticalTabPanelInRestoredWindowsWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for ShowVerticalTabPanelInRestoredWindowsWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "vertical tabs panel restore window session snapshot" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let tab_settings = TabSettings::as_ref(app); render_body_item::( "Show vertical tabs panel in restored windows".into(), None, LocalOnlyIconState::for_setting( ShowVerticalTabPanelInRestoredWindows::storage_key(), ShowVerticalTabPanelInRestoredWindows::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*tab_settings.show_vertical_tab_panel_in_restored_windows) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( AppearancePageAction::ToggleShowVerticalTabPanelInRestoredWindows, ); }) .finish(), Some( "When enabled, reopening or restoring a window opens the vertical tabs panel even if it was closed when the window was last saved." .to_string(), ), ) } } #[derive(Default)] struct HideTitleBarSearchBarInVerticalTabsWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for HideTitleBarSearchBarInVerticalTabsWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "hide title bar search bar vertical tabs chrome minimal" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let tab_settings = TabSettings::as_ref(app); render_body_item::( "Hide search bar in vertical tab layout".into(), None, LocalOnlyIconState::for_setting( HideTitleBarSearchBarInVerticalTabs::storage_key(), HideTitleBarSearchBarInVerticalTabs::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*tab_settings.hide_title_bar_search_bar_in_vertical_tabs) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( AppearancePageAction::ToggleHideTitleBarSearchBarInVerticalTabs, ); }) .finish(), Some( "When using the vertical tab layout, hide the search bar in the title bar. Search stays available via the command palette and keyboard shortcuts." .to_string(), ), ) } } #[derive(Default)] struct UseLatestUserPromptAsConversationTitleInTabNamesWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for UseLatestUserPromptAsConversationTitleInTabNamesWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "latest user prompt conversation title tab names vertical tabs oz third-party agent" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let tab_settings = TabSettings::as_ref(app); render_body_item::( "Use latest user prompt as conversation title in tab names".into(), None, LocalOnlyIconState::for_setting( UseLatestUserPromptAsConversationTitleInTabNames::storage_key(), UseLatestUserPromptAsConversationTitleInTabNames::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check( *tab_settings .use_latest_user_prompt_as_conversation_title_in_tab_names, ) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( AppearancePageAction::ToggleUseLatestUserPromptAsConversationTitleInTabNames, ); }) .finish(), Some( "Show the latest user prompt instead of the generated conversation title for Oz and third-party agent sessions in vertical tabs." .to_string(), ), ) } } #[derive(Default)] struct EditToolbarWidget; impl SettingsWidget for EditToolbarWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "edit toolbar header panel buttons configure arrange layout chip chips rearrange re-arrange customize" } fn render( &self, view: &Self::View, appearance: &Appearance, _app: &AppContext, ) -> Box { let label = render_body_item_label::( "Header toolbar layout".to_string(), None, None, LocalOnlyIconState::Hidden, ToggleState::Enabled, appearance, ); let editor = Container::new(ChildView::new(&view.header_toolbar_inline_editor).finish()) .with_padding_bottom(HEADER_PADDING) .finish(); Flex::column() .with_child(Container::new(label).with_margin_bottom(4.).finish()) .with_child(editor) .finish() } } /// Returns the visible directory tab colors from the persisted setting. fn build_directory_delete_buttons( ctx: &mut ViewContext, ) -> Vec> { directory_tab_colors(ctx) .into_iter() .map(|(dir_path, _)| { let delete_path = PathBuf::from(&dir_path); ctx.add_typed_action_view(move |_| { ActionButton::new("", NakedTheme) .with_icon(Icon::X) .with_size(ButtonSize::XSmall) .on_click(move |ctx| { ctx.dispatch_typed_action( AppearancePageAction::RemoveDefaultDirectoryTabColor { path: delete_path.clone(), }, ); }) }) }) .collect() } fn add_directory_tab_color_path(path: PathBuf, ctx: &mut ViewContext) { TabSettings::handle(ctx).update(ctx, |settings, ctx| { let current = settings.directory_tab_colors.value(); let key = canonical_directory_key(&path); let dominated_by_existing = current .0 .get(&key) .is_some_and(|c| *c != DirectoryTabColor::Suppressed); if !dominated_by_existing { let new_value = current.with_color(&path, DirectoryTabColor::Unassigned); let _ = settings.directory_tab_colors.set_value(new_value, ctx); } }); } fn open_directory_tab_color_folder_picker(ctx: &mut ViewContext) { let file_picker_config = FilePickerConfiguration::new().folders_only(); ctx.open_file_picker( move |result, ctx| { if let Ok(paths) = result { if let Some(directory_path) = paths.first() { add_directory_tab_color_path(PathBuf::from(directory_path), ctx); } } }, file_picker_config, ); } fn directory_tab_colors(app: &AppContext) -> Vec<(String, DirectoryTabColor)> { let configured = &TabSettings::as_ref(app).directory_tab_colors.value().0; let mut sorted: Vec<_> = configured .iter() .filter(|(_, color)| !matches!(color, DirectoryTabColor::Suppressed)) .map(|(path, color)| (path.clone(), *color)) .collect(); sorted.sort_by(|(a, _), (b, _)| a.cmp(b)); sorted } struct DirectoryTabColorsWidget { add_picker: ViewHandle, } impl SettingsWidget for DirectoryTabColorsWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "directory tab color folder codebase repo" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let theme = appearance.theme(); let mut content = Flex::column().with_spacing(8.); let header_text = Flex::column() .with_spacing(4.) .with_child( Text::new( "Directory tab colors", appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(theme.active_ui_text_color().into()) .soft_wrap(false) .finish(), ) .with_child( Text::new( "Automatically color tabs based on the directory or repo you're working in.", appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(theme.nonactive_ui_text_color().into()) .finish(), ) .finish(); content.add_child( Flex::row() .with_main_axis_size(MainAxisSize::Max) .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_child(Shrinkable::new(1., header_text).finish()) .with_child(ChildView::new(&self.add_picker).finish()) .finish(), ); let home_dir = dirs::home_dir().and_then(|home_dir| home_dir.to_str().map(|s| s.to_owned())); for (idx, (dir_path, current_color)) in directory_tab_colors(app).into_iter().enumerate() { let Some(dot_mouse_states) = view.color_picker_dot_states.get(idx).cloned() else { log::error!("Missing color picker dot states for directory index {idx}"); continue; }; let friendly_path = user_friendly_path(&dir_path, home_dir.as_deref()).to_string(); let path_label = Shrinkable::new( 1., Text::new( friendly_path, appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(theme.nonactive_ui_text_color().into()) .soft_wrap(false) .finish(), ) .finish(); let mut dots_row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center); // None = default (no-color) dot, Some = ANSI color dot let color_options = std::iter::once(None).chain(TAB_COLOR_OPTIONS.iter().copied().map(Some)); for (ansi_id, mouse_state) in color_options.zip(dot_mouse_states.iter().cloned()) { let tab_color = match ansi_id { None => DirectoryTabColor::Unassigned, Some(id) => DirectoryTabColor::Color(id), }; let dot_color = match ansi_id { None => pathfinder_color::ColorU::transparent_black(), Some(id) => id.to_ansi_color(&theme.terminal_colors().normal).into(), }; let is_selected = current_color == tab_color; let tooltip_text = match ansi_id { None => "Default (no color)".to_string(), Some(id) => id.to_string(), }; let dir_path_clone = PathBuf::from(&dir_path); dots_row.add_child( render_color_dot( mouse_state, dot_color, is_selected, theme.accent().into(), ansi_id.is_none(), theme.foreground(), tooltip_text, appearance, ) .on_click(move |ctx, _, _| { if !is_selected { ctx.dispatch_typed_action( AppearancePageAction::SetDefaultDirectoryTabColor { path: dir_path_clone.clone(), color: tab_color, }, ); } }) .finish(), ); } let mut right_controls = Flex::row() .with_spacing(8.) .with_cross_axis_alignment(CrossAxisAlignment::Center); right_controls.add_child(dots_row.finish()); if let Some(delete_button) = view.directory_tab_color_delete_buttons.get(idx) { right_controls.add_child(ChildView::new(delete_button).finish()); } let right_controls = right_controls.finish(); let row = Flex::row() .with_main_axis_size(MainAxisSize::Max) .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_child(path_label) .with_child(right_controls) .finish(); content.add_child( Container::new(row) .with_horizontal_padding(16.) .with_vertical_padding(8.) .with_background(theme.surface_1()) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.))) .finish(), ); } Container::new(content.finish()) .with_padding_bottom(HEADER_PADDING) .finish() } } #[derive(Default)] struct ZenModeWidget {} impl SettingsWidget for ZenModeWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "zen mode minimal tab bar window decoration" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_dropdown_item( appearance, "Show the tab bar", None, None, LocalOnlyIconState::for_setting( WorkspaceDecorationVisibility::storage_key(), WorkspaceDecorationVisibility::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), None, &view.workspace_decorations_dropdown, ) } } #[derive(Default)] struct AltScreenPaddingWidget { switch_state: SwitchStateHandle, additional_info_mouse_state: MouseStateHandle, } impl SettingsWidget for AltScreenPaddingWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "alt screen padding border space vim" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let terminal_settings = &TerminalSettings::as_ref(app); let theme = appearance.theme(); let mut column = Flex::column().with_child(render_body_item::( "Use custom padding in alt-screen".into(), Some(AdditionalInfo { mouse_state: self.additional_info_mouse_state.clone(), on_click_action: Some(AppearancePageAction::OpenUrl( "https://docs.warp.dev/terminal/more-features/full-screen-apps#padding".into(), )), secondary_text: None, tooltip_override_text: None, }), LocalOnlyIconState::for_setting( AltScreenPadding::storage_key(), AltScreenPadding::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(matches!( terminal_settings.alt_screen_padding.value(), AltScreenPaddingMode::Custom { .. } )) .build() .on_click(move |ctx, app, _| { let new_mode = TerminalSettings::as_ref(app).alt_screen_padding.toggled(); ctx.dispatch_typed_action(AppearancePageAction::UpdateAltScreenPaddingMode( new_mode, )); }) .finish(), None, )); if matches!( terminal_settings.alt_screen_padding.value(), AltScreenPaddingMode::Custom { .. } ) { let buffer_text = view.alt_screen_padding_editor.as_ref(app).buffer_text(app); let border_color = match buffer_text.parse::() { Ok(p) if p >= 0. => None, _ => Some(themes::theme::Fill::error().into()), }; let editor_style = UiComponentStyles { width: Some(40.), padding: Some(Coords::uniform(5.)), background: Some(theme.surface_2().into()), border_color, ..Default::default() }; let editor_row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_main_axis_size(MainAxisSize::Max) .with_child( Container::new( Align::new( Text::new( "Uniform padding (px)", appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(theme.active_ui_text_color().into()) .finish(), ) .finish(), ) .finish(), ) .with_child( appearance .ui_builder() .text_input(view.alt_screen_padding_editor.clone()) .with_style(editor_style) .build() .finish(), ) .finish(); column.add_child(render_group([editor_row], appearance)); } column.finish() } } struct ZoomLevelWidget; impl SettingsWidget for ZoomLevelWidget { type View = AppearanceSettingsPageView; fn search_terms(&self) -> &str { "zoom level zoom size scale" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let current_zoom_level = *WindowSettings::as_ref(app).zoom_level.value(); let changed_from_default = current_zoom_level != ZoomLevel::default_value(); let reset_button = build_reset_button( appearance, view.zoom_reset_button_mouse_state.clone(), changed_from_default, ) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(AppearancePageAction::ResetZoomLevel); }) .finish(); render_dropdown_item( appearance, "Zoom", Some("Adjusts the default zoom level across all windows"), Some(reset_button), LocalOnlyIconState::for_setting( crate::window_settings::ZoomLevel::storage_key(), crate::window_settings::ZoomLevel::sync_to_cloud(), &mut view.local_only_icon_tooltip_states.borrow_mut(), app, ), None, &view.zoom_level_dropdown, ) } } impl SettingsPageMeta for AppearanceSettingsPageView { fn section() -> SettingsSection { SettingsSection::Appearance } fn should_render(&self, _ctx: &AppContext) -> bool { true } fn on_page_selected(&mut self, _: bool, ctx: &mut ViewContext) { ctx.notify(); } 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::Appearance(view_handle) } }