Compare commits

...
Sign in to create a new pull request.

30 commits

Author SHA1 Message Date
Nate Butler
94102e962c Update global ai setting 2025-07-07 12:15:52 -04:00
Nate Butler
adb2e95e2a Update focus outline styles 2025-07-07 12:15:42 -04:00
Nate Butler
7e22e57e08 Introduce global ai setting 2025-07-07 11:41:55 -04:00
Nate Butler
d30664d4b1 Merge branch 'main' into onboarding-ui 2025-07-07 09:33:18 -04:00
Nate Butler
5b2874b01a WIP use components 2025-07-04 21:56:04 -04:00
Nate Butler
ffb557c2d9 Add additional row components 2025-07-04 21:38:49 -04:00
Nate Butler
6e6e6d86c3 Update selectable_tile.rs 2025-07-04 20:00:55 -04:00
Nate Butler
e32e1baf5f Add SelectableTile 2025-07-04 19:52:55 -04:00
Nate Butler
bdabdbbf91 Use existing welcome page 2025-07-03 15:19:18 -04:00
Nate Butler
fe9ef209a8 Start scaffolding out ai section 2025-07-03 14:04:18 -04:00
Nate Butler
b31af62d42 Refinement 2025-07-03 13:37:49 -04:00
Nate Butler
8fde323dd0 Stub out KeyboardNavigation 2025-07-03 10:07:30 -04:00
Nate Butler
bff1f1c78a Remove unused welcome ui module 2025-07-03 09:47:01 -04:00
Nate Butler
66b36b772f Clean up unused 2025-07-02 10:15:52 -04:00
Nate Butler
3c5d78800b Update keymap logos 2025-07-02 00:22:04 -04:00
Nate Butler
094c2bc32a Tidy up keymap section 2025-07-02 00:02:27 -04:00
Nate Butler
16cfdf7b94 more themes -> launch theme selector 2025-07-01 23:12:21 -04:00
Nate Butler
ff0345bb4f Fix theme tiles 2025-07-01 23:04:35 -04:00
Nate Butler
124ebb872a Start on onboarding button 2025-07-01 08:44:52 -04:00
Nate Butler
530b24258f Add shadow_hairline 2025-07-01 08:03:50 -04:00
Nate Butler
8c446ecd59 wip basics section 2025-06-30 21:44:17 -04:00
Nate Butler
f6407fdff1 Start on keyboard navigation story 2025-06-30 21:08:51 -04:00
Nate Butler
db52799129 wip 2025-06-30 20:17:45 -04:00
Nate Butler
db575d8a04 Style nav items 2025-06-30 19:43:00 -04:00
Nate Butler
63dc4925f6 Onboarding serializaion 2025-06-30 18:00:50 -04:00
Nate Butler
3dfaac0b4d More UI 2025-06-30 16:51:20 -04:00
Nate Butler
4f52cece69 Tidy & set up for pages 2025-06-30 15:37:43 -04:00
Nate Butler
02f54e5958 impl item 2025-06-30 15:32:32 -04:00
Nate Butler
435dad71fb Start scaffolding ui 2025-06-30 15:14:40 -04:00
Nate Butler
ea74f4784a init 2025-06-30 15:02:02 -04:00
33 changed files with 2561 additions and 151 deletions

31
Cargo.lock generated
View file

