Files
galaxy/app/src/settings_view/privacy_page.rs
T

1760 lines
64 KiB
Rust

use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use std::time::Duration;
use pathfinder_geometry::vector::vec2f;
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::r#async::{SpawnedFutureHandle, Timer};
use regex::Regex;
use settings::Setting as _;
use galaxy_core::context_flag::ContextFlag;
use galaxy_core::features::FeatureFlag;
use galaxy_core::ui::theme::WarpTheme;
use galaxyui::elements::{
Align, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
Empty, Expanded, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, Rect, Shrinkable,
Stack, Text,
};
use galaxyui::keymap::ContextPredicate;
use galaxyui::platform::Cursor;
use galaxyui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
use galaxyui::ui_components::{
components::{Coords, UiComponent, UiComponentStyles},
switch::{SwitchStateHandle, TooltipConfig},
};
use galaxyui::{
Action, AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView,
UpdateModel, View, ViewContext, ViewHandle,
};
use crate::settings::{CustomSecretRegex, RegexDisplayInfo};
use crate::settings_view::privacy::AddRegexModalViewState;
use crate::settings_view::render_body_item_label;
use crate::settings_view::settings_page::CONTENT_FONT_SIZE;
use crate::terminal::safe_mode_settings::{
get_effective_secret_display_mode, SecretDisplayMode, SecretDisplayModeSetting,
};
use crate::ui_components::buttons::icon_button;
use crate::view_components::{Dropdown, DropdownItem};
use crate::{
appearance::Appearance,
channel::ChannelState,
report_if_error, send_telemetry_from_ctx,
server::telemetry::TelemetryEvent,
settings::{AISettings, PrivacySettings},
terminal::safe_mode_settings::{SafeModeEnabled, SafeModeSettings},
ui_components::icons::Icon,
workspaces::{
user_workspaces::UserWorkspaces,
workspace::{CustomerType, UgcCollectionEnablementSetting},
},
};
use super::{
flags,
privacy::{AddRegexModal, AddRegexModalEvent},
settings_page::{
render_body_item, render_sub_header, SettingsPageMeta, SettingsPageViewHandle, ToggleState,
HEADER_PADDING, TOGGLE_BUTTON_RIGHT_PADDING,
},
settings_page::{LocalOnlyIconState, MatchData, PageType, SettingsWidget, PAGE_PADDING},
SettingsAction, SettingsSection, ToggleSettingActionPair,
};
use crate::modal::{Modal, ModalEvent, ModalViewState};
use galaxyui::fonts::Weight;
const FONT_SIZE: f32 = 12.;
const SAFE_MODE_TITLE: &str = "Secret redaction";
static SAFE_MODE_DESCRIPTION: LazyLock<&'static str> = LazyLock::new(|| {
"When this setting is enabled, Warp will scan blocks, the contents of \
Warp Drive objects, and Oz prompts for potential sensitive \
information and prevent saving or sending this data to any \
servers. You can customize this list via regexes."
});
const USER_SECRET_REGEX_TITLE: &str = "Custom secret redaction";
const USER_SECRET_REGEX_DESCRIPTION: &str =
"Use regex to define additional secrets or data you'd like to redact. This will take effect \
when the next command runs. You can use the inline (?i) flag as a prefix to your regex \
to make it case-insensitive.";
const TELEMETRY_DESCRIPTION_OLD: &str =
"App analytics help us make the product better for you. We only collect \
app usage metadata, never console input or output.";
const TELEMETRY_TITLE: &str = "Help improve Warp";
const TELEMETRY_DESCRIPTION: &str =
"App analytics help us make the product better for you. We may collect \
certain console interactions to improve Warp's AI capabilities.";
const TELEMETRY_FREE_TIER_NOTE: &str =
"On the free tier, analytics must be enabled to use AI features.";
const TELEMETRY_DOCS_URL: &str =
"https://docs.warp.dev/support-and-community/privacy-and-security/privacy#what-telemetry-data-does-warp-collect-and-why";
pub struct PrivacyPageView {
page: PageType<Self>,
local_only_icon_tooltip_states: RefCell<HashMap<String, MouseStateHandle>>,
/// This needs to mirror the length of PrivacySettings::user_secret_regex_list.
added_user_secret_regex_list_button_handles: Vec<MouseStateHandle>,
/// Set of indices for regex items that are pending removal
pending_regex_removals: HashSet<usize>,
/// Handle to the current debounce timer
pending_timer: Option<SpawnedFutureHandle>,
/// Modal state
add_regex_modal_state: AddRegexModalViewState,
/// Active tab for secret redaction settings
active_secret_redaction_tab: SecretRedactionTab,
/// Dropdown for selecting secret redaction display mode
secret_redaction_display_dropdown: ViewHandle<Dropdown<PrivacyPageAction>>,
}
#[derive(Clone, Copy)]
pub enum PrivacyPageViewEvent {
LaunchNetworkLogging,
ShowAddRegexModal,
HideAddRegexModal,
}
impl PrivacyPageView {
const BATCH_TIMEOUT_MS: u64 = 700;
pub fn new(ctx: &mut ViewContext<PrivacyPageView>) -> Self {
let privacy_settings_handle = PrivacySettings::handle(ctx);
ctx.observe(&privacy_settings_handle, |_, _, ctx| {
// It is possible that PrivacySettings are updated without an interaction in this view
// (e.g. if the server response fetching settings to be synced is received after the
// view is opened), so notify the view if the model is updated.
ctx.notify();
});
ctx.observe(&privacy_settings_handle, Self::update_button_states);
ctx.subscribe_to_model(&privacy_settings_handle, |me, model, _, ctx| {
me.update_button_states(model, ctx);
ctx.notify();
});
ctx.subscribe_to_model(&SafeModeSettings::handle(ctx), |me, _, _, ctx| {
me.update_secret_display_dropdown(ctx);
ctx.notify();
});
let add_regex_body = ctx.add_typed_action_view(AddRegexModal::new);
ctx.subscribe_to_view(&add_regex_body, |me, _, event, ctx| {
me.handle_add_regex_modal_event(event, ctx);
});
let add_regex_modal_view = ctx.add_typed_action_view(|ctx| {
Modal::new(Some("Add regex pattern".to_string()), add_regex_body, ctx)
.with_modal_style(UiComponentStyles {
width: Some(600.),
height: Some(400.),
..Default::default()
})
.with_header_style(UiComponentStyles {
padding: Some(Coords {
top: 24.,
bottom: 0.,
left: 24.,
right: 24.,
}),
font_size: Some(16.),
font_weight: Some(Weight::Bold),
..Default::default()
})
.with_body_style(UiComponentStyles {
padding: Some(Coords {
top: 0.,
bottom: 24.,
left: 24.,
right: 24.,
}),
..Default::default()
})
.with_background_opacity(100)
.with_dismiss_on_click()
});
ctx.subscribe_to_view(&add_regex_modal_view, |me, _, event, ctx| {
me.handle_modal_event(event, ctx);
});
let secret_display_dropdown = ctx.add_typed_action_view(|ctx| {
let mut dropdown = Dropdown::new(ctx);
dropdown.set_items(
SecretDisplayMode::all_modes()
.iter()
.map(|mode| {
DropdownItem::new(
mode.display_name(),
PrivacyPageAction::SetSecretDisplayMode(*mode),
)
})
.collect(),
ctx,
);
dropdown
});
let mut privacy_page_view = Self {
page: Self::build_page(),
local_only_icon_tooltip_states: Default::default(),
added_user_secret_regex_list_button_handles: Default::default(),
pending_regex_removals: Default::default(),
pending_timer: None,
add_regex_modal_state: AddRegexModalViewState::new(ModalViewState::new(
add_regex_modal_view,
)),
active_secret_redaction_tab: SecretRedactionTab::Personal,
secret_redaction_display_dropdown: secret_display_dropdown,
};
privacy_page_view.update_button_states(privacy_settings_handle, ctx);
privacy_page_view.update_secret_display_dropdown(ctx);
privacy_page_view
}
fn build_page() -> PageType<Self> {
let mut widgets: Vec<Box<dyn SettingsWidget<View = Self>>> = vec![
Box::new(SecretRedactionWidget::default()),
Box::new(AppAnalyticsWidget::default()),
Box::new(CrashReportsWidget::default()),
];
if ContextFlag::NetworkLogConsole.is_enabled() {
widgets.push(Box::new(NetworkLogWidget::default()));
}
PageType::new_uncategorized(widgets, Some("Privacy"))
}
fn update_button_states(
&mut self,
privacy_settings_handle: ModelHandle<PrivacySettings>,
ctx: &mut ViewContext<Self>,
) {
let privacy_settings = privacy_settings_handle.as_ref(ctx);
self.added_user_secret_regex_list_button_handles = privacy_settings
.user_secret_regex_list
.iter()
.map(|_| Default::default())
.collect();
}
fn toggle_safe_mode(&mut self, ctx: &mut ViewContext<Self>) {
let safe_mode_settings = SafeModeSettings::handle(ctx);
let new_value = { !*safe_mode_settings.as_ref(ctx).safe_mode_enabled.value() };
send_telemetry_from_ctx!(
TelemetryEvent::ToggleSecretRedaction { enabled: new_value },
ctx
);
ctx.update_model(&safe_mode_settings, move |safe_mode_settings, ctx| {
report_if_error!(safe_mode_settings
.safe_mode_enabled
.set_value(new_value, ctx));
});
ctx.notify();
}
fn toggle_hide_secrets_in_block_list(&mut self, ctx: &mut ViewContext<Self>) {
let safe_mode_settings = SafeModeSettings::handle(ctx);
let new_value = {
!*safe_mode_settings
.as_ref(ctx)
.hide_secrets_in_block_list
.value()
};
ctx.update_model(&safe_mode_settings, move |safe_mode_settings, ctx| {
report_if_error!(safe_mode_settings
.hide_secrets_in_block_list
.set_value(new_value, ctx));
});
ctx.notify();
}
fn set_secret_display_mode(&mut self, mode: SecretDisplayMode, ctx: &mut ViewContext<Self>) {
let safe_mode_settings = SafeModeSettings::handle(ctx);
ctx.update_model(&safe_mode_settings, move |safe_mode_settings, ctx| {
report_if_error!(safe_mode_settings.secret_display_mode.set_value(mode, ctx));
});
ctx.notify();
}
fn toggle_telemetry(&mut self, ctx: &mut ViewContext<Self>) {
let privacy_settings_handle = PrivacySettings::handle(ctx);
let old_value = privacy_settings_handle.as_ref(ctx).is_telemetry_enabled;
ctx.update_model(&privacy_settings_handle, |privacy_settings, ctx| {
privacy_settings.set_is_telemetry_enabled(!old_value, ctx);
});
ctx.notify();
}
fn toggle_crash_reporting(&mut self, ctx: &mut ViewContext<Self>) {
let privacy_settings_handle = PrivacySettings::handle(ctx);
let old_value = privacy_settings_handle
.as_ref(ctx)
.is_crash_reporting_enabled;
ctx.update_model(&privacy_settings_handle, |privacy_settings, ctx| {
privacy_settings.set_is_crash_reporting_enabled(!old_value, ctx);
});
ctx.notify();
}
fn queue_regex_removal(&mut self, idx: usize, ctx: &mut ViewContext<Self>) {
// Check if this removal is already pending
if self.pending_regex_removals.contains(&idx) {
return;
}
if let Some(timer) = self.pending_timer.take() {
timer.abort();
}
// Add to pending set
self.pending_regex_removals.insert(idx);
ctx.notify();
// Start a new timer only if we don't have one
if self.pending_timer.is_none() {
let handle = ctx.spawn(
async move {
Timer::after(Duration::from_millis(Self::BATCH_TIMEOUT_MS)).await;
},
|me, _, ctx| {
// Only process if we still have pending removals and a timer
// (they might have been processed by an add operation)
if !me.pending_regex_removals.is_empty() && me.pending_timer.is_some() {
me.pending_timer = None;
me.process_pending_removals(ctx);
}
},
);
self.pending_timer = Some(handle);
}
}
fn update_secret_display_dropdown(&mut self, ctx: &mut ViewContext<Self>) {
let safe_mode_settings = SafeModeSettings::as_ref(ctx);
let current_mode = get_effective_secret_display_mode(safe_mode_settings);
self.secret_redaction_display_dropdown
.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_action(
PrivacyPageAction::SetSecretDisplayMode(current_mode),
ctx,
);
});
}
fn process_pending_removals(&mut self, ctx: &mut ViewContext<Self>) {
let mut indices: Vec<_> = self.pending_regex_removals.iter().copied().collect();
if indices.is_empty() {
return;
}
indices.sort_unstable_by(|a, b| b.cmp(a)); // Sort in reverse order to remove from highest index first
let privacy_settings_handle = PrivacySettings::handle(ctx);
for idx in indices {
privacy_settings_handle.update(ctx, |privacy_settings, ctx| {
privacy_settings.remove_user_secret_regex(&idx, ctx);
});
}
self.pending_regex_removals.clear();
ctx.notify();
}
fn launch_network_logging(&mut self, ctx: &mut ViewContext<Self>) {
ctx.emit(PrivacyPageViewEvent::LaunchNetworkLogging);
}
fn show_add_regex_modal(&mut self, ctx: &mut ViewContext<Self>) {
self.add_regex_modal_state.open(ctx);
ctx.emit(PrivacyPageViewEvent::ShowAddRegexModal);
}
fn hide_add_regex_modal(&mut self, ctx: &mut ViewContext<Self>) {
self.add_regex_modal_state.close(ctx);
ctx.emit(PrivacyPageViewEvent::HideAddRegexModal);
}
fn handle_modal_event(&mut self, event: &ModalEvent, ctx: &mut ViewContext<Self>) {
match event {
ModalEvent::Close => {
self.hide_add_regex_modal(ctx);
}
}
}
fn handle_add_regex_modal_event(
&mut self,
event: &AddRegexModalEvent,
ctx: &mut ViewContext<Self>,
) {
match event {
AddRegexModalEvent::Close => {
self.hide_add_regex_modal(ctx);
}
AddRegexModalEvent::Submit { name, pattern } => {
self.add_custom_regex(name.clone(), pattern.clone(), ctx);
self.hide_add_regex_modal(ctx);
}
}
}
fn add_custom_regex(&mut self, name: String, pattern: String, ctx: &mut ViewContext<Self>) {
// First process any pending removals
if !self.pending_regex_removals.is_empty() {
self.process_pending_removals(ctx);
}
let privacy_settings_handle = PrivacySettings::handle(ctx);
ctx.update_model(&privacy_settings_handle, |privacy_settings, ctx| {
if let Ok(regex) = Regex::new(&pattern) {
let mut new_user_secret_regex_list =
privacy_settings.user_secret_regex_list.to_vec();
new_user_secret_regex_list.push(CustomSecretRegex {
pattern: regex,
name: if name.trim().is_empty() {
None
} else {
Some(name.trim().to_string())
},
});
if privacy_settings
.user_secret_regex_list
.set_value(new_user_secret_regex_list, ctx)
.is_err()
{
log::error!("Failed to add custom regex to secret regex list");
}
ctx.notify();
} else {
log::error!("Invalid regex pattern: {pattern}");
}
});
}
pub fn get_modal_content(&self) -> Option<Box<dyn Element>> {
if self.add_regex_modal_state.is_open() {
Some(self.add_regex_modal_state.render())
} else {
None
}
}
}
impl View for PrivacyPageView {
fn ui_name() -> &'static str {
"PrivacyPageView"
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
self.page.render(self, app)
}
}
impl Entity for PrivacyPageView {
type Event = PrivacyPageViewEvent;
}
#[derive(Clone, Debug, PartialEq)]
pub enum PrivacyPageAction {
ToggleSafeMode,
ToggleHideSecretsInBlockList,
SetSecretDisplayMode(SecretDisplayMode),
ToggleTelemetry,
ToggleCrashReporting,
LaunchNetworkLogging,
RemoveCustomRegex(usize),
AddAllRecommendedRegexes,
ShowAddRegexModal,
AddRecommendedRegex(usize),
SwitchSecretRedactionTab(SecretRedactionTab),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SecretRedactionTab {
Personal,
Enterprise,
}
impl TypedActionView for PrivacyPageView {
type Action = PrivacyPageAction;
fn handle_action(&mut self, action: &PrivacyPageAction, ctx: &mut ViewContext<Self>) {
match action {
PrivacyPageAction::AddRecommendedRegex(idx) => {
// First process any pending removals
if !self.pending_regex_removals.is_empty() {
self.process_pending_removals(ctx);
}
let privacy_settings_handle = PrivacySettings::handle(ctx);
ctx.update_model(&privacy_settings_handle, |privacy_settings, ctx| {
let workspaces = UserWorkspaces::as_ref(ctx);
let enterprise_regex_list =
workspaces.get_enterprise_secret_redaction_regex_list();
let current_patterns: Vec<&str> = enterprise_regex_list
.iter()
.map(|s| s.pattern.as_str())
.chain(
privacy_settings
.user_secret_regex_list
.iter()
.map(|r| r.pattern().as_str()),
)
.collect();
let recommended_regexes: Vec<_> =
crate::terminal::model::secrets::regexes::DEFAULT_REGEXES_WITH_NAMES
.iter()
.filter(|r| !current_patterns.contains(&r.pattern))
.collect();
if let Some(regex) = recommended_regexes.get(*idx) {
if let Ok(pattern) = Regex::new(regex.pattern) {
let mut new_user_secret_regex_list =
privacy_settings.user_secret_regex_list.to_vec();
new_user_secret_regex_list.push(CustomSecretRegex {
pattern,
name: Some(regex.name.to_string()),
});
if privacy_settings
.user_secret_regex_list
.set_value(new_user_secret_regex_list, ctx)
.is_err()
{
log::error!(
"Failed to add recommended regex to custom secret regex list"
);
}
ctx.notify();
}
}
});
}
PrivacyPageAction::ToggleSafeMode => self.toggle_safe_mode(ctx),
PrivacyPageAction::ToggleHideSecretsInBlockList => {
self.toggle_hide_secrets_in_block_list(ctx)
}
PrivacyPageAction::SetSecretDisplayMode(mode) => {
self.set_secret_display_mode(*mode, ctx)
}
PrivacyPageAction::ToggleTelemetry => self.toggle_telemetry(ctx),
PrivacyPageAction::ToggleCrashReporting => self.toggle_crash_reporting(ctx),
PrivacyPageAction::LaunchNetworkLogging => self.launch_network_logging(ctx),
PrivacyPageAction::RemoveCustomRegex(idx) => {
self.queue_regex_removal(*idx, ctx);
}
PrivacyPageAction::AddAllRecommendedRegexes => {
// First process any pending removals
if !self.pending_regex_removals.is_empty() {
self.process_pending_removals(ctx);
}
let privacy_settings_handle = PrivacySettings::handle(ctx);
ctx.update_model(&privacy_settings_handle, |privacy_settings, ctx| {
privacy_settings.add_all_recommended_regex(ctx);
});
}
PrivacyPageAction::ShowAddRegexModal => {
self.show_add_regex_modal(ctx);
}
PrivacyPageAction::SwitchSecretRedactionTab(tab) => {
self.active_secret_redaction_tab = *tab;
ctx.notify();
}
}
}
}
impl SettingsPageMeta for PrivacyPageView {
fn section() -> SettingsSection {
SettingsSection::Privacy
}
fn should_render(&self, _ctx: &AppContext) -> bool {
true
}
fn update_filter(&mut self, query: &str, ctx: &mut ViewContext<Self>) -> 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<ViewHandle<PrivacyPageView>> for SettingsPageViewHandle {
fn from(view_handle: ViewHandle<PrivacyPageView>) -> Self {
SettingsPageViewHandle::Privacy(view_handle)
}
}
#[derive(Default)]
struct SecretRedactionWidget {
switch_state: SwitchStateHandle,
add_regex_button_mouse_state: MouseStateHandle,
add_recommended_button_mouse_states: RefCell<Vec<MouseStateHandle>>,
add_all_button_mouse_state: MouseStateHandle,
personal_tab_mouse_state: MouseStateHandle,
enterprise_tab_mouse_state: MouseStateHandle,
}
impl SecretRedactionWidget {
/// Ensures there's enough mouse states for the recommended regexes to be added.
fn ensure_recommended_regex_mouse_states(&self, count: usize) {
while self.add_recommended_button_mouse_states.borrow().len() < count {
self.add_recommended_button_mouse_states
.borrow_mut()
.push(Default::default());
}
}
fn render_tab(
&self,
label: String,
count: usize,
tab_type: SecretRedactionTab,
is_active: bool,
mouse_state: MouseStateHandle,
appearance: &Appearance,
) -> Box<dyn Element> {
let theme = appearance.theme();
let (background_fill, text_color, count_color) = if is_active {
(
Some(theme.surface_overlay_1()),
theme.active_ui_text_color().into(),
theme.sub_text_color(theme.surface_2()).into(),
)
} else {
(
None,
theme.sub_text_color(theme.background()).into(),
theme
.sub_text_color(theme.background())
.with_opacity(56)
.into(),
)
};
let hover_background = if !is_active {
Some(appearance.theme().surface_overlay_2())
} else {
None
};
Hoverable::new(mouse_state, move |mouse_state| {
let is_hovered = mouse_state.is_hovered();
let tab_content = Flex::row()
.with_child(
Text::new_inline(label.clone(), appearance.ui_font_family(), FONT_SIZE)
.with_color(text_color)
.finish(),
)
.with_child(
Container::new(
Text::new_inline(
format!(" {count}"),
appearance.ui_font_family(),
FONT_SIZE,
)
.with_color(count_color)
.finish(),
)
.finish(),
)
.finish();
let mut container = Container::new(tab_content)
.with_vertical_padding(9.)
.with_horizontal_padding(12.)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.)));
// Apply background based on hover state
if is_hovered && !is_active {
if let Some(hover_bg) = hover_background {
container = container.with_background(hover_bg);
}
} else if let Some(bg) = background_fill {
container = container.with_background(bg);
}
container.finish()
})
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(PrivacyPageAction::SwitchSecretRedactionTab(tab_type));
})
.with_cursor(Cursor::PointingHand)
.finish()
}
/// Renders the tab bar for switching between Personal and Enterprise views
fn render_tab_bar(
&self,
appearance: &Appearance,
privacy_settings: &PrivacySettings,
active_tab: SecretRedactionTab,
view: &PrivacyPageView,
app: &AppContext,
) -> Box<dyn Element> {
if !privacy_settings.is_enterprise_secret_redaction_enabled() {
return Empty::new().finish();
}
let workspaces = UserWorkspaces::as_ref(app);
let enterprise_regex_list = workspaces.get_enterprise_secret_redaction_regex_list();
let enterprise_count = enterprise_regex_list.len();
// Count personal regexes excluding pending removals
let personal_count = privacy_settings
.user_secret_regex_list
.iter()
.enumerate()
.filter(|(i, _)| !view.pending_regex_removals.contains(i))
.count();
let personal_tab = self.render_tab(
"Personal".to_string(),
personal_count,
SecretRedactionTab::Personal,
active_tab == SecretRedactionTab::Personal,
self.personal_tab_mouse_state.clone(),
appearance,
);
let is_enterprise_tab_active = active_tab == SecretRedactionTab::Enterprise;
let enterprise_tab = self.render_tab(
"Enterprise".to_string(),
enterprise_count,
SecretRedactionTab::Enterprise,
is_enterprise_tab_active,
self.enterprise_tab_mouse_state.clone(),
appearance,
);
let mut row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(personal_tab)
.with_child(enterprise_tab);
if is_enterprise_tab_active {
row.add_child(Shrinkable::new(1., Empty::new().finish()).finish());
row.add_child(self.render_info(
"Enterprise secret redaction cannot be modified.".to_string(),
appearance,
));
}
Container::new(row.finish())
.with_margin_bottom(16.)
.finish()
}
/// Renders a section title with consistent styling
fn render_section_title(&self, title: String, appearance: &Appearance) -> Box<dyn Element> {
Text::new_inline(title, appearance.ui_font_family(), FONT_SIZE)
.with_color(appearance.theme().active_ui_text_color().into())
.finish()
}
/// Renders a description paragraph with consistent styling
fn render_description(
&self,
text: String,
appearance: &Appearance,
margin_bottom: f32,
) -> Box<dyn Element> {
let description_text_color = description_text_color(appearance.theme()).into_solid();
appearance
.ui_builder()
.paragraph(text)
.with_style(UiComponentStyles {
font_color: Some(description_text_color),
margin: Some(
Coords::default()
.top(styles::DESCRIPTION_LINE_MARGIN_BOTTOM)
.bottom(margin_bottom),
),
..Default::default()
})
.build()
.finish()
}
/// Renders a regex item with consistent container styling
fn render_regex_item(
&self,
content: Box<dyn Element>,
appearance: &Appearance,
) -> Box<dyn Element> {
let background = appearance.theme().surface_overlay_1();
Container::new(content)
.with_background(background)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_uniform_padding(8.)
.with_margin_bottom(4.)
.finish()
}
fn horizontal_divider(&self, appearance: &Appearance) -> Box<dyn Element> {
Container::new(
ConstrainedBox::new(
Rect::new()
.with_background(appearance.theme().outline())
.finish(),
)
.with_height(1.)
.finish(),
)
.with_vertical_margin(24.)
.finish()
}
/// Renders regex content using the RegexDisplayInfo trait (supports both user and enterprise regexes)
fn render_regex_content<T: RegexDisplayInfo>(
&self,
regex_info: &T,
appearance: &Appearance,
) -> Box<dyn Element> {
let regex_color = internal_colors::fg_overlay_6(appearance.theme());
if let Some(name) = regex_info.name() {
Flex::column()
.with_child(
Text::new_inline(name.to_string(), appearance.ui_font_family(), FONT_SIZE)
.with_color(appearance.theme().active_ui_text_color().into())
.finish(),
)
.with_child(
Text::new_inline(
regex_info.pattern().to_string(),
appearance.ui_font_family(),
FONT_SIZE,
)
.with_color(regex_color.into())
.finish(),
)
.finish()
} else {
Text::new_inline(
regex_info.pattern().to_string(),
appearance.ui_font_family(),
FONT_SIZE,
)
.with_color(regex_color.into())
.finish()
}
}
/// Renders the enterprise tab content (regexes with title support)
fn render_enterprise_content(
&self,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let workspaces = UserWorkspaces::as_ref(app);
let enterprise_regex_list = workspaces.get_enterprise_secret_redaction_regex_list();
let ui_builder = appearance.ui_builder();
let description_text_color = description_text_color(appearance.theme()).into_solid();
if enterprise_regex_list.is_empty() {
return ui_builder
.paragraph("No enterprise regexes have been configured by your organization.")
.with_style(UiComponentStyles {
font_color: Some(description_text_color),
..Default::default()
})
.build()
.finish();
}
let mut column = Flex::column();
for enterprise_regex in enterprise_regex_list {
let content = self.render_regex_content(&enterprise_regex, appearance);
let item = self.render_regex_item(
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(Expanded::new(1., content).finish())
.finish(),
appearance,
);
column.add_child(item);
}
column.finish()
}
/// Renders the personal tab content (user regexes + recommended regexes)
fn render_personal_content(
&self,
view: &PrivacyPageView,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let privacy_settings = PrivacySettings::as_ref(app);
let ui_builder = appearance.ui_builder();
let workspaces = UserWorkspaces::as_ref(app);
let mut column = Flex::column();
for (i, regex) in privacy_settings.user_secret_regex_list.iter().enumerate() {
if view.pending_regex_removals.contains(&i) {
continue;
}
let text_content = self.render_regex_content(regex, appearance);
let item = self.render_regex_item(
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(Expanded::new(1., text_content).finish())
.with_child(
ui_builder
.close_button(
20., // diameter
view.added_user_secret_regex_list_button_handles[i].clone(),
)
.build()
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(PrivacyPageAction::RemoveCustomRegex(i));
})
.finish(),
)
.finish(),
appearance,
);
column.add_child(item);
}
// Get a list of regexes that are recommended but not currently in use
let enterprise_regex_list_with_titles =
workspaces.get_enterprise_secret_redaction_regex_list();
let current_patterns: Vec<&str> = enterprise_regex_list_with_titles
.iter()
.map(|r| r.pattern.as_str())
.chain(
privacy_settings
.user_secret_regex_list
.iter()
.map(|r| r.pattern().as_str()),
)
.collect();
let recommended_regexes: Vec<_> =
crate::terminal::model::secrets::regexes::DEFAULT_REGEXES_WITH_NAMES
.iter()
.filter(|r| !current_patterns.contains(&r.pattern))
.collect();
if !recommended_regexes.is_empty() {
column.add_child(self.horizontal_divider(appearance));
// Add the "Recommended" header with "Add all" button
column.add_child(
Container::new(
Flex::row()
.with_main_axis_size(MainAxisSize::Max)
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
self.render_section_title("Recommended".to_string(), appearance),
)
.with_child(
Container::new(
ui_builder
.button(
ButtonVariant::Secondary,
self.add_all_button_mouse_state.clone(),
)
.with_text_and_icon_label(Self::add_button(
"Add all", appearance,
))
.with_style(Self::add_button_style())
.build()
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(
PrivacyPageAction::AddAllRecommendedRegexes,
);
})
.finish(),
)
.with_margin_bottom(8.)
.finish(),
)
.finish(),
)
.finish(),
);
self.ensure_recommended_regex_mouse_states(recommended_regexes.len());
let recommended_button_states = self.add_recommended_button_mouse_states.borrow();
for (i, regex) in recommended_regexes.iter().enumerate() {
let text_content = self.render_regex_content(regex, appearance);
let item = self.render_regex_item(
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(Expanded::new(1., text_content).finish())
.with_child(
icon_button(
appearance,
Icon::Plus,
false,
recommended_button_states[i].clone(),
)
.build()
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(PrivacyPageAction::AddRecommendedRegex(
i,
));
})
.finish(),
)
.finish(),
appearance,
);
column.add_child(item);
}
}
column.finish()
}
fn render_info(&self, text: String, appearance: &Appearance) -> Box<dyn Element> {
let info_icon = Container::new(
ConstrainedBox::new(
Icon::Info
.to_galaxyui_icon(
appearance
.theme()
.hint_text_color(appearance.theme().background()),
)
.finish(),
)
.with_width(appearance.ui_font_size() * 1.2)
.with_height(appearance.ui_font_size() * 1.2)
.finish(),
)
.with_padding_right(4.)
.finish();
Flex::row()
.with_child(info_icon)
.with_child(
appearance
.ui_builder()
.span(text)
.with_style(UiComponentStyles {
font_color: Some(
appearance
.theme()
.hint_text_color(appearance.theme().background())
.into_solid(),
),
font_size: Some(FONT_SIZE),
..Default::default()
})
.build()
.finish(),
)
.finish()
}
fn add_button(text: impl Into<Cow<'static, str>>, appearance: &Appearance) -> TextAndIcon {
TextAndIcon::new(
TextAndIconAlignment::IconFirst,
text,
Icon::Plus.to_galaxyui_icon(appearance.theme().active_ui_text_color()),
MainAxisSize::Min,
MainAxisAlignment::SpaceBetween,
vec2f(16., 16.),
)
.with_inner_padding(3.)
}
fn add_button_style() -> UiComponentStyles {
UiComponentStyles {
padding: Some(Coords {
// There's some offset issue with the button component
left: 8.,
right: 12.,
top: 6.,
bottom: 6.,
}),
margin: Some(Coords {
left: 8.,
right: 0.,
top: 0.,
bottom: 0.,
}),
..Default::default()
}
}
}
impl SettingsWidget for SecretRedactionWidget {
type View = PrivacyPageView;
fn search_terms(&self) -> &str {
"secret redaction safe mode hide"
}
fn render(
&self,
view: &Self::View,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let safe_mode_settings = SafeModeSettings::as_ref(app);
let privacy_settings = PrivacySettings::as_ref(app);
let description_text_color = description_text_color(appearance.theme()).into_solid();
let ui_builder = appearance.ui_builder();
let is_enterprise_enabled = privacy_settings.is_enterprise_secret_redaction_enabled();
let local_only_icon_state = LocalOnlyIconState::for_setting(
SafeModeEnabled::storage_key(),
SafeModeEnabled::sync_to_cloud(),
&mut view.local_only_icon_tooltip_states.borrow_mut(),
app,
);
let secret_redaction_title_row = Container::new(
Flex::row()
.with_child(
Shrinkable::new(
1.0,
render_sub_header(appearance, SAFE_MODE_TITLE, Some(local_only_icon_state)),
)
.finish(),
)
.with_child(
Container::new({
if is_enterprise_enabled {
self.render_info(
"Enabled by your organization.".to_string(),
appearance,
)
} else {
ui_builder
.switch(self.switch_state.clone())
.check(*safe_mode_settings.safe_mode_enabled.value())
.build()
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(PrivacyPageAction::ToggleSafeMode)
})
.finish()
}
})
.with_padding_right(TOGGLE_BUTTON_RIGHT_PADDING)
.finish(),
)
.with_cross_axis_alignment(CrossAxisAlignment::Start)
.finish(),
)
.with_padding_bottom(HEADER_PADDING)
.finish();
let mut column = Flex::column()
.with_child(secret_redaction_title_row)
.with_child(
ui_builder
.paragraph((*SAFE_MODE_DESCRIPTION).to_owned())
.with_style(UiComponentStyles {
font_color: Some(description_text_color),
font_size: Some(FONT_SIZE + 1.), // One size up from current 12px to 13px
margin: Some(
Coords::default()
.top(-24.)
.bottom(styles::DESCRIPTION_LINE_MARGIN_BOTTOM),
),
..Default::default()
})
.build()
.finish(),
);
if *safe_mode_settings.safe_mode_enabled {
// Add the secret display mode dropdown
let local_only_icon_state = LocalOnlyIconState::for_setting(
SecretDisplayModeSetting::storage_key(),
SecretDisplayModeSetting::sync_to_cloud(),
&mut view.local_only_icon_tooltip_states.borrow_mut(),
app,
);
// Create the label with local-only icon if needed
let label_with_icon = super::settings_page::render_dropdown_item_label(
"Secret visual redaction mode".to_string(),
None,
local_only_icon_state,
None,
appearance,
);
// Create left column with label and description
let left_content = Flex::column()
.with_child(label_with_icon)
.with_child(
Container::new(
ui_builder
.paragraph(
"Choose how secrets are visually presented in the block list while keeping them searchable. This setting only affects what you see in the block list.",
)
.with_style(UiComponentStyles {
font_color: Some(description_text_color),
margin: Some(
Coords::default()
.top(4.)
.bottom(0.),
),
..Default::default()
})
.build()
.finish()
)
.finish()
)
.finish();
// Create the horizontal row with left content and dropdown
let dropdown_row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Start)
.with_child(
Shrinkable::new(
1.0,
Container::new(left_content)
.with_padding_right(16.) // Space between left content and dropdown
.finish(),
)
.finish(),
)
.with_child(ChildView::new(&view.secret_redaction_display_dropdown).finish())
.finish();
column.add_child(
Container::new(dropdown_row)
.with_margin_top(8.)
.with_margin_bottom(styles::DESCRIPTION_MARGIN_BOTTOM)
.finish(),
);
// User regexes section
column.add_child(
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Start)
.with_child(
Expanded::new(
1.,
Flex::column()
.with_child(self.render_section_title(
USER_SECRET_REGEX_TITLE.to_string(),
appearance,
))
.with_child(self.render_description(
USER_SECRET_REGEX_DESCRIPTION.to_owned(),
appearance,
if privacy_settings.user_secret_regex_list.iter().count() > 0 {
10.
} else {
0.
},
))
.finish(),
)
.finish(),
)
.with_child(
ui_builder
.button(
ButtonVariant::Secondary,
self.add_regex_button_mouse_state.clone(),
)
.with_text_and_icon_label(Self::add_button("Add regex", appearance))
.with_style(Self::add_button_style())
.build()
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(PrivacyPageAction::ShowAddRegexModal);
})
.finish(),
)
.finish(),
);
let workspaces = UserWorkspaces::as_ref(app);
let enterprise_regex_list = workspaces.get_enterprise_secret_redaction_regex_list();
if is_enterprise_enabled && !enterprise_regex_list.is_empty() {
column.add_child(self.render_tab_bar(
appearance,
privacy_settings,
view.active_secret_redaction_tab,
view,
app,
));
}
let tab_content = if is_enterprise_enabled && !enterprise_regex_list.is_empty() {
match view.active_secret_redaction_tab {
SecretRedactionTab::Personal => {
self.render_personal_content(view, appearance, app)
}
SecretRedactionTab::Enterprise => {
self.render_enterprise_content(appearance, app)
}
}
} else {
self.render_personal_content(view, appearance, app)
};
column.add_child(tab_content);
column.add_child(self.horizontal_divider(appearance));
}
Container::new(column.finish())
.with_padding_top(PAGE_PADDING)
.finish()
}
}
#[derive(Default)]
struct AppAnalyticsWidget {
switch_state: SwitchStateHandle,
docs_link_mouse_state: MouseStateHandle,
zdr_badge_mouse_state: MouseStateHandle,
}
impl AppAnalyticsWidget {
fn render_zero_data_retention_badge(&self, appearance: &Appearance) -> Box<dyn Element> {
let ui_builder = appearance.ui_builder();
Hoverable::new(self.zdr_badge_mouse_state.clone(), move |mouse_state| {
let is_hovered = mouse_state.is_hovered();
let theme = appearance.theme();
let background_color = appearance.theme().accent();
let badge = Container::new(
Text::new_inline("ZDR", appearance.ui_font_family(), CONTENT_FONT_SIZE - 2.)
.with_color(theme.active_ui_text_color().into())
.finish(),
)
.with_background(background_color)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(3.)))
.with_uniform_padding(3.)
.with_margin_left(8.)
.finish();
let mut stack = Stack::new().with_child(badge);
if is_hovered {
let tooltip = ui_builder.tool_tip(
"Your administrator has enabled zero data retention for your team. User generated content will never be collected."
.to_string(),
);
stack.add_positioned_child(
tooltip.build().finish(),
OffsetPositioning::offset_from_parent(
vec2f(0., -3.),
ParentOffsetBounds::Unbounded,
ParentAnchor::TopLeft,
ChildAnchor::BottomLeft,
),
);
}
stack.finish()
})
.with_cursor(Cursor::PointingHand)
.finish()
}
fn should_show_zdr_badge(&self, app: &AppContext) -> bool {
let setting = UserWorkspaces::as_ref(app).get_ugc_collection_enablement_setting();
matches!(setting, UgcCollectionEnablementSetting::Disable)
}
}
impl SettingsWidget for AppAnalyticsWidget {
type View = PrivacyPageView;
fn search_terms(&self) -> &str {
"telemetry usage analytics data collection"
}
fn should_render(&self, app: &AppContext) -> bool {
// Builds without a telemetry config (e.g. OpenWarp) cannot ship
// telemetry, so the toggle would be a no-op. Hide it in that case.
if !ChannelState::is_telemetry_available() {
return false;
}
let privacy_settings = PrivacySettings::as_ref(app);
!privacy_settings.is_telemetry_force_enabled()
}
fn render(
&self,
_view: &Self::View,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let privacy_settings = PrivacySettings::as_ref(app);
let ui_builder = appearance.ui_builder();
let description_text_color = description_text_color(appearance.theme()).into_solid();
let is_enterprise = UserWorkspaces::as_ref(app)
.current_workspace()
.is_some_and(|w| w.billing_metadata.customer_type == CustomerType::Enterprise);
// Keep the old description for enterprise users because we do not collect block input/output for them.
let description = if is_enterprise {
TELEMETRY_DESCRIPTION_OLD
} else {
TELEMETRY_DESCRIPTION
};
let org_setting = UserWorkspaces::handle(app)
.as_ref(app)
.get_ugc_collection_enablement_setting();
let (is_toggleable, is_checked) = match org_setting {
UgcCollectionEnablementSetting::Enable => (false, true),
UgcCollectionEnablementSetting::Disable => (false, false),
UgcCollectionEnablementSetting::RespectUserSetting => {
(true, privacy_settings.is_telemetry_enabled)
}
};
let zdr_label_component = if self.should_show_zdr_badge(app) {
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(render_body_item_label::<PrivacyPageAction>(
TELEMETRY_TITLE.into(),
None,
None,
LocalOnlyIconState::Hidden,
is_toggleable.into(),
appearance,
))
.with_child(self.render_zero_data_retention_badge(appearance))
.finish()
} else {
render_body_item_label::<PrivacyPageAction>(
TELEMETRY_TITLE.into(),
None,
None,
LocalOnlyIconState::Hidden,
is_toggleable.into(),
appearance,
)
};
let switch = ui_builder
.switch(self.switch_state.clone())
.check(is_checked);
let switch = if is_toggleable {
switch
.build()
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(PrivacyPageAction::ToggleTelemetry)
})
.finish()
} else {
switch
.with_tooltip(TooltipConfig {
text: "This setting is managed by your organization.".to_string(),
styles: ui_builder.default_tool_tip_styles(),
})
.disable()
.build()
.finish()
};
// Check if user is on free tier to show the AI requirement note
// Fail safe: if billing status is unknown, assume paid (don't show free tier note)
let is_on_paid_plan = UserWorkspaces::as_ref(app)
.current_workspace()
.map(|w| w.billing_metadata.is_user_on_paid_plan())
.unwrap_or(true);
let mut column = Flex::column();
column.add_child(super::settings_page::build_toggle_element(
zdr_label_component,
switch,
appearance,
None,
));
column.add_child(
ui_builder
.paragraph(description)
.with_style(UiComponentStyles {
font_color: Some(description_text_color),
margin: Some(
Coords::default()
.top(styles::DESCRIPTION_NEGATIVE_MARGIN_OFFSET)
.bottom(styles::DESCRIPTION_LINE_MARGIN_BOTTOM),
),
..Default::default()
})
.build()
.finish(),
);
// Show free tier note only for non-paid users
if !is_on_paid_plan {
column.add_child(
ui_builder
.paragraph(TELEMETRY_FREE_TIER_NOTE)
.with_style(UiComponentStyles {
font_color: Some(description_text_color),
margin: Some(
Coords::default().bottom(styles::DESCRIPTION_LINE_MARGIN_BOTTOM),
),
..Default::default()
})
.build()
.finish(),
);
}
column.add_child(
Align::new(
ui_builder
.link(
"Read more about Warp's use of data".into(),
Some(TELEMETRY_DOCS_URL.into()),
None,
self.docs_link_mouse_state.clone(),
)
.soft_wrap(false)
.build()
.with_margin_bottom(styles::DESCRIPTION_MARGIN_BOTTOM)
.finish(),
)
.left()
.finish(),
);
column.finish()
}
}
#[derive(Default)]
struct CrashReportsWidget {
switch_state: SwitchStateHandle,
}
impl SettingsWidget for CrashReportsWidget {
type View = PrivacyPageView;
fn search_terms(&self) -> &str {
"telemetry crash reports stability data collection"
}
fn should_render(&self, app: &AppContext) -> bool {
// Builds without a crash reporting config (e.g. OpenWarp) cannot ship
// crash reports, so the toggle would be a no-op. Hide it in that case.
if !ChannelState::is_crash_reporting_available() {
return false;
}
let privacy_settings = PrivacySettings::as_ref(app);
!privacy_settings.is_telemetry_force_enabled()
}
fn render(
&self,
_view: &Self::View,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let ui_builder = appearance.ui_builder();
let privacy_settings = PrivacySettings::as_ref(app);
Flex::column()
.with_child(render_body_item::<PrivacyPageAction>(
"Send crash reports".into(),
None,
// Crash report state is always synced to cloud, so no need to show local only icon.
LocalOnlyIconState::Hidden,
ToggleState::Enabled,
appearance,
ui_builder
.switch(self.switch_state.clone())
.check(privacy_settings.is_crash_reporting_enabled)
.build()
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(PrivacyPageAction::ToggleCrashReporting)
})
.finish(),
None,
))
.with_child(
ui_builder
.paragraph(
"Crash reports assist with debugging and stability improvements."
.to_owned(),
)
.with_style(UiComponentStyles {
font_color: Some(
appearance
.theme()
.sub_text_color(appearance.theme().surface_2())
.into_solid(),
),
margin: Some(
Coords::default()
.top(styles::DESCRIPTION_NEGATIVE_MARGIN_OFFSET)
.bottom(styles::DESCRIPTION_MARGIN_BOTTOM),
),
..Default::default()
})
.build()
.finish(),
)
.finish()
}
}
#[derive(Default)]
struct NetworkLogWidget {
link_mouse_state: MouseStateHandle,
}
impl SettingsWidget for NetworkLogWidget {
type View = PrivacyPageView;
fn search_terms(&self) -> &str {
"network log audit console data collection"
}
fn render(
&self,
_view: &Self::View,
appearance: &Appearance,
_app: &AppContext,
) -> Box<dyn Element> {
let ui_builder = appearance.ui_builder();
Flex::column()
.with_child(render_body_item::<PrivacyPageAction>(
"Network log console".into(),
None,
// Not rendering a setting, so no need to show local only icon state.
LocalOnlyIconState::Hidden,
ToggleState::Enabled,
appearance,
Empty::new().finish(),
None,
))
.with_child(
ui_builder
.paragraph(
"We've built a native console that allows you to view all communications \
from Warp to external servers to ensure you feel comfortable that your \
work is always kept safe."
.to_owned(),
)
.with_style(UiComponentStyles {
font_color: Some(
appearance
.theme()
.sub_text_color(appearance.theme().surface_2())
.into_solid(),
),
margin: Some(
Coords::default()
.top(styles::DESCRIPTION_NEGATIVE_MARGIN_OFFSET)
.bottom(styles::DESCRIPTION_LINE_MARGIN_BOTTOM),
),
..Default::default()
})
.build()
.finish(),
)
.with_child(
Align::new(
ui_builder
.link(
"View network logging".to_owned(),
None,
Some(Box::new(|ctx| {
ctx.dispatch_typed_action(PrivacyPageAction::LaunchNetworkLogging);
})),
self.link_mouse_state.clone(),
)
.soft_wrap(false)
.build()
.with_margin_bottom(styles::DESCRIPTION_MARGIN_BOTTOM)
.finish(),
)
.left()
.finish(),
)
.finish()
}
}
pub fn init_actions_from_parent_view<T: Action + Clone>(
app: &mut AppContext,
context: &ContextPredicate,
builder: fn(SettingsAction) -> T,
) {
let mut toggle_binding_pairs = vec![
ToggleSettingActionPair::new(
"app analytics",
builder(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleTelemetry,
)),
context,
flags::TELEMETRY_FLAG,
),
ToggleSettingActionPair::new(
"crash reporting",
builder(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleCrashReporting,
)),
context,
flags::CRASH_REPORTING_FLAG,
),
];
toggle_binding_pairs.push(ToggleSettingActionPair::new(
"secret redaction",
builder(SettingsAction::PrivacyPageToggle(
PrivacyPageAction::ToggleSafeMode,
)),
context,
flags::SAFE_MODE_FLAG,
));
ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings(toggle_binding_pairs, app);
}
mod styles {
// Apply a negative margin to the description text so it appears closer to the main
// settings option text.
pub const DESCRIPTION_NEGATIVE_MARGIN_OFFSET: f32 = -8.;
/// The space between a description and the next toggle.
pub const DESCRIPTION_MARGIN_BOTTOM: f32 = 12.;
/// The space between two description lines which are describing the same toggle.
pub const DESCRIPTION_LINE_MARGIN_BOTTOM: f32 = 6.;
}
fn description_text_color(theme: &WarpTheme) -> galaxy_core::ui::theme::Fill {
theme.sub_text_color(theme.surface_2())
}