@ -10814,6 +10814,35 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "onboarding_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"command_palette_hooks",
"component",
"db",
"editor",
"feature_flags",
"gpui",
"language",
"log",
"menu",
"project",
"serde_json",
"settings",
"settings_ui",
"smallvec",
"theme",
"ui",
"util",
"vim_mode_setting",
"welcome",
"workspace",
"zed_actions",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@ -19972,6 +20001,7 @@ dependencies = [
"collab_ui",
"collections",
"command_palette",
"command_palette_hooks",
"component",
"copilot",
"dap",
@ -20022,6 +20052,7 @@ dependencies = [
"nix 0.29.0",
"node_runtime",
"notifications",
"onboarding_ui",
"outline",
"outline_panel",
"parking_lot",

View file

@ -102,6 +102,7 @@ members = [
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
"crates/onboarding_ui",
"crates/open_ai",
"crates/open_router",
"crates/outline",
@ -314,6 +315,7 @@ multi_buffer = { path = "crates/multi_buffer" }
node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
onboarding_ui = { path = "crates/onboarding_ui" }
open_ai = { path = "crates/open_ai" }
open_router = { path = "crates/open_router", features = ["schemars"] }
outline = { path = "crates/outline" }

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,19 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_460_1758)" filter="url(#filter0_d_460_1758)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.53471 10.9653C6.40877 11.2268 6.29908 11.4479 6.19486 11.6718C5.6733 12.7932 5.24127 13.9457 5.0183 15.1667C4.89424 15.8457 4.82642 16.5298 5.00377 17.2115C5.15611 17.7973 5.47314 18.0981 6.01549 18.1229C6.25283 18.134 6.50111 18.0837 6.73221 18.0186C7.34408 17.8453 7.87643 17.5106 8.39127 17.1481C8.51627 17.06 8.64408 17.005 8.80002 17.0629C9.07627 17.1657 9.17627 17.532 8.9433 17.7059C8.10252 18.3323 7.21267 18.8725 6.12314 18.9059C5.2308 18.9334 4.58486 18.4548 4.30017 17.6042C4.08767 16.9695 4.07517 16.3167 4.15314 15.6617C4.38471 13.712 5.13721 11.9406 6.0408 10.2234C6.11846 10.0759 6.12314 9.95871 6.06814 9.80106C5.84252 9.15309 5.63267 8.49949 5.42658 7.84496C5.38752 7.7209 5.34752 7.68559 5.21283 7.71512C4.32752 7.90981 3.45533 8.14746 2.67002 8.61262C2.36674 8.79231 2.0758 9.01543 1.8333 9.27012C1.43002 9.69356 1.42236 10.1622 1.75189 10.6456C2.03939 11.0676 2.44455 11.3562 2.87205 11.617C3.0058 11.6989 3.14689 11.7686 3.28158 11.8489C3.47736 11.9659 3.54439 12.1928 3.44392 12.3815C3.34314 12.5704 3.11705 12.6453 2.91174 12.5396C2.31111 12.2309 1.75096 11.8645 1.30174 11.3504C1.02642 11.0354 0.824706 10.6814 0.757987 10.2617C0.674862 9.73825 0.824862 9.27699 1.13924 8.86262C1.46346 8.43559 1.88924 8.1309 2.35408 7.87778C3.17846 7.42887 4.06674 7.16075 4.97924 6.96309C5.04174 6.94949 5.10361 6.93262 5.1883 6.91153C5.14533 6.66809 5.09924 6.43184 5.06236 6.19387C4.91283 5.22606 4.84392 4.25746 5.08158 3.29278C5.17564 2.91121 5.3233 2.55059 5.58017 2.24668C6.10455 1.62684 6.77892 1.48887 7.53518 1.67356C8.28268 1.85621 8.91033 2.27012 9.49564 2.75184C9.66127 2.88809 9.6658 3.13418 9.53064 3.29278C9.39268 3.45449 9.17814 3.47559 8.99033 3.34762C8.69533 3.14684 8.40846 2.93231 8.10189 2.7509C7.75064 2.54324 7.37346 2.3959 6.95314 2.39387C6.60596 2.39215 6.33611 2.5334 6.1383 2.81465C5.88767 3.17137 5.79814 3.58293 5.7508 4.00496C5.65064 4.89762 5.76955 5.77512 5.94346 6.6484C5.96205 6.74121 5.99767 6.77403 6.09533 6.76418C6.80002 6.69387 7.50486 6.62371 8.21064 6.56481C8.36408 6.55215 8.46424 6.49668 8.55939 6.37637C9.47658 5.21778 10.482 4.14496 11.6649 3.24981C12.3238 2.75121 13.02 2.31653 13.8211 2.07918C14.323 1.93059 14.831 1.87559 15.3364 2.05324C15.9677 2.27512 16.3205 2.75481 16.5024 3.37387C16.6952 4.03106 16.665 4.70106 16.5867 5.3709C16.5653 5.5425 16.5378 5.71329 16.5042 5.88293C16.4603 6.11387 16.2891 6.23621 16.0585 6.20934C15.8488 6.18496 15.7119 6.01637 15.7339 5.78231C15.7644 5.45949 15.838 5.1384 15.8413 4.81621C15.8452 4.43496 15.837 4.04387 15.7577 3.67356C15.5922 2.90356 15.0595 2.60153 14.2716 2.77528C13.6805 2.90543 13.1563 3.18356 12.6572 3.51246C11.632 4.18809 10.7569 5.03293 9.94439 5.94637C9.7933 6.11621 9.64799 6.29137 9.50111 6.46481C9.49221 6.47512 9.49283 6.49356 9.48158 6.53746H9.69424C11.7866 6.55856 13.8481 6.80731 15.8628 7.38715C16.8222 7.6634 17.7483 8.0234 18.6005 8.55356C19.0539 8.83559 19.4702 9.16199 19.7994 9.58793C20.4708 10.4567 20.4144 11.432 19.642 12.21C19.1205 12.7354 18.4781 13.0673 17.7939 13.3251C17.6003 13.3978 17.3872 13.2911 17.3131 13.1056C17.2317 12.9018 17.32 12.6911 17.5272 12.5906C17.8431 12.4375 18.1717 12.3048 18.4705 12.1229C18.7201 11.9716 18.9485 11.7878 19.1497 11.5762C19.568 11.1311 19.5835 10.6445 19.2288 10.1464C18.9631 9.77324 18.6063 9.50043 18.2225 9.26199C17.2756 8.67356 16.2366 8.30903 15.1667 8.0245C14.0514 7.72778 12.9183 7.52074 11.7675 7.43887C10.8505 7.37371 9.93017 7.35496 9.01143 7.31137C8.87877 7.30496 8.78814 7.34418 8.71439 7.45793C8.1758 8.28856 7.63111 9.11528 7.09533 9.94778C7.05627 10.0084 7.03971 10.1175 7.06643 10.1817C7.97346 12.3401 9.12221 14.3548 10.6446 16.1434C11.2855 16.8961 11.9825 17.5922 12.8289 18.1189C13.2261 18.3659 13.6406 18.572 14.1214 18.604C14.557 18.6334 14.8749 18.4511 15.0911 18.0814C15.2949 17.7331 15.3766 17.3478 15.4131 16.9518C15.5313 15.6742 15.2786 14.4418 14.9436 13.2221C14.6005 11.9728 14.1149 10.7762 13.5585 9.60715C13.5136 9.51309 13.4769 9.40137 13.4817 9.30012C13.4906 9.1159 13.636 8.98481 13.8177 8.95637C13.9774 8.93137 14.1455 9.01918 14.2253 9.19043C14.4244 9.61668 14.6264 10.0422 14.8088 10.4757C15.3716 11.815 15.8247 13.189 16.0641 14.6262C16.2311 15.6293 16.311 16.6361 16.0791 17.6409C16.0263 17.8671 15.951 18.0875 15.8542 18.2987C15.4594 19.1618 14.6588 19.5518 13.7311 19.349C12.9769 19.1839 12.3403 18.7898 11.7464 18.319C10.6541 17.4532 9.76783 16.3976 8.96627 15.2667C8.05252 13.9784 7.2808 12.6084 6.63424 11.1676C6.61002 11.114 6.58221 11.062 6.53471 10.9653ZM6.13971 7.53559L6.63908 9.12371C7.03361 8.52153 7.40142 7.96028 7.79705 7.35606L6.13971 7.53559ZM10.5731 11.6353C9.94549 11.6345 9.43471 11.1257 9.43596 10.5026C9.43705 9.86949 9.94721 9.36496 10.5853 9.36512C11.2125 9.36543 11.7222 9.87371 11.7227 10.4987C11.723 11.1311 11.2122 11.6361 10.5731 11.6353Z" fill="#AAAFBB"/>
</g>
<defs>
<filter id="filter0_d_460_1758" x="0.5" y="0.5" width="20" height="21.25" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1.25"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_460_1758"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_460_1758" result="shape"/>
</filter>
<clipPath id="clip0_460_1758">
<rect width="20" height="20" fill="white" transform="translate(0.5 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -25,7 +25,11 @@
// Features that can be globally enabled or disabled
"features": {
// Which edit prediction provider to use.
"edit_prediction_provider": "zed"
"edit_prediction_provider": "zed",
// A globally enable or disable AI features.
//
// This setting supersedes all other settings related to AI features.
"ai_assistance": true
},
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Plex Mono",

View file

@ -33,6 +33,7 @@ use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
WeakEntity, Window, point,
};
use language::language_settings;
use language::{Buffer, Point, Selection, TransactionId};
use language_model::{
ConfigurationError, ConfiguredModel, LanguageModelRegistry, report_assistant_event,
@ -1768,7 +1769,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
_: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<CodeAction>>> {
if !AgentSettings::get_global(cx).enabled {
if !AgentSettings::get_global(cx).enabled || !language_settings::ai_enabled(cx) {
return Task::ready(Ok(Vec::new()));
}

View file

@ -405,6 +405,23 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
self
}
/// Sets the box shadow of the element.
///
/// A hairline shadow is a very thin shadow that is often used
/// to create a subtle depth effect under an element.
#visibility fn shadow_hairline(mut self) -> Self {
use gpui::{BoxShadow, hsla, point, px};
use std::vec;
self.style().box_shadow = Some(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.16),
offset: point(px(0.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
}]);
self
}
/// Sets the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow)
#visibility fn shadow_2xs(mut self) -> Self {

View file

@ -16,8 +16,10 @@ use serde::{
de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
};
use fs::Fs;
use settings::{
ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore,
update_settings_file,
};
use shellexpand;
use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc};
@ -54,11 +56,18 @@ pub fn all_language_settings<'a>(
AllLanguageSettings::get(location, cx)
}
/// Returns whether AI assistance is globally enabled or disabled.
pub fn ai_enabled(cx: &App) -> bool {
all_language_settings(None, cx).ai_assistance
}
/// The settings for all languages.
#[derive(Debug, Clone)]
pub struct AllLanguageSettings {
/// The edit prediction settings.
pub edit_predictions: EditPredictionSettings,
/// Whether AI assistance is enabled.
pub ai_assistance: bool,
pub defaults: LanguageSettings,
languages: HashMap<LanguageName, LanguageSettings>,
pub(crate) file_types: FxHashMap<Arc<str>, GlobSet>,
@ -646,6 +655,8 @@ pub struct CopilotSettingsContent {
pub struct FeaturesContent {
/// Determines which edit prediction provider to use.
pub edit_prediction_provider: Option<EditPredictionProvider>,
/// Whether AI assistance is enabled.
pub ai_assistance: Option<bool>,
}
/// Controls the soft-wrapping behavior in the editor.
@ -1122,6 +1133,26 @@ impl AllLanguageSettings {
pub fn edit_predictions_mode(&self) -> EditPredictionsMode {
self.edit_predictions.mode
}
/// Returns whether AI assistance is enabled.
pub fn is_ai_assistance_enabled(&self) -> bool {
self.ai_assistance
}
/// Sets AI assistance to the specified state and updates the settings file.
pub fn set_ai_assistance(enabled: bool, fs: Arc<dyn Fs>, cx: &mut App) {
let current_state = Self::get_global(cx).ai_assistance;
if current_state == enabled {
return;
}
update_settings_file::<Self>(fs, cx, move |file, _| {
file.features
.get_or_insert(Default::default())
.ai_assistance = Some(enabled);
});
}
}
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
@ -1247,6 +1278,12 @@ impl settings::Settings for AllLanguageSettings {
.map(|settings| settings.enabled_in_text_threads)
.unwrap_or(true);
let mut ai_assistance = default_value
.features
.as_ref()
.and_then(|f| f.ai_assistance)
.unwrap_or(true);
let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
for (language, patterns) in &default_value.file_types {
@ -1268,6 +1305,14 @@ impl settings::Settings for AllLanguageSettings {
edit_prediction_provider = Some(provider);
}
if let Some(user_ai_assistance) = user_settings
.features
.as_ref()
.and_then(|f| f.ai_assistance)
{
ai_assistance = user_ai_assistance;
}
if let Some(edit_predictions) = user_settings.edit_predictions.as_ref() {
edit_predictions_mode = edit_predictions.mode;
enabled_in_text_threads = edit_predictions.enabled_in_text_threads;
@ -1359,6 +1404,7 @@ impl settings::Settings for AllLanguageSettings {
copilot: copilot_settings,
enabled_in_text_threads,
},
ai_assistance,
defaults,
languages,
file_types,

View file

@ -0,0 +1,42 @@
[package]
name = "onboarding_ui"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/onboarding_ui.rs"
[features]
test-support = []
[dependencies]
anyhow.workspace = true
client.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
db.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
project.workspace = true
serde_json.workspace = true
settings.workspace = true
settings_ui.workspace = true
smallvec.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
vim_mode_setting.workspace = true
welcome.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View file

@ -0,0 +1,86 @@
use component::{example_group_with_title, single_example};
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use smallvec::SmallVec;
use ui::{Label, prelude::*};
#[derive(IntoElement, RegisterComponent)]
pub struct CalloutRow {
title: SharedString,
lines: SmallVec<[SharedString; 4]>,
}
impl CalloutRow {
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
lines: SmallVec::new(),
}
}
pub fn line(mut self, line: impl Into<SharedString>) -> Self {
self.lines.push(line.into());
self
}
}
impl RenderOnce for CalloutRow {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div().px_2().child(
v_flex()
.p_3()
.gap_1()
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.child(Label::new(self.title).weight(gpui::FontWeight::MEDIUM))
.children(
self.lines
.into_iter()
.map(|line| Label::new(line).size(LabelSize::Small).color(Color::Muted)),
),
)
}
}
impl Component for CalloutRow {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn sort_name() -> &'static str {
"RowCallout"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let examples = example_group_with_title(
"CalloutRow Examples",
vec![
single_example(
"Privacy Notice",
CalloutRow::new("We don't use your code to train AI models")
.line("You choose which providers you enable, and they have their own privacy policies.")
.line("Read more about our privacy practices in our Privacy Policy.")
.into_any_element(),
),
single_example(
"Single Line",
CalloutRow::new("Important Notice")
.line("This is a single line of information.")
.into_any_element(),
),
single_example(
"Multi Line",
CalloutRow::new("Getting Started")
.line("Welcome to Zed! Here are some things to know:")
.line("• Use Cmd+P to quickly open files")
.line("• Use Cmd+Shift+P to access the command palette")
.line("• Check out the documentation for more tips")
.into_any_element(),
),
],
);
Some(v_flex().p_4().gap_4().child(examples).into_any_element())
}
}

View file

@ -0,0 +1,134 @@
use component::{example_group_with_title, single_example};
use gpui::StatefulInteractiveElement as _;
use gpui::{AnyElement, App, ClickEvent, IntoElement, RenderOnce, Window};
use ui::prelude::*;
#[derive(IntoElement, RegisterComponent)]
pub struct CheckboxRow {
label: SharedString,
description: Option<SharedString>,
checked: bool,
on_click: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
}
impl CheckboxRow {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
description: None,
checked: false,
on_click: None,
}
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_click = Some(Box::new(handler));
self
}
}
impl RenderOnce for CheckboxRow {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let checked = self.checked;
let on_click = self.on_click;
let checkbox = gpui::div()
.w_4()
.h_4()
.rounded_sm()
.border_1()
.border_color(cx.theme().colors().border)
.when(checked, |this| {
this.bg(cx.theme().colors().element_selected)
.border_color(cx.theme().colors().border_selected)
})
.hover(|this| this.bg(cx.theme().colors().element_hover))
.child(gpui::div().when(checked, |this| {
this.size_full()
.flex()
.items_center()
.justify_center()
.child(Icon::new(IconName::Check))
}));
let main_row = if let Some(on_click) = on_click {
gpui::div()
.id("checkbox-row")
.h_flex()
.gap_2()
.items_center()
.child(checkbox)
.child(Label::new(self.label))
.cursor_pointer()
.on_click(move |_event, window, cx| on_click(window, cx))
} else {
gpui::div()
.id("checkbox-row")
.h_flex()
.gap_2()
.items_center()
.child(checkbox)
.child(Label::new(self.label))
};
v_flex()
.px_5()
.py_1()
.gap_1()
.child(main_row)
.when_some(self.description, |this, desc| {
this.child(
gpui::div()
.ml_6()
.child(Label::new(desc).size(LabelSize::Small).color(Color::Muted)),
)
})
}
}
impl Component for CheckboxRow {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn sort_name() -> &'static str {
"RowCheckbox"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let examples = example_group_with_title(
"CheckboxRow Examples",
vec![
single_example(
"Unchecked",
CheckboxRow::new("Enable Vim Mode").into_any_element(),
),
single_example(
"Checked",
CheckboxRow::new("Send Crash Reports")
.checked(true)
.into_any_element(),
),
single_example(
"With Description",
CheckboxRow::new("Send Telemetry")
.description("Help improve Zed by sending anonymous usage data")
.checked(true)
.into_any_element(),
),
],
);
Some(v_flex().p_4().gap_4().child(examples).into_any_element())
}
}

View file

@ -0,0 +1,78 @@
use component::{example_group_with_title, single_example};
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Label, prelude::*};
#[derive(IntoElement, RegisterComponent)]
pub struct HeaderRow {
label: SharedString,
end_slot: Option<AnyElement>,
}
impl HeaderRow {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
end_slot: None,
}
}
pub fn end_slot(mut self, slot: impl IntoElement) -> Self {
self.end_slot = Some(slot.into_any_element());
self
}
}
impl RenderOnce for HeaderRow {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
h_flex()
.h(px(32.))
.w_full()
.px_5()
.justify_between()
.child(Label::new(self.label))
.when_some(self.end_slot, |this, slot| this.child(slot))
}
}
impl Component for HeaderRow {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn sort_name() -> &'static str {
"RowHeader"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let examples = example_group_with_title(
"HeaderRow Examples",
vec![
single_example(
"Simple Header",
HeaderRow::new("Pick a Theme").into_any_element(),
),
single_example(
"Header with Button",
HeaderRow::new("Pick a Theme")
.end_slot(
Button::new("more_themes", "More Themes")
.style(ButtonStyle::Subtle)
.color(Color::Muted),
)
.into_any_element(),
),
single_example(
"Header with Icon Button",
HeaderRow::new("Settings")
.end_slot(
IconButton::new("refresh", IconName::RotateCw)
.style(ButtonStyle::Subtle),
)
.into_any_element(),
),
],
);
Some(v_flex().p_4().gap_4().child(examples).into_any_element())
}
}

View file

@ -0,0 +1,11 @@
mod callout_row;
mod checkbox_row;
mod header_row;
mod selectable_tile;
mod selectable_tile_row;
pub use callout_row::CalloutRow;
pub use checkbox_row::CheckboxRow;
pub use header_row::HeaderRow;
pub use selectable_tile::SelectableTile;
pub use selectable_tile_row::SelectableTileRow;

View file

@ -0,0 +1,165 @@
use component::{example_group_with_title, single_example};
use gpui::{ClickEvent, transparent_black};
use smallvec::SmallVec;
use ui::{Vector, VectorName, prelude::*, utils::CornerSolver};
#[derive(IntoElement, RegisterComponent)]
pub struct SelectableTile {
id: ElementId,
width: DefiniteLength,
height: DefiniteLength,
parent_focused: bool,
selected: bool,
children: SmallVec<[AnyElement; 2]>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
impl SelectableTile {
pub fn new(
id: impl Into<ElementId>,
width: impl Into<DefiniteLength>,
height: impl Into<DefiniteLength>,
) -> Self {
Self {
id: id.into(),
width: width.into(),
height: height.into(),
parent_focused: false,
selected: false,
children: SmallVec::new(),
on_click: None,
}
}
pub fn w(mut self, width: impl Into<DefiniteLength>) -> Self {
self.width = width.into();
self
}
pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
self.height = height.into();
self
}
pub fn parent_focused(mut self, focused: bool) -> Self {
self.parent_focused = focused;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn on_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Box::new(handler));
self
}
}
impl RenderOnce for SelectableTile {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let ring_corner_radius = px(8.);
let ring_width = px(1.);
let padding = px(2.);
let content_border_width = px(0.);
let content_border_radius = CornerSolver::child_radius(
ring_corner_radius,
ring_width,
padding,
content_border_width,
);
let mut element = h_flex()
.id(self.id)
.w(self.width)
.h(self.height)
.overflow_hidden()
.rounded(ring_corner_radius)
.border(ring_width)
.border_color(if self.selected && self.parent_focused {
cx.theme().status().info
} else if self.selected {
cx.theme().colors().border
} else {
transparent_black()
})
.p(padding)
.child(
h_flex()
.size_full()
.rounded(content_border_radius)
.items_center()
.justify_center()
.shadow_hairline()
.bg(cx.theme().colors().surface_background)
.children(self.children),
);
if let Some(on_click) = self.on_click {
element = element.on_click(move |event, window, cx| {
on_click(event, window, cx);
});
}
element
}
}
impl ParentElement for SelectableTile {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl Component for SelectableTile {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let states = example_group(vec![
single_example(
"Default",
SelectableTile::new("default", px(40.), px(40.))
.parent_focused(false)
.selected(false)
.child(div().p_4().child(Vector::new(
VectorName::ZedLogo,
rems(18. / 16.),
rems(18. / 16.),
)))
.into_any_element(),
),
single_example(
"Selected",
SelectableTile::new("selected", px(40.), px(40.))
.parent_focused(false)
.selected(true)
.child(div().p_4().child(Vector::new(
VectorName::ZedLogo,
rems(18. / 16.),
rems(18. / 16.),
)))
.into_any_element(),
),
single_example(
"Selected & Parent Focused",
SelectableTile::new("selected_focused", px(40.), px(40.))
.parent_focused(true)
.selected(true)
.child(div().p_4().child(Vector::new(
VectorName::ZedLogo,
rems(18. / 16.),
rems(18. / 16.),
)))
.into_any_element(),
),
]);
Some(v_flex().p_4().gap_4().child(states).into_any_element())
}
}

View file

@ -0,0 +1,124 @@
use super::selectable_tile::SelectableTile;
use component::{example_group_with_title, single_example};
use gpui::{
AnyElement, App, IntoElement, RenderOnce, StatefulInteractiveElement, Window, prelude::*,
};
use smallvec::SmallVec;
use ui::{Label, prelude::*};
#[derive(IntoElement, RegisterComponent)]
pub struct SelectableTileRow {
gap: Pixels,
tiles: SmallVec<[SelectableTile; 8]>,
}
impl SelectableTileRow {
pub fn new() -> Self {
Self {
gap: px(12.),
tiles: SmallVec::new(),
}
}
pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
self.gap = gap.into();
self
}
pub fn tile(mut self, tile: SelectableTile) -> Self {
self.tiles.push(tile);
self
}
}
impl RenderOnce for SelectableTileRow {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
h_flex().w_full().px_5().gap(self.gap).children(self.tiles)
}
}
impl Component for SelectableTileRow {
fn scope() -> ComponentScope {
ComponentScope::Layout
}
fn sort_name() -> &'static str {
"RowSelectableTile"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let examples = example_group_with_title(
"SelectableTileRow Examples",
vec![
single_example(
"Theme Tiles",
SelectableTileRow::new()
.gap(px(12.))
.tile(
SelectableTile::new("tile1", px(100.), px(80.))
.selected(true)
.child(
div()
.size_full()
.bg(gpui::red())
.flex()
.items_center()
.justify_center()
.child(Label::new("Dark")),
),
)
.tile(
SelectableTile::new("tile2", px(100.), px(80.)).child(
div()
.size_full()
.bg(gpui::green())
.flex()
.items_center()
.justify_center()
.child(Label::new("Light")),
),
)
.tile(
SelectableTile::new("tile3", px(100.), px(80.))
.parent_focused(true)
.child(
div()
.size_full()
.bg(gpui::blue())
.flex()
.items_center()
.justify_center()
.child(Label::new("Auto")),
),
)
.into_any_element(),
),
single_example(
"Icon Tiles",
SelectableTileRow::new()
.gap(px(8.))
.tile(
SelectableTile::new("icon1", px(48.), px(48.))
.selected(true)
.child(Icon::new(IconName::Code).size(IconSize::Medium)),
)
.tile(
SelectableTile::new("icon2", px(48.), px(48.))
.child(Icon::new(IconName::Terminal).size(IconSize::Medium)),
)
.tile(
SelectableTile::new("icon3", px(48.), px(48.))
.child(Icon::new(IconName::FileCode).size(IconSize::Medium)),
)
.tile(
SelectableTile::new("icon4", px(48.), px(48.))
.child(Icon::new(IconName::Settings).size(IconSize::Medium)),
)
.into_any_element(),
),
],
);
Some(v_flex().p_4().gap_4().child(examples).into_any_element())
}
}

View file

@ -0,0 +1,94 @@
use gpui::{FontWeight, *};
use ui::prelude::*;
#[derive(IntoElement)]
pub struct JuicyButton {
base: Div,
label: SharedString,
keybinding: Option<AnyElement>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
impl JuicyButton {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
base: div(),
label: label.into(),
keybinding: None,
on_click: None,
}
}
pub fn keybinding(mut self, keybinding: Option<impl IntoElement>) -> Self {
if let Some(kb) = keybinding {
self.keybinding = Some(kb.into_any_element());
}
self
}
}
impl Clickable for JuicyButton {
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
self.on_click = Some(Box::new(handler));
self
}
fn cursor_style(mut self, style: gpui::CursorStyle) -> Self {
self.base = self.base.cursor(style);
self
}
}
impl RenderOnce for JuicyButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let mut children = vec![
h_flex()
.flex_1()
.items_center()
.justify_center()
.child(
div()
.text_size(px(14.))
.font_weight(FontWeight::MEDIUM)
.text_color(cx.theme().colors().text)
.child(self.label),
)
.into_any_element(),
];
if let Some(keybinding) = self.keybinding {
children.push(
div()
.flex_none()
.bg(gpui::white().opacity(0.2))
.rounded_md()
.px_1()
.child(keybinding)
.into_any_element(),
);
}
self.base
.id("juicy-button")
.w_full()
.h(rems(2.))
.px(rems(1.5))
.rounded(px(6.))
.bg(cx.theme().colors().icon.opacity(0.12))
.shadow_hairline()
.hover(|style| {
style.bg(cx.theme().colors().icon.opacity(0.12)) // Darker blue on hover
})
.active(|style| {
style
.bg(rgb(0x1e40af)) // Even darker on active
.shadow_md()
})
.cursor_pointer()
.flex()
.items_center()
.justify_between()
.when_some(self.on_click, |div, on_click| div.on_click(on_click))
.children(children)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
use anyhow::Result;
use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
use workspace::{WorkspaceDb, WorkspaceId};
define_connection! {
pub static ref ONBOARDING_DB: OnboardingDb<WorkspaceDb> =
&[sql!(
CREATE TABLE onboarding_state (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
current_page TEXT,
completed_pages TEXT,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
)];
}
impl OnboardingDb {
pub async fn save_state(
&self,
item_id: u64,
workspace_id: WorkspaceId,
current_page: String,
completed_pages: String,
) -> Result<()> {
let query =
"INSERT INTO onboarding_state(item_id, workspace_id, current_page, completed_pages)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT DO UPDATE SET
current_page = ?3,
completed_pages = ?4";
self.write(move |conn| {
let mut statement = Statement::prepare(conn, query)?;
let mut next_index = statement.bind(&item_id, 1)?;
next_index = statement.bind(&workspace_id, next_index)?;
next_index = statement.bind(&current_page, next_index)?;
statement.bind(&completed_pages, next_index)?;
statement.exec()
})
.await
}
query! {
pub fn get_state(item_id: u64, workspace_id: WorkspaceId) -> Result<Option<(String, String)>> {
SELECT current_page, completed_pages
FROM onboarding_state
WHERE item_id = ? AND workspace_id = ?
}
}
}

View file

@ -2,13 +2,11 @@
use gpui::{Hsla, Length};
use std::sync::Arc;
use theme::{Theme, ThemeRegistry};
use ui::{
IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius,
};
use ui::{IntoElement, RenderOnce, prelude::*, utils::CornerSolver};
/// Shows a preview of a theme as an abstract illustration
/// of a thumbnail-sized editor.
#[derive(IntoElement, RegisterComponent, Documented)]
#[derive(IntoElement)]
pub struct ThemePreviewTile {
theme: Arc<Theme>,
selected: bool,
@ -36,10 +34,10 @@ impl RenderOnce for ThemePreviewTile {
let root_radius = px(8.0);
let root_border = px(2.0);
let root_padding = px(2.0);
let root_padding = px(0.0);
let child_border = px(1.0);
let inner_radius =
inner_corner_radius(root_radius, root_border, root_padding, child_border);
CornerSolver::child_radius(root_radius, root_border, root_padding, child_border);
let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg);
@ -200,81 +198,3 @@ impl RenderOnce for ThemePreviewTile {
)
}
}
impl Component for ThemePreviewTile {
fn description() -> Option<&'static str> {
Some(Self::DOCS)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let theme_registry = ThemeRegistry::global(cx);
let one_dark = theme_registry.get("One Dark");
let one_light = theme_registry.get("One Light");
let gruvbox_dark = theme_registry.get("Gruvbox Dark");
let gruvbox_light = theme_registry.get("Gruvbox Light");
let themes_to_preview = vec![
one_dark.clone().ok(),
one_light.clone().ok(),
gruvbox_dark.clone().ok(),
gruvbox_light.clone().ok(),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
Some(
v_flex()
.gap_6()
.p_4()
.children({
if let Some(one_dark) = one_dark.ok() {
vec![example_group(vec![
single_example(
"Default",
div()
.w(px(240.))
.h(px(180.))
.child(ThemePreviewTile::new(one_dark.clone(), false, 0.42))
.into_any_element(),
),
single_example(
"Selected",
div()
.w(px(240.))
.h(px(180.))
.child(ThemePreviewTile::new(one_dark, true, 0.42))
.into_any_element(),
),
])]
} else {
vec![]
}
})
.child(
example_group(vec![single_example(
"Default Themes",
h_flex()
.gap_4()
.children(
themes_to_preview
.iter()
.enumerate()
.map(|(i, theme)| {
div().w(px(200.)).h(px(140.)).child(ThemePreviewTile::new(
theme.clone(),
false,
0.42,
))
})
.collect::<Vec<_>>(),
)
.into_any_element(),
)])
.grow(),
)
.into_any_element(),
)
}
}

View file

@ -8,6 +8,7 @@ mod disclosure;
mod divider;
mod dropdown_menu;
mod facepile;
mod focus_outline;
mod group;
mod icon;
mod image;
@ -15,6 +16,7 @@ mod indent_guides;
mod indicator;
mod keybinding;
mod keybinding_hint;
mod keyboard_navigation;
mod label;
mod list;
mod modal;
@ -49,6 +51,7 @@ pub use disclosure::*;
pub use divider::*;
pub use dropdown_menu::*;
pub use facepile::*;
pub use focus_outline::*;
pub use group::*;
pub use icon::*;
pub use image::*;
@ -56,6 +59,7 @@ pub use indent_guides::*;
pub use indicator::*;
pub use keybinding::*;
pub use keybinding_hint::*;
pub use keyboard_navigation::*;
pub use label::*;
pub use list::*;
pub use modal::*;

View file

@ -1,7 +1,4 @@
use gpui::{
AnyElement, App, BoxShadow, IntoElement, ParentElement, RenderOnce, Styled, Window, div, hsla,
point, px,
};
use gpui::{AnyElement, App, IntoElement, ParentElement, RenderOnce, Styled, Window, div};
use theme::ActiveTheme;
use crate::{ElevationIndex, h_flex};
@ -41,11 +38,6 @@ impl RenderOnce for SplitButton {
)
.child(self.right)
.bg(ElevationIndex::Surface.on_elevation_bg(cx))
.shadow(vec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.16),
offset: point(px(0.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
}])
.shadow_hairline()
}
}

View file

@ -0,0 +1,65 @@
use gpui::{
AnyElement, IntoElement, ParentElement, Pixels, RenderOnce, Styled, px, transparent_black,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use crate::{h_flex, utils::CornerSolver};
/// An outline is a stylistic focus indicator that draws a ring around
/// an element with some space between the element and ring.
#[derive(IntoElement)]
pub struct FocusOutline {
corner_radius: Pixels,
border_width: Pixels,
padding: Pixels,
focused: bool,
active: bool,
children: SmallVec<[AnyElement; 2]>,
}
impl FocusOutline {
pub fn new(child_corner_radius: Pixels, focused: bool, offset: Pixels) -> Self {
let ring_width = px(1.);
let corner_radius =
CornerSolver::parent_radius(child_corner_radius, ring_width, offset, px(0.));
Self {
corner_radius,
border_width: ring_width,
padding: offset,
focused,
active: false,
children: SmallVec::new(),
}
}
pub fn active(mut self, active: bool) -> Self {
self.active = active;
self
}
}
impl RenderOnce for FocusOutline {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
let border_color = if self.focused && self.active {
cx.theme().colors().border_focused.opacity(0.48)
} else if self.focused {
cx.theme().colors().border_variant
} else {
transparent_black()
};
h_flex()
.border(self.border_width)
.border_color(border_color)
.rounded(self.corner_radius)
.p(self.padding)
.children(self.children)
}
}
impl ParentElement for FocusOutline {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View file

@ -12,11 +12,13 @@ use crate::prelude::*;
)]
#[strum(serialize_all = "snake_case")]
pub enum VectorName {
AiGrid,
AtomLogo,
DebuggerGrid,
Grid,
SublimeLogo,
ZedLogo,
ZedXCopilot,
Grid,
AiGrid,
DebuggerGrid,
}
impl VectorName {

View file

@ -0,0 +1,59 @@
use gpui::{Focusable, actions};
actions!(
keyboard_navigation,
[NextRow, PreviousRow, NextColumn, PreviousColumn]
);
/// Implement this trait to enable grid-like keyboard navigation for a layout.
///
/// This trait allows you to navigate through a layout of rows with mixed column
/// lengths. In example, a layout might have rows with 5, 1 and 3 columns.
///
/// Moving up or down between rows will focus the first element in the next or previous row.
/// Moving left or right between columns will focus the next or previous element in the same row.
///
/// Wrapping can be enabled via `vertical_wrapping` and `horizontal_wrapping` respectively.
pub trait KeyboardNavigation: Focusable {
fn has_focus(&self) -> bool;
/// The focused row. Always has a value to allow for "focused inactive" states.
fn focused_row(&self) -> usize;
/// The focused column. Always has a value to allow for "focused inactive" states.
fn focused_column(&self) -> usize;
/// Focus the first focusable element in the layout.
fn focus_first(&self);
/// Focus the next row, wrapping back to the first row if necessary.
///
/// Is a no-op if wrapping is not enabled.
fn focus_next_row(&self);
/// Focus the previous row, wrapping back to the last row if necessary.
///
/// Is a no-op if wrapping is not enabled.
fn focus_previous_row(&self);
/// Focus the next column, wrapping back to the first column if necessary.
///
/// Is a no-op if wrapping is not enabled.
fn focus_next_column(&self);
/// Focus the previous column, wrapping back to the last column if necessary.
///
/// Is a no-op if wrapping is not enabled.
fn focus_previous_column(&self);
/// Focus the row at the given index.
fn focus_row_index(&self, index: usize);
/// Focus the column at the given index.
fn focus_column_index(&self, ix: usize);
/// When reaching the last row, should moving down wrap
/// back to the first row, and vice versa?
fn vertical_wrap(&self) -> bool {
false
}
/// When reaching the last column, should moving right wrap
/// back to the first column, and vice versa?
fn horizontal_wrap(&self) -> bool {
false
}
}
pub struct NavigationRow {}
pub struct NavigationColumn {}

View file

@ -10,7 +10,7 @@ mod search_input;
mod with_rem_size;
pub use color_contrast::*;
pub use corner_solver::{CornerSolver, inner_corner_radius};
pub use corner_solver::{CornerSolver, NestedCornerSolver};
pub use format_distance::*;
pub use search_input::*;
pub use with_rem_size::*;

View file

@ -1,61 +1,196 @@
use gpui::Pixels;
/// Calculates the childs content-corner radius for a single nested level.
/// Calculates corner radii for nested elements in both directions.
///
/// child_content_radius = max(0, parent_radius - parent_border - parent_padding + self_border)
/// ## Forward calculation (parent → child)
/// Given a parent's corner radius, calculates the child's corner radius:
/// ```
/// child_radius = max(0, parent_radius - parent_border - parent_padding + child_border)
/// ```
///
/// - parent_radius: outer corner radius of the parent element
/// - parent_border: border width of the parent element
/// - parent_padding: padding of the parent element
/// - self_border: border width of this child element (for content inset)
pub fn inner_corner_radius(
parent_radius: Pixels,
parent_border: Pixels,
parent_padding: Pixels,
self_border: Pixels,
) -> Pixels {
(parent_radius - parent_border - parent_padding + self_border).max(Pixels::ZERO)
}
/// Solver for arbitrarily deep nested corner radii.
///
/// Each nested levels outer border-box radius is:
/// R₀ = max(0, root_radius - root_border - root_padding)
/// Rᵢ = max(0, Rᵢ₋₁ - childᵢ₋₁_border - childᵢ₋₁_padding) for i > 0
pub struct CornerSolver {
root_radius: Pixels,
root_border: Pixels,
root_padding: Pixels,
children: Vec<(Pixels, Pixels)>, // (border, padding)
}
/// ## Inverse calculation (child → parent)
/// Given a child's desired corner radius, calculates the required parent radius:
/// ```
/// parent_radius = child_radius + parent_border + parent_padding - child_border
/// ```
pub struct CornerSolver;
impl CornerSolver {
pub fn new(root_radius: Pixels, root_border: Pixels, root_padding: Pixels) -> Self {
Self {
root_radius,
root_border,
root_padding,
children: Vec::new(),
}
/// Calculates the child's corner radius given the parent's properties.
///
/// # Arguments
/// - `parent_radius`: Outer corner radius of the parent element
/// - `parent_border`: Border width of the parent element
/// - `parent_padding`: Padding of the parent element
/// - `child_border`: Border width of the child element
pub fn child_radius(
parent_radius: Pixels,
parent_border: Pixels,
parent_padding: Pixels,
child_border: Pixels,
) -> Pixels {
(parent_radius - parent_border - parent_padding + child_border).max(Pixels::ZERO)
}
pub fn add_child(mut self, border: Pixels, padding: Pixels) -> Self {
self.children.push((border, padding));
/// Calculates the required parent radius to achieve a desired child radius.
///
/// # Arguments
/// - `child_radius`: Desired corner radius for the child element
/// - `parent_border`: Border width of the parent element
/// - `parent_padding`: Padding of the parent element
/// - `child_border`: Border width of the child element
pub fn parent_radius(
child_radius: Pixels,
parent_border: Pixels,
parent_padding: Pixels,
child_border: Pixels,
) -> Pixels {
child_radius + parent_border + parent_padding - child_border
}
}
/// Builder for calculating corner radii across multiple nested levels.
pub struct NestedCornerSolver {
levels: Vec<Level>,
}
#[derive(Debug, Clone, Copy)]
struct Level {
border: Pixels,
padding: Pixels,
}
impl NestedCornerSolver {
/// Creates a new nested corner solver.
pub fn new() -> Self {
Self { levels: Vec::new() }
}
/// Adds a level to the nesting hierarchy.
///
/// Levels should be added from outermost to innermost.
pub fn add_level(mut self, border: Pixels, padding: Pixels) -> Self {
self.levels.push(Level { border, padding });
self
}
pub fn corner_radius(&self, level: usize) -> Pixels {
if level == 0 {
return (self.root_radius - self.root_border - self.root_padding).max(Pixels::ZERO);
/// Calculates the corner radius at a specific nesting level given the root radius.
///
/// # Arguments
/// - `root_radius`: The outermost corner radius
/// - `level`: The nesting level (0 = first child of root, 1 = grandchild, etc.)
pub fn radius_at_level(&self, root_radius: Pixels, level: usize) -> Pixels {
let mut radius = root_radius;
for i in 0..=level.min(self.levels.len().saturating_sub(1)) {
let current_level = &self.levels[i];
let next_border = if i < self.levels.len() - 1 {
self.levels[i + 1].border
} else {
Pixels::ZERO
};
radius = CornerSolver::child_radius(
radius,
current_level.border,
current_level.padding,
next_border,
);
}
if level >= self.children.len() {
return Pixels::ZERO;
radius
}
/// Calculates the required root radius to achieve a desired radius at a specific level.
///
/// # Arguments
/// - `target_radius`: The desired corner radius at the target level
/// - `target_level`: The nesting level where the target radius should be achieved
pub fn root_radius_for_level(&self, target_radius: Pixels, target_level: usize) -> Pixels {
if target_level >= self.levels.len() {
return target_radius;
}
let mut r = (self.root_radius - self.root_border - self.root_padding).max(Pixels::ZERO);
for i in 0..level {
let (b, p) = self.children[i];
r = (r - b - p).max(Pixels::ZERO);
let mut radius = target_radius;
// Work backwards from the target level to the root
for i in (0..=target_level).rev() {
let current_level = &self.levels[i];
let child_border = if i < self.levels.len() - 1 {
self.levels[i + 1].border
} else {
Pixels::ZERO
};
radius = CornerSolver::parent_radius(
radius,
current_level.border,
current_level.padding,
child_border,
);
}
r
radius
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_forward_calculation() {
let parent_radius = Pixels(20.0);
let parent_border = Pixels(2.0);
let parent_padding = Pixels(8.0);
let child_border = Pixels(1.0);
let child_radius =
CornerSolver::child_radius(parent_radius, parent_border, parent_padding, child_border);
assert_eq!(child_radius, Pixels(11.0)); // 20 - 2 - 8 + 1 = 11
}
#[test]
fn test_inverse_calculation() {
let child_radius = Pixels(11.0);
let parent_border = Pixels(2.0);
let parent_padding = Pixels(8.0);
let child_border = Pixels(1.0);
let parent_radius =
CornerSolver::parent_radius(child_radius, parent_border, parent_padding, child_border);
assert_eq!(parent_radius, Pixels(20.0)); // 11 + 2 + 8 - 1 = 20
}
#[test]
fn test_nested_forward() {
let solver = NestedCornerSolver::new()
.add_level(Pixels(2.0), Pixels(8.0)) // Root level
.add_level(Pixels(1.0), Pixels(4.0)) // First child
.add_level(Pixels(1.0), Pixels(2.0)); // Second child
let root_radius = Pixels(20.0);
assert_eq!(solver.radius_at_level(root_radius, 0), Pixels(11.0)); // 20 - 2 - 8 + 1
assert_eq!(solver.radius_at_level(root_radius, 1), Pixels(7.0)); // 11 - 1 - 4 + 1
assert_eq!(solver.radius_at_level(root_radius, 2), Pixels(4.0)); // 7 - 1 - 2 + 0
}
#[test]
fn test_nested_inverse() {
let solver = NestedCornerSolver::new()
.add_level(Pixels(2.0), Pixels(8.0)) // Root level
.add_level(Pixels(1.0), Pixels(4.0)) // First child
.add_level(Pixels(1.0), Pixels(2.0)); // Second child
let target_radius = Pixels(4.0);
let root_radius = solver.root_radius_for_level(target_radius, 2);
assert_eq!(root_radius, Pixels(20.0));
// Verify by calculating forward
assert_eq!(solver.radius_at_level(root_radius, 2), target_radius);
}
}

View file

@ -23,7 +23,6 @@ pub use multibuffer_hint::*;
mod base_keymap_picker;
mod base_keymap_setting;
mod multibuffer_hint;
mod welcome_ui;
actions!(
welcome,

View file

@ -1 +0,0 @@
mod theme_preview;

View file

@ -21,8 +21,8 @@ path = "src/main.rs"
[dependencies]
activity_indicator.workspace = true
agent.workspace = true
agent_ui.workspace = true
agent_settings.workspace = true
agent_ui.workspace = true
anyhow.workspace = true
askpass.workspace = true
assets.workspace = true
@ -42,6 +42,7 @@ client.workspace = true
collab_ui.workspace = true
collections.workspace = true
command_palette.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
copilot.workspace = true
dap_adapters.workspace = true
@ -70,7 +71,6 @@ gpui = { workspace = true, features = [
"windows-manifest",
] }
gpui_tokio.workspace = true
http_client.workspace = true
image_viewer.workspace = true
indoc.workspace = true
@ -90,13 +90,13 @@ libc.workspace = true
log.workspace = true
markdown.workspace = true
markdown_preview.workspace = true
svg_preview.workspace = true
menu.workspace = true
migrator.workspace = true
mimalloc = { version = "0.1", optional = true }
nix = { workspace = true, features = ["pthread", "signal"] }
node_runtime.workspace = true
notifications.workspace = true
onboarding_ui.workspace = true
outline.workspace = true
outline_panel.workspace = true
parking_lot.workspace = true
@ -125,6 +125,7 @@ smol.workspace = true
snippet_provider.workspace = true
snippets_ui.workspace = true
supermaven.workspace = true
svg_preview.workspace = true
sysinfo.workspace = true
tab_switcher.workspace = true
task.workspace = true

View file

@ -583,6 +583,7 @@ pub fn main() {
collab_ui::init(&app_state, cx);
git_ui::init(cx);
jj_ui::init(cx);
onboarding_ui::init(cx);
feedback::init(cx);
markdown_preview::init(cx);
svg_preview::init(cx);

View file

@ -16,6 +16,7 @@ use assets::Assets;
use breadcrumbs::Breadcrumbs;
use client::zed_urls;
use collections::VecDeque;
use command_palette_hooks::CommandPaletteFilter;
use debugger_ui::debugger_panel::DebugPanel;
use editor::ProposedChangesEditorToolbar;
use editor::{Editor, MultiBuffer};
@ -52,6 +53,7 @@ use settings::{
Settings, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
initial_project_settings_content, initial_tasks_content, update_settings_file,
};
use std::any::TypeId;
use std::path::PathBuf;
use std::sync::atomic::{self, AtomicBool};
use std::{borrow::Cow, path::Path, sync::Arc};
@ -72,7 +74,8 @@ use workspace::{
use workspace::{CloseIntent, CloseWindow, RestoreBanner, with_active_or_new_workspace};
use workspace::{Pane, notifications::DetachAndPromptErr};
use zed_actions::{
OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettings, OpenZedUrl, Quit,
DisableAiAssistance, EnableAiAssistance, OpenAccountSettings, OpenBrowser, OpenDocs,
OpenServerSettings, OpenSettings, OpenZedUrl, Quit,
};
actions!(
@ -215,6 +218,35 @@ pub fn init(cx: &mut App) {
);
});
});
cx.on_action(|_: &EnableAiAssistance, cx| {
with_active_or_new_workspace(cx, |workspace, _, cx| {
let fs = workspace.app_state().fs.clone();
language::language_settings::AllLanguageSettings::set_ai_assistance(true, fs, cx);
});
});
cx.on_action(|_: &DisableAiAssistance, cx| {
with_active_or_new_workspace(cx, |workspace, _, cx| {
let fs = workspace.app_state().fs.clone();
language::language_settings::AllLanguageSettings::set_ai_assistance(false, fs, cx);
});
});
// Filter AI assistance actions based on current state
cx.observe_global::<SettingsStore>(move |cx| {
let ai_enabled =
language::language_settings::all_language_settings(None, cx).is_ai_assistance_enabled();
CommandPaletteFilter::update_global(cx, |filter, _cx| {
if ai_enabled {
filter.hide_action_types(&[TypeId::of::<EnableAiAssistance>()]);
filter.show_action_types([TypeId::of::<DisableAiAssistance>()].iter());
} else {
filter.show_action_types([TypeId::of::<EnableAiAssistance>()].iter());
filter.hide_action_types(&[TypeId::of::<DisableAiAssistance>()]);
}
});
})
.detach();
}
fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {

View file

@ -50,6 +50,10 @@ actions!(
OpenLicenses,
/// Opens the telemetry log.
OpenTelemetryLog,
/// Enables AI assistance features.
EnableAiAssistance,
/// Disables AI assistance features.
DisableAiAssistance,
]
);