Merge remote-tracking branch 'origin/main' into save-conversations

This commit is contained in:
Antonio Scandurra 2023-06-23 09:09:42 +02:00
commit 6f0efec146
112 changed files with 4161 additions and 1653 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

View file

@ -51,6 +51,7 @@ jobs:
rustup set profile minimal rustup set profile minimal
rustup update stable rustup update stable
rustup target add wasm32-wasi rustup target add wasm32-wasi
cargo install cargo-nextest
- name: Install Node - name: Install Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -70,7 +71,7 @@ jobs:
run: cargo check --workspace run: cargo check --workspace
- name: Run tests - name: Run tests
run: cargo test --workspace --no-fail-fast run: cargo nextest run --workspace --no-fail-fast
- name: Build collab - name: Build collab
run: cargo build -p collab run: cargo build -p collab

2
.gitignore vendored
View file

@ -4,6 +4,8 @@
/plugins/bin /plugins/bin
/script/node_modules /script/node_modules
/styles/node_modules /styles/node_modules
/styles/src/types/zed.ts
/crates/theme/schemas/theme.json
/crates/collab/static/styles.css /crates/collab/static/styles.css
/vendor/bin /vendor/bin
/assets/themes/*.json /assets/themes/*.json

131
Cargo.lock generated
View file

@ -192,6 +192,55 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anstream"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is-terminal 0.4.7",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
[[package]]
name = "anstyle-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "anstyle-wincon"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.71" version = "1.0.71"
@ -1104,8 +1153,8 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"clap_derive", "clap_derive 3.2.25",
"clap_lex", "clap_lex 0.2.4",
"indexmap", "indexmap",
"once_cell", "once_cell",
"strsim", "strsim",
@ -1113,6 +1162,30 @@ dependencies = [
"textwrap", "textwrap",
] ]
[[package]]
name = "clap"
version = "4.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2686c4115cb0810d9a984776e197823d08ec94f176549a89a9efded477c456dc"
dependencies = [
"clap_builder",
"clap_derive 4.3.2",
"once_cell",
]
[[package]]
name = "clap_builder"
version = "4.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e53afce1efce6ed1f633cf0e57612fe51db54a1ee4fd8f8503d078fe02d69ae"
dependencies = [
"anstream",
"anstyle",
"bitflags",
"clap_lex 0.5.0",
"strsim",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "3.2.25" version = "3.2.25"
@ -1126,6 +1199,18 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "clap_derive"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.18",
]
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.2.4" version = "0.2.4"
@ -1135,12 +1220,18 @@ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
[[package]]
name = "clap_lex"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]] [[package]]
name = "cli" name = "cli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap 3.2.25",
"core-foundation", "core-foundation",
"core-services", "core-services",
"dirs 3.0.2", "dirs 3.0.2",
@ -1250,7 +1341,7 @@ dependencies = [
"axum-extra", "axum-extra",
"base64 0.13.1", "base64 0.13.1",
"call", "call",
"clap", "clap 3.2.25",
"client", "client",
"collections", "collections",
"ctor", "ctor",
@ -1345,6 +1436,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]] [[package]]
name = "command_palette" name = "command_palette"
version = "0.1.0" version = "0.1.0"
@ -6918,18 +7015,6 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "theme_testbench"
version = "0.1.0"
dependencies = [
"gpui",
"project",
"settings",
"smallvec",
"theme",
"workspace",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.40" version = "1.0.40"
@ -8782,6 +8867,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "xtask"
version = "0.1.0"
dependencies = [
"anyhow",
"clap 4.3.5",
"schemars",
"serde_json",
"theme",
]
[[package]] [[package]]
name = "yaml-rust" name = "yaml-rust"
version = "0.4.5" version = "0.4.5"
@ -8811,7 +8907,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.92.0" version = "0.93.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"ai", "ai",
@ -8890,7 +8986,6 @@ dependencies = [
"text", "text",
"theme", "theme",
"theme_selector", "theme_selector",
"theme_testbench",
"thiserror", "thiserror",
"tiny_http", "tiny_http",
"toml", "toml",

View file

@ -61,11 +61,11 @@ members = [
"crates/text", "crates/text",
"crates/theme", "crates/theme",
"crates/theme_selector", "crates/theme_selector",
"crates/theme_testbench",
"crates/util", "crates/util",
"crates/vim", "crates/vim",
"crates/workspace", "crates/workspace",
"crates/welcome", "crates/welcome",
"crates/xtask",
"crates/zed", "crates/zed",
] ]
default-members = ["crates/zed"] default-members = ["crates/zed"]
@ -118,3 +118,4 @@ split-debuginfo = "unpacked"
[profile.release] [profile.release]
debug = true debug = true
lto = "thin" lto = "thin"
codegen-units = 1

View file

@ -412,6 +412,7 @@
"ctrl-shift-k": "editor::DeleteLine", "ctrl-shift-k": "editor::DeleteLine",
"cmd-shift-d": "editor::DuplicateLine", "cmd-shift-d": "editor::DuplicateLine",
"cmd-shift-l": "editor::SplitSelectionIntoLines", "cmd-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-j": "editor::JoinLines",
"ctrl-cmd-up": "editor::MoveLineUp", "ctrl-cmd-up": "editor::MoveLineUp",
"ctrl-cmd-down": "editor::MoveLineDown", "ctrl-cmd-down": "editor::MoveLineDown",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",

View file

@ -25,11 +25,15 @@
} }
], ],
"h": "vim::Left", "h": "vim::Left",
"left": "vim::Left",
"backspace": "vim::Backspace", "backspace": "vim::Backspace",
"j": "vim::Down", "j": "vim::Down",
"down": "vim::Down",
"enter": "vim::NextLineStart", "enter": "vim::NextLineStart",
"k": "vim::Up", "k": "vim::Up",
"up": "vim::Up",
"l": "vim::Right", "l": "vim::Right",
"right": "vim::Right",
"$": "vim::EndOfLine", "$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument", "shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart", "w": "vim::NextWordStart",
@ -90,6 +94,8 @@
} }
} }
], ],
"ctrl-o": "pane::GoBack",
"ctrl-]": "editor::GoToDefinition",
"escape": "editor::Cancel", "escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion "0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [ "1": [
@ -143,6 +149,7 @@
"Delete" "Delete"
], ],
"shift-d": "vim::DeleteToEndOfLine", "shift-d": "vim::DeleteToEndOfLine",
"shift-j": "editor::JoinLines",
"y": [ "y": [
"vim::PushOperator", "vim::PushOperator",
"Yank" "Yank"
@ -184,7 +191,6 @@
"p": "vim::Paste", "p": "vim::Paste",
"u": "editor::Undo", "u": "editor::Undo",
"ctrl-r": "editor::Redo", "ctrl-r": "editor::Redo",
"ctrl-o": "pane::GoBack",
"/": [ "/": [
"buffer_search::Deploy", "buffer_search::Deploy",
{ {

View file

@ -326,7 +326,7 @@ impl View for ActivityIndicator {
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| { let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar.lsp_status; let theme = &theme::current(cx).workspace.status_bar.lsp_status;
let style = if state.hovered() && on_click.is_some() { let style = if state.hovered() && on_click.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default) theme.hovered.as_ref().unwrap_or(&theme.default)
} else { } else {
&theme.default &theme.default
}; };

View file

@ -309,7 +309,7 @@ impl AssistantPanel {
Some( Some(
MouseEventHandler::<Model, _>::new(0, cx, |state, _| { MouseEventHandler::<Model, _>::new(0, cx, |state, _| {
let style = style.model.style_for(state, false); let style = style.model.style_for(state);
Label::new(model, style.text.clone()) Label::new(model, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -414,7 +414,7 @@ impl AssistantPanel {
.with_style(style.title.container), .with_style(style.title.container),
) )
.contained() .contained()
.with_style(*style.container.style_for(state, false)) .with_style(*style.container.style_for(state))
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
@ -1668,19 +1668,19 @@ impl ConversationEditor {
cx, cx,
|state, _| match message.role { |state, _| match message.role {
Role::User => { Role::User => {
let style = style.user_sender.style_for(state, false); let style = style.user_sender.style_for(state);
Label::new("You", style.text.clone()) Label::new("You", style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
} }
Role::Assistant => { Role::Assistant => {
let style = style.assistant_sender.style_for(state, false); let style = style.assistant_sender.style_for(state);
Label::new("Assistant", style.text.clone()) Label::new("Assistant", style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
} }
Role::System => { Role::System => {
let style = style.system_sender.style_for(state, false); let style = style.system_sender.style_for(state);
Label::new("System", style.text.clone()) Label::new("System", style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)

View file

@ -49,7 +49,7 @@ impl View for UpdateNotification {
) )
.with_child( .with_child(
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| { MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false); let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg") Svg::new("icons/x_mark_8.svg")
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()
@ -74,7 +74,7 @@ impl View for UpdateNotification {
), ),
) )
.with_child({ .with_child({
let style = theme.action_message.style_for(state, false); let style = theme.action_message.style_for(state);
Text::new("View the release notes", style.text.clone()) Text::new("View the release notes", style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)

View file

@ -83,7 +83,7 @@ impl View for Breadcrumbs {
} }
MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| { MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| {
let style = style.style_for(state, false); let style = style.style_for(state);
crumbs.with_style(style.container) crumbs.with_style(style.container)
}) })
.on_click(MouseButton::Left, |_, this, cx| { .on_click(MouseButton::Left, |_, this, cx| {

View file

@ -299,7 +299,12 @@ impl CollabTitlebarItem {
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) { pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
let theme = theme::current(cx).clone(); let theme = theme::current(cx).clone();
let avatar_style = theme.workspace.titlebar.leader_avatar.clone(); let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
let item_style = theme.context_menu.item.disabled_style().clone(); let item_style = theme
.context_menu
.item
.inactive_state()
.disabled_style()
.clone();
self.user_menu.update(cx, |user_menu, cx| { self.user_menu.update(cx, |user_menu, cx| {
let items = if let Some(user) = self.user_store.read(cx).current_user() { let items = if let Some(user) = self.user_store.read(cx).current_user() {
vec![ vec![
@ -361,8 +366,20 @@ impl CollabTitlebarItem {
.contained() .contained()
.with_style(titlebar.toggle_contacts_badge) .with_style(titlebar.toggle_contacts_badge)
.contained() .contained()
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width) .with_margin_left(
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width) titlebar
.toggle_contacts_button
.inactive_state()
.default
.icon_width,
)
.with_margin_top(
titlebar
.toggle_contacts_button
.inactive_state()
.default
.icon_width,
)
.aligned(), .aligned(),
) )
}; };
@ -372,7 +389,8 @@ impl CollabTitlebarItem {
MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| { MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
let style = titlebar let style = titlebar
.toggle_contacts_button .toggle_contacts_button
.style_for(state, self.contacts_popover.is_some()); .in_state(self.contacts_popover.is_some())
.style_for(state);
Svg::new("icons/user_plus_16.svg") Svg::new("icons/user_plus_16.svg")
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()
@ -419,7 +437,7 @@ impl CollabTitlebarItem {
let titlebar = &theme.workspace.titlebar; let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| { MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false); let style = titlebar.call_control.style_for(state);
Svg::new(icon) Svg::new(icon)
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()
@ -473,7 +491,7 @@ impl CollabTitlebarItem {
.with_child( .with_child(
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| { MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistent width for both text variations //TODO: Ensure this button has consistent width for both text variations
let style = titlebar.share_button.style_for(state, false); let style = titlebar.share_button.inactive_state().style_for(state);
Label::new(label, style.text.clone()) Label::new(label, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -511,7 +529,7 @@ impl CollabTitlebarItem {
Stack::new() Stack::new()
.with_child( .with_child(
MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| { MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false); let style = titlebar.call_control.style_for(state);
Svg::new("icons/ellipsis_14.svg") Svg::new("icons/ellipsis_14.svg")
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()
@ -549,7 +567,7 @@ impl CollabTitlebarItem {
fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let titlebar = &theme.workspace.titlebar; let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| { MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
let style = titlebar.sign_in_prompt.style_for(state, false); let style = titlebar.sign_in_prompt.inactive_state().style_for(state);
Label::new("Sign In", style.text.clone()) Label::new("Sign In", style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)

View file

@ -117,7 +117,8 @@ impl PickerDelegate for ContactFinderDelegate {
.contact_finder .contact_finder
.picker .picker
.item .item
.style_for(mouse_state, selected); .in_state(selected)
.style_for(mouse_state);
Flex::row() Flex::row()
.with_children(user.avatar.clone().map(|avatar| { .with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar) Image::from_data(avatar)

View file

@ -774,7 +774,8 @@ impl ContactList {
.with_style( .with_style(
*theme *theme
.contact_row .contact_row
.style_for(&mut Default::default(), is_selected), .in_state(is_selected)
.style_for(&mut Default::default()),
) )
.into_any() .into_any()
} }
@ -797,7 +798,7 @@ impl ContactList {
.width .width
.or(theme.contact_avatar.height) .or(theme.contact_avatar.height)
.unwrap_or(0.); .unwrap_or(0.);
let row = &theme.project_row.default; let row = &theme.project_row.inactive_state().default;
let tree_branch = theme.tree_branch; let tree_branch = theme.tree_branch;
let line_height = row.name.text.line_height(font_cache); let line_height = row.name.text.line_height(font_cache);
let cap_height = row.name.text.cap_height(font_cache); let cap_height = row.name.text.cap_height(font_cache);
@ -810,8 +811,11 @@ impl ContactList {
}; };
MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| { MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected); let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
let row = theme.project_row.style_for(mouse_state, is_selected); let row = theme
.project_row
.in_state(is_selected)
.style_for(mouse_state);
Flex::row() Flex::row()
.with_child( .with_child(
@ -893,7 +897,7 @@ impl ContactList {
.width .width
.or(theme.contact_avatar.height) .or(theme.contact_avatar.height)
.unwrap_or(0.); .unwrap_or(0.);
let row = &theme.project_row.default; let row = &theme.project_row.inactive_state().default;
let tree_branch = theme.tree_branch; let tree_branch = theme.tree_branch;
let line_height = row.name.text.line_height(font_cache); let line_height = row.name.text.line_height(font_cache);
let cap_height = row.name.text.cap_height(font_cache); let cap_height = row.name.text.cap_height(font_cache);
@ -904,8 +908,11 @@ impl ContactList {
peer_id.as_u64() as usize, peer_id.as_u64() as usize,
cx, cx,
|mouse_state, _| { |mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected); let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
let row = theme.project_row.style_for(mouse_state, is_selected); let row = theme
.project_row
.in_state(is_selected)
.style_for(mouse_state);
Flex::row() Flex::row()
.with_child( .with_child(
@ -989,7 +996,8 @@ impl ContactList {
let header_style = theme let header_style = theme
.header_row .header_row
.style_for(&mut Default::default(), is_selected); .in_state(is_selected)
.style_for(&mut Default::default());
let text = match section { let text = match section {
Section::ActiveCall => "Collaborators", Section::ActiveCall => "Collaborators",
Section::Requests => "Contact Requests", Section::Requests => "Contact Requests",
@ -999,7 +1007,7 @@ impl ContactList {
let leave_call = if section == Section::ActiveCall { let leave_call = if section == Section::ActiveCall {
Some( Some(
MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| { MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
let style = theme.leave_call.style_for(state, false); let style = theme.leave_call.style_for(state);
Label::new("Leave Call", style.text.clone()) Label::new("Leave Call", style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -1110,8 +1118,7 @@ impl ContactList {
contact.user.id as usize, contact.user.id as usize,
cx, cx,
|mouse_state, _| { |mouse_state, _| {
let button_style = let button_style = theme.contact_button.style_for(mouse_state);
theme.contact_button.style_for(mouse_state, false);
render_icon_button(button_style, "icons/x_mark_8.svg") render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned() .aligned()
.flex_float() .flex_float()
@ -1146,7 +1153,8 @@ impl ContactList {
.with_style( .with_style(
*theme *theme
.contact_row .contact_row
.style_for(&mut Default::default(), is_selected), .in_state(is_selected)
.style_for(&mut Default::default()),
) )
}) })
.on_click(MouseButton::Left, move |_, this, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
@ -1204,7 +1212,7 @@ impl ContactList {
let button_style = if is_contact_request_pending { let button_style = if is_contact_request_pending {
&theme.disabled_button &theme.disabled_button
} else { } else {
theme.contact_button.style_for(mouse_state, false) theme.contact_button.style_for(mouse_state)
}; };
render_icon_button(button_style, "icons/x_mark_8.svg").aligned() render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
}) })
@ -1227,7 +1235,7 @@ impl ContactList {
let button_style = if is_contact_request_pending { let button_style = if is_contact_request_pending {
&theme.disabled_button &theme.disabled_button
} else { } else {
theme.contact_button.style_for(mouse_state, false) theme.contact_button.style_for(mouse_state)
}; };
render_icon_button(button_style, "icons/check_8.svg") render_icon_button(button_style, "icons/check_8.svg")
.aligned() .aligned()
@ -1250,7 +1258,7 @@ impl ContactList {
let button_style = if is_contact_request_pending { let button_style = if is_contact_request_pending {
&theme.disabled_button &theme.disabled_button
} else { } else {
theme.contact_button.style_for(mouse_state, false) theme.contact_button.style_for(mouse_state)
}; };
render_icon_button(button_style, "icons/x_mark_8.svg") render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned() .aligned()
@ -1277,7 +1285,8 @@ impl ContactList {
.with_style( .with_style(
*theme *theme
.contact_row .contact_row
.style_for(&mut Default::default(), is_selected), .in_state(is_selected)
.style_for(&mut Default::default()),
) )
.into_any() .into_any()
} }

View file

@ -53,7 +53,7 @@ where
) )
.with_child( .with_child(
MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| { MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false); let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg") Svg::new("icons/x_mark_8.svg")
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()
@ -93,7 +93,7 @@ where
.with_children(buttons.into_iter().enumerate().map( .with_children(buttons.into_iter().enumerate().map(
|(ix, (message, handler))| { |(ix, (message, handler))| {
MouseEventHandler::<Button, V>::new(ix, cx, |state, _| { MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
let button = theme.button.style_for(state, false); let button = theme.button.style_for(state);
Label::new(message, button.text.clone()) Label::new(message, button.text.clone())
.contained() .contained()
.with_style(button.container) .with_style(button.container)

View file

@ -185,8 +185,8 @@ impl PickerDelegate for CommandPaletteDelegate {
let mat = &self.matches[ix]; let mat = &self.matches[ix];
let command = &self.actions[mat.candidate_id]; let command = &self.actions[mat.candidate_id];
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let key_style = &theme.command_palette.key.style_for(mouse_state, selected); let key_style = &theme.command_palette.key.in_state(selected);
let keystroke_spacing = theme.command_palette.keystroke_spacing; let keystroke_spacing = theme.command_palette.keystroke_spacing;
Flex::row() Flex::row()

View file

@ -328,10 +328,8 @@ impl ContextMenu {
Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| { Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item { match item {
ContextMenuItem::Item { label, .. } => { ContextMenuItem::Item { label, .. } => {
let style = style.item.style_for( let style = style.item.in_state(self.selected_index == Some(ix));
&mut Default::default(), let style = style.style_for(&mut Default::default());
Some(ix) == self.selected_index,
);
match label { match label {
ContextMenuItemLabel::String(label) => { ContextMenuItemLabel::String(label) => {
@ -363,10 +361,8 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| { .with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item { match item {
ContextMenuItem::Item { action, .. } => { ContextMenuItem::Item { action, .. } => {
let style = style.item.style_for( let style = style.item.in_state(self.selected_index == Some(ix));
&mut Default::default(), let style = style.style_for(&mut Default::default());
Some(ix) == self.selected_index,
);
match action { match action {
ContextMenuItemAction::Action(action) => KeystrokeLabel::new( ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
@ -412,8 +408,8 @@ impl ContextMenu {
let action = action.clone(); let action = action.clone();
let view_id = self.parent_view_id; let view_id = self.parent_view_id;
MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| { MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
let style = let style = style.item.in_state(self.selected_index == Some(ix));
style.item.style_for(state, Some(ix) == self.selected_index); let style = style.style_for(state);
let keystroke = match &action { let keystroke = match &action {
ContextMenuItemAction::Action(action) => Some( ContextMenuItemAction::Action(action) => Some(
KeystrokeLabel::new( KeystrokeLabel::new(

View file

@ -127,16 +127,16 @@ impl CopilotCodeVerification {
.with_child( .with_child(
Label::new( Label::new(
if copied { "Copied!" } else { "Copy" }, if copied { "Copied!" } else { "Copy" },
device_code_style.cta.style_for(state, false).text.clone(), device_code_style.cta.style_for(state).text.clone(),
) )
.aligned() .aligned()
.contained() .contained()
.with_style(*device_code_style.right_container.style_for(state, false)) .with_style(*device_code_style.right_container.style_for(state))
.constrained() .constrained()
.with_width(device_code_style.right), .with_width(device_code_style.right),
) )
.contained() .contained()
.with_style(device_code_style.cta.style_for(state, false).container) .with_style(device_code_style.cta.style_for(state).container)
}) })
.on_click(gpui::platform::MouseButton::Left, { .on_click(gpui::platform::MouseButton::Left, {
let user_code = data.user_code.clone(); let user_code = data.user_code.clone();

View file

@ -71,7 +71,8 @@ impl View for CopilotButton {
.status_bar .status_bar
.panel_buttons .panel_buttons
.button .button
.style_for(state, active); .in_state(active)
.style_for(state);
Flex::row() Flex::row()
.with_child( .with_child(
@ -255,7 +256,7 @@ impl CopilotButton {
move |state: &mut MouseState, style: &theme::ContextMenuItem| { move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row() Flex::row()
.with_child(Label::new("Copilot Settings", style.label.clone())) .with_child(Label::new("Copilot Settings", style.label.clone()))
.with_child(theme::ui::icon(icon_style.style_for(state, false))) .with_child(theme::ui::icon(icon_style.style_for(state)))
.align_children_center() .align_children_center()
.into_any() .into_any()
}, },

View file

@ -1509,7 +1509,8 @@ mod tests {
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
snapshot snapshot
.blocks_in_range(0..snapshot.max_point().row()) .blocks_in_range(0..snapshot.max_point().row())
.filter_map(|(row, block)| { .enumerate()
.filter_map(|(ix, (row, block))| {
let name = match block { let name = match block {
TransformBlock::Custom(block) => block TransformBlock::Custom(block) => block
.render(&mut BlockContext { .render(&mut BlockContext {
@ -1520,6 +1521,7 @@ mod tests {
gutter_width: 0., gutter_width: 0.,
line_height: 0., line_height: 0.,
em_width: 0., em_width: 0.,
block_id: ix,
}) })
.name()? .name()?
.to_string(), .to_string(),

View file

@ -100,7 +100,7 @@ impl View for DiagnosticIndicator {
.workspace .workspace
.status_bar .status_bar
.diagnostic_summary .diagnostic_summary
.style_for(state, false); .style_for(state);
let mut summary_row = Flex::row(); let mut summary_row = Flex::row();
if self.summary.error_count > 0 { if self.summary.error_count > 0 {
@ -198,7 +198,7 @@ impl View for DiagnosticIndicator {
MouseEventHandler::<Message, _>::new(1, cx, |state, _| { MouseEventHandler::<Message, _>::new(1, cx, |state, _| {
Label::new( Label::new(
diagnostic.message.split('\n').next().unwrap().to_string(), diagnostic.message.split('\n').next().unwrap().to_string(),
message_style.style_for(state, false).text.clone(), message_style.style_for(state).text.clone(),
) )
.aligned() .aligned()
.contained() .contained()

View file

@ -88,6 +88,7 @@ pub struct BlockContext<'a, 'b, 'c> {
pub gutter_padding: f32, pub gutter_padding: f32,
pub em_width: f32, pub em_width: f32,
pub line_height: f32, pub line_height: f32,
pub block_id: usize,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]

View file

@ -206,6 +206,7 @@ actions!(
DuplicateLine, DuplicateLine,
MoveLineUp, MoveLineUp,
MoveLineDown, MoveLineDown,
JoinLines,
Transpose, Transpose,
Cut, Cut,
Copy, Copy,
@ -321,6 +322,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::indent); cx.add_action(Editor::indent);
cx.add_action(Editor::outdent); cx.add_action(Editor::outdent);
cx.add_action(Editor::delete_line); cx.add_action(Editor::delete_line);
cx.add_action(Editor::join_lines);
cx.add_action(Editor::delete_to_previous_word_start); cx.add_action(Editor::delete_to_previous_word_start);
cx.add_action(Editor::delete_to_previous_subword_start); cx.add_action(Editor::delete_to_previous_subword_start);
cx.add_action(Editor::delete_to_next_word_end); cx.add_action(Editor::delete_to_next_word_end);
@ -3320,15 +3322,21 @@ impl Editor {
pub fn render_code_actions_indicator( pub fn render_code_actions_indicator(
&self, &self,
style: &EditorStyle, style: &EditorStyle,
active: bool, is_active: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<AnyElement<Self>> { ) -> Option<AnyElement<Self>> {
if self.available_code_actions.is_some() { if self.available_code_actions.is_some() {
enum CodeActions {} enum CodeActions {}
Some( Some(
MouseEventHandler::<CodeActions, _>::new(0, cx, |state, _| { MouseEventHandler::<CodeActions, _>::new(0, cx, |state, _| {
Svg::new("icons/bolt_8.svg") Svg::new("icons/bolt_8.svg").with_color(
.with_color(style.code_actions.indicator.style_for(state, active).color) style
.code_actions
.indicator
.in_state(is_active)
.style_for(state)
.color,
)
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(3.)) .with_padding(Padding::uniform(3.))
@ -3378,10 +3386,8 @@ impl Editor {
.with_color( .with_color(
style style
.indicator .indicator
.style_for( .in_state(fold_status == FoldStatus::Folded)
mouse_state, .style_for(mouse_state)
fold_status == FoldStatus::Folded,
)
.color, .color,
) )
.constrained() .constrained()
@ -3952,6 +3958,60 @@ impl Editor {
}); });
} }
pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
let mut row_ranges = Vec::<Range<u32>>::new();
for selection in self.selections.all::<Point>(cx) {
let start = selection.start.row;
let end = if selection.start.row == selection.end.row {
selection.start.row + 1
} else {
selection.end.row
};
if let Some(last_row_range) = row_ranges.last_mut() {
if start <= last_row_range.end {
last_row_range.end = end;
continue;
}
}
row_ranges.push(start..end);
}
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut cursor_positions = Vec::new();
for row_range in &row_ranges {
let anchor = snapshot.anchor_before(Point::new(
row_range.end - 1,
snapshot.line_len(row_range.end - 1),
));
cursor_positions.push(anchor.clone()..anchor);
}
self.transact(cx, |this, cx| {
for row_range in row_ranges.into_iter().rev() {
for row in row_range.rev() {
let end_of_line = Point::new(row, snapshot.line_len(row));
let indent = snapshot.indent_size_for_line(row + 1);
let start_of_next_line = Point::new(row + 1, indent.len);
let replace = if snapshot.line_len(row + 1) > indent.len {
" "
} else {
""
};
this.buffer.update(cx, |buffer, cx| {
buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
});
}
}
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchor_ranges(cursor_positions)
});
});
}
pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) { pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot; let buffer = &display_map.buffer_snapshot;
@ -7949,6 +8009,7 @@ impl Deref for EditorStyle {
pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock { pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock {
let mut highlighted_lines = Vec::new(); let mut highlighted_lines = Vec::new();
for (index, line) in diagnostic.message.lines().enumerate() { for (index, line) in diagnostic.message.lines().enumerate() {
let line = match &diagnostic.source { let line = match &diagnostic.source {
Some(source) if index == 0 => { Some(source) if index == 0 => {
@ -7960,25 +8021,44 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
}; };
highlighted_lines.push(line); highlighted_lines.push(line);
} }
let message = diagnostic.message;
Arc::new(move |cx: &mut BlockContext| { Arc::new(move |cx: &mut BlockContext| {
let message = message.clone();
let settings = settings::get::<ThemeSettings>(cx); let settings = settings::get::<ThemeSettings>(cx);
let tooltip_style = settings.theme.tooltip.clone();
let theme = &settings.theme.editor; let theme = &settings.theme.editor;
let style = diagnostic_style(diagnostic.severity, is_valid, theme); let style = diagnostic_style(diagnostic.severity, is_valid, theme);
let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
Flex::column() let anchor_x = cx.anchor_x;
.with_children(highlighted_lines.iter().map(|(line, highlights)| { enum BlockContextToolip {}
Label::new( MouseEventHandler::<BlockContext, _>::new(cx.block_id, cx, |_, _| {
line.clone(), Flex::column()
style.message.clone().with_font_size(font_size), .with_children(highlighted_lines.iter().map(|(line, highlights)| {
) Label::new(
.with_highlights(highlights.clone()) line.clone(),
.contained() style.message.clone().with_font_size(font_size),
.with_margin_left(cx.anchor_x) )
})) .with_highlights(highlights.clone())
.aligned() .contained()
.left() .with_margin_left(anchor_x)
.into_any() }))
.aligned()
.left()
.into_any()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
})
// We really need to rethink this ID system...
.with_tooltip::<BlockContextToolip>(
cx.block_id,
"Copy diagnostic message".to_string(),
None,
tooltip_style,
cx,
)
.into_any()
}) })
} }

View file

@ -1,7 +1,10 @@
use super::*; use super::*;
use crate::test::{ use crate::{
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, test::{
editor_test_context::EditorTestContext, select_ranges, assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
editor_test_context::EditorTestContext, select_ranges,
},
JoinLines,
}; };
use drag_and_drop::DragAndDrop; use drag_and_drop::DragAndDrop;
use futures::StreamExt; use futures::StreamExt;
@ -2325,6 +2328,137 @@ fn test_delete_line(cx: &mut TestAppContext) {
}); });
} }
#[gpui::test]
fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
let mut editor = build_editor(buffer.clone(), cx);
let buffer = buffer.read(cx).as_singleton().unwrap();
assert_eq!(
editor.selections.ranges::<Point>(cx),
&[Point::new(0, 0)..Point::new(0, 0)]
);
// When on single line, replace newline at end by space
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
&[Point::new(0, 3)..Point::new(0, 3)]
);
// When multiple lines are selected, remove newlines that are spanned by the selection
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
});
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
&[Point::new(0, 11)..Point::new(0, 11)]
);
// Undo should be transactional
editor.undo(&Undo, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
&[Point::new(0, 5)..Point::new(2, 2)]
);
// When joining an empty line don't insert a space
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
});
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
[Point::new(2, 3)..Point::new(2, 3)]
);
// We can remove trailing newlines
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
assert_eq!(
editor.selections.ranges::<Point>(cx),
[Point::new(2, 3)..Point::new(2, 3)]
);
// We don't blow up on the last line
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
assert_eq!(
editor.selections.ranges::<Point>(cx),
[Point::new(2, 3)..Point::new(2, 3)]
);
// reset to test indentation
editor.buffer.update(cx, |buffer, cx| {
buffer.edit(
[
(Point::new(1, 0)..Point::new(1, 2), " "),
(Point::new(2, 0)..Point::new(2, 3), " \n\td"),
],
None,
cx,
)
});
// We remove any leading spaces
assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
});
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
// We don't insert a space for a line containing only spaces
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
// We ignore any leading tabs
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
editor
});
}
#[gpui::test]
fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
let mut editor = build_editor(buffer.clone(), cx);
let buffer = buffer.read(cx).as_singleton().unwrap();
editor.change_selections(None, cx, |s| {
s.select_ranges([
Point::new(0, 2)..Point::new(1, 1),
Point::new(1, 2)..Point::new(1, 2),
Point::new(3, 1)..Point::new(3, 2),
])
});
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
[
Point::new(0, 7)..Point::new(0, 7),
Point::new(1, 3)..Point::new(1, 3)
]
);
editor
});
}
#[gpui::test] #[gpui::test]
fn test_duplicate_line(cx: &mut TestAppContext) { fn test_duplicate_line(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});

View file

@ -1467,6 +1467,7 @@ impl EditorElement {
editor: &mut Editor, editor: &mut Editor,
cx: &mut LayoutContext<Editor>, cx: &mut LayoutContext<Editor>,
) -> (f32, Vec<BlockLayout>) { ) -> (f32, Vec<BlockLayout>) {
let mut block_id = 0;
let scroll_x = snapshot.scroll_anchor.offset.x(); let scroll_x = snapshot.scroll_anchor.offset.x();
let (fixed_blocks, non_fixed_blocks) = snapshot let (fixed_blocks, non_fixed_blocks) = snapshot
.blocks_in_range(rows.clone()) .blocks_in_range(rows.clone())
@ -1474,7 +1475,7 @@ impl EditorElement {
TransformBlock::ExcerptHeader { .. } => false, TransformBlock::ExcerptHeader { .. } => false,
TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
}); });
let mut render_block = |block: &TransformBlock, width: f32| { let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| {
let mut element = match block { let mut element = match block {
TransformBlock::Custom(block) => { TransformBlock::Custom(block) => {
let align_to = block let align_to = block
@ -1499,6 +1500,7 @@ impl EditorElement {
scroll_x, scroll_x,
gutter_width, gutter_width,
em_width, em_width,
block_id,
}) })
} }
TransformBlock::ExcerptHeader { TransformBlock::ExcerptHeader {
@ -1527,7 +1529,7 @@ impl EditorElement {
enum JumpIcon {} enum JumpIcon {}
MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| { MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
let style = style.jump_icon.style_for(state, false); let style = style.jump_icon.style_for(state);
Svg::new("icons/arrow_up_right_8.svg") Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()
@ -1634,7 +1636,8 @@ impl EditorElement {
let mut fixed_block_max_width = 0f32; let mut fixed_block_max_width = 0f32;
let mut blocks = Vec::new(); let mut blocks = Vec::new();
for (row, block) in fixed_blocks { for (row, block) in fixed_blocks {
let element = render_block(block, f32::INFINITY); let element = render_block(block, f32::INFINITY, block_id);
block_id += 1;
fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width); fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width);
blocks.push(BlockLayout { blocks.push(BlockLayout {
row, row,
@ -1654,7 +1657,8 @@ impl EditorElement {
.max(gutter_width + scroll_width), .max(gutter_width + scroll_width),
BlockStyle::Fixed => unreachable!(), BlockStyle::Fixed => unreachable!(),
}; };
let element = render_block(block, width); let element = render_block(block, width, block_id);
block_id += 1;
blocks.push(BlockLayout { blocks.push(BlockLayout {
row, row,
element, element,
@ -2090,7 +2094,7 @@ impl Element<Editor> for EditorElement {
.folds .folds
.ellipses .ellipses
.background .background
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false) .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
.color; .color;
(id, fold, color) (id, fold, color)

View file

@ -41,7 +41,8 @@ impl View for DeployFeedbackButton {
.status_bar .status_bar
.panel_buttons .panel_buttons
.button .button
.style_for(state, active); .in_state(active)
.style_for(state);
Svg::new("icons/feedback_16.svg") Svg::new("icons/feedback_16.svg")
.with_color(style.icon_color) .with_color(style.icon_color)

View file

@ -48,7 +48,7 @@ impl View for SubmitFeedbackButton {
let theme = theme::current(cx).clone(); let theme = theme::current(cx).clone();
enum SubmitFeedbackButton {} enum SubmitFeedbackButton {}
MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| { MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
let style = theme.feedback.submit_button.style_for(state, false); let style = theme.feedback.submit_button.style_for(state);
Label::new("Submit as Markdown", style.text.clone()) Label::new("Submit as Markdown", style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)

View file

@ -546,7 +546,7 @@ impl PickerDelegate for FileFinderDelegate {
.get(ix) .get(ix)
.expect("Invalid matches state: no element for index {ix}"); .expect("Invalid matches state: no element for index {ix}");
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let (file_name, file_name_positions, full_path, full_path_positions) = let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match, cx, ix); self.labels_for_match(path_match, cx, ix);
Flex::column() Flex::column()

View file

@ -6,15 +6,16 @@ use std::{
use crate::json::ToJson; use crate::json::ToJson;
use pathfinder_color::{ColorF, ColorU}; use pathfinder_color::{ColorF, ColorU};
use schemars::JsonSchema;
use serde::{ use serde::{
de::{self, Unexpected}, de::{self, Unexpected},
Deserialize, Deserializer, Deserialize, Deserializer,
}; };
use serde_json::json; use serde_json::json;
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
#[repr(transparent)] #[repr(transparent)]
pub struct Color(ColorU); pub struct Color(#[schemars(with = "String")] ColorU);
impl Color { impl Color {
pub fn transparent_black() -> Self { pub fn transparent_black() -> Self {

View file

@ -12,10 +12,11 @@ use crate::{
scene::{self, Border, CursorRegion, Quad}, scene::{self, Border, CursorRegion, Quad},
AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
}; };
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
#[derive(Clone, Copy, Debug, Default, Deserialize)] #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
pub struct ContainerStyle { pub struct ContainerStyle {
#[serde(default)] #[serde(default)]
pub margin: Margin, pub margin: Margin,
@ -332,7 +333,7 @@ impl ToJson for ContainerStyle {
} }
} }
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, JsonSchema)]
pub struct Margin { pub struct Margin {
pub top: f32, pub top: f32,
pub left: f32, pub left: f32,
@ -359,7 +360,7 @@ impl ToJson for Margin {
} }
} }
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, JsonSchema)]
pub struct Padding { pub struct Padding {
pub top: f32, pub top: f32,
pub left: f32, pub left: f32,
@ -486,9 +487,10 @@ impl ToJson for Padding {
} }
} }
#[derive(Clone, Copy, Debug, Default, Deserialize)] #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
pub struct Shadow { pub struct Shadow {
#[serde(default, deserialize_with = "deserialize_vec2f")] #[serde(default, deserialize_with = "deserialize_vec2f")]
#[schemars(with = "Vec::<f32>")]
offset: Vector2F, offset: Vector2F,
#[serde(default)] #[serde(default)]
blur: f32, blur: f32,

View file

@ -8,6 +8,7 @@ use crate::{
scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View, scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View,
ViewContext, ViewContext,
}; };
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
@ -21,7 +22,7 @@ pub struct Image {
style: ImageStyle, style: ImageStyle,
} }
#[derive(Copy, Clone, Default, Deserialize)] #[derive(Copy, Clone, Default, Deserialize, JsonSchema)]
pub struct ImageStyle { pub struct ImageStyle {
#[serde(default)] #[serde(default)]
pub border: Border, pub border: Border,

View file

@ -10,6 +10,7 @@ use crate::{
text_layout::{Line, RunStyle}, text_layout::{Line, RunStyle},
Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
}; };
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
@ -20,7 +21,7 @@ pub struct Label {
highlight_indices: Vec<usize>, highlight_indices: Vec<usize>,
} }
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct LabelStyle { pub struct LabelStyle {
pub text: TextStyle, pub text: TextStyle,
pub highlight_text: Option<TextStyle>, pub highlight_text: Option<TextStyle>,

View file

@ -8,6 +8,7 @@ use crate::{
}, },
scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
}; };
use schemars::JsonSchema;
use serde_derive::Deserialize; use serde_derive::Deserialize;
use serde_json::json; use serde_json::json;
use std::{borrow::Cow, ops::Range}; use std::{borrow::Cow, ops::Range};
@ -115,14 +116,14 @@ impl<V: View> Element<V> for Svg {
} }
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct SvgStyle { pub struct SvgStyle {
pub color: Color, pub color: Color,
pub asset: String, pub asset: String,
pub dimensions: Dimensions, pub dimensions: Dimensions,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Dimensions { pub struct Dimensions {
pub width: f32, pub width: f32,
pub height: f32, pub height: f32,

View file

@ -9,6 +9,7 @@ use crate::{
Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View, Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View,
ViewContext, ViewContext,
}; };
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
@ -33,7 +34,7 @@ struct TooltipState {
debounce: RefCell<Option<Task<()>>>, debounce: RefCell<Option<Task<()>>>,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct TooltipStyle { pub struct TooltipStyle {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -42,7 +43,7 @@ pub struct TooltipStyle {
pub max_text_width: Option<f32>, pub max_text_width: Option<f32>,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct KeystrokeStyle { pub struct KeystrokeStyle {
#[serde(flatten)] #[serde(flatten)]
container: ContainerStyle, container: ContainerStyle,

View file

@ -7,13 +7,14 @@ use crate::{
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use schemars::JsonSchema;
use std::{ use std::{
collections::HashMap, collections::HashMap,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::Arc, sync::Arc,
}; };
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
pub struct FamilyId(usize); pub struct FamilyId(usize);
struct Family { struct Family {

View file

@ -16,7 +16,7 @@ use serde::{de, Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::{cell::RefCell, sync::Arc}; use std::{cell::RefCell, sync::Arc};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
pub struct FontId(pub usize); pub struct FontId(pub usize);
pub type GlyphId = u32; pub type GlyphId = u32;
@ -59,20 +59,44 @@ pub struct Features {
pub zero: Option<bool>, pub zero: Option<bool>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, JsonSchema)]
pub struct TextStyle { pub struct TextStyle {
pub color: Color, pub color: Color,
pub font_family_name: Arc<str>, pub font_family_name: Arc<str>,
pub font_family_id: FamilyId, pub font_family_id: FamilyId,
pub font_id: FontId, pub font_id: FontId,
pub font_size: f32, pub font_size: f32,
#[schemars(with = "PropertiesDef")]
pub font_properties: Properties, pub font_properties: Properties,
pub underline: Underline, pub underline: Underline,
} }
#[derive(Copy, Clone, Debug, Default, PartialEq)] #[derive(JsonSchema)]
#[serde(remote = "Properties")]
pub struct PropertiesDef {
/// The font style, as defined in CSS.
pub style: StyleDef,
/// The font weight, as defined in CSS.
pub weight: f32,
/// The font stretchiness, as defined in CSS.
pub stretch: f32,
}
#[derive(JsonSchema)]
#[schemars(remote = "Style")]
pub enum StyleDef {
/// A face that is neither italic not obliqued.
Normal,
/// A form that is generally cursive in nature.
Italic,
/// A typically-sloped version of the regular face.
Oblique,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, JsonSchema)]
pub struct HighlightStyle { pub struct HighlightStyle {
pub color: Option<Color>, pub color: Option<Color>,
#[schemars(with = "Option::<f32>")]
pub weight: Option<Weight>, pub weight: Option<Weight>,
pub italic: Option<bool>, pub italic: Option<bool>,
pub underline: Option<Underline>, pub underline: Option<Underline>,
@ -81,9 +105,10 @@ pub struct HighlightStyle {
impl Eq for HighlightStyle {} impl Eq for HighlightStyle {}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, JsonSchema)]
pub struct Underline { pub struct Underline {
pub color: Option<Color>, pub color: Option<Color>,
#[schemars(with = "f32")]
pub thickness: OrderedFloat<f32>, pub thickness: OrderedFloat<f32>,
pub squiggly: bool, pub squiggly: bool,
} }

View file

@ -25,6 +25,7 @@ use anyhow::{anyhow, bail, Result};
use async_task::Runnable; use async_task::Runnable;
pub use event::*; pub use event::*;
use postage::oneshot; use postage::oneshot;
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use sqlez::{ use sqlez::{
bindable::{Bind, Column, StaticColumnCount}, bindable::{Bind, Column, StaticColumnCount},
@ -282,7 +283,7 @@ pub enum PromptLevel {
Critical, Critical,
} }
#[derive(Copy, Clone, Debug, Deserialize)] #[derive(Copy, Clone, Debug, Deserialize, JsonSchema)]
pub enum CursorStyle { pub enum CursorStyle {
Arrow, Arrow,
ResizeLeftRight, ResizeLeftRight,

View file

@ -3,6 +3,7 @@ mod mouse_region;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
use collections::HashSet; use collections::HashSet;
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};
@ -99,7 +100,7 @@ pub struct Icon {
pub color: Color, pub color: Color,
} }
#[derive(Clone, Copy, Default, Debug)] #[derive(Clone, Copy, Default, Debug, JsonSchema)]
pub struct Border { pub struct Border {
pub width: f32, pub width: f32,
pub color: Color, pub color: Color,

View file

@ -55,7 +55,7 @@ impl View for ActiveBufferLanguage {
MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| { MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar; let theme = &theme::current(cx).workspace.status_bar;
let style = theme.active_language.style_for(state, false); let style = theme.active_language.style_for(state);
Label::new(active_language_text, style.text.clone()) Label::new(active_language_text, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)

View file

@ -180,7 +180,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx); let theme = theme::current(cx);
let mat = &self.matches[ix]; let mat = &self.matches[ix];
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
let mut label = mat.string.clone(); let mut label = mat.string.clone();
if buffer_language_name.as_deref() == Some(mat.string.as_str()) { if buffer_language_name.as_deref() == Some(mat.string.as_str()) {

View file

@ -681,7 +681,7 @@ impl LspLogToolbarItemView {
) )
}) })
.unwrap_or_else(|| "No server selected".into()); .unwrap_or_else(|| "No server selected".into());
let style = theme.toolbar_dropdown_menu.header.style_for(state, false); let style = theme.toolbar_dropdown_menu.header.style_for(state);
Label::new(label, style.text.clone()) Label::new(label, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -722,7 +722,8 @@ impl LspLogToolbarItemView {
let style = theme let style = theme
.toolbar_dropdown_menu .toolbar_dropdown_menu
.item .item
.style_for(state, logs_selected); .in_state(logs_selected)
.style_for(state);
Label::new(SERVER_LOGS, style.text.clone()) Label::new(SERVER_LOGS, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -739,7 +740,8 @@ impl LspLogToolbarItemView {
let style = theme let style = theme
.toolbar_dropdown_menu .toolbar_dropdown_menu
.item .item
.style_for(state, rpc_trace_selected); .in_state(rpc_trace_selected)
.style_for(state);
Flex::row() Flex::row()
.with_child( .with_child(
Label::new(RPC_MESSAGES, style.text.clone()) Label::new(RPC_MESSAGES, style.text.clone())

View file

@ -565,7 +565,7 @@ impl SyntaxTreeToolbarItemView {
) -> impl Element<Self> { ) -> impl Element<Self> {
enum ToggleMenu {} enum ToggleMenu {}
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| { MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
let style = theme.toolbar_dropdown_menu.header.style_for(state, false); let style = theme.toolbar_dropdown_menu.header.style_for(state);
Flex::row() Flex::row()
.with_child( .with_child(
Label::new(active_layer.language.name().to_string(), style.text.clone()) Label::new(active_layer.language.name().to_string(), style.text.clone())
@ -601,7 +601,8 @@ impl SyntaxTreeToolbarItemView {
let style = theme let style = theme
.toolbar_dropdown_menu .toolbar_dropdown_menu
.item .item
.style_for(state, is_selected); .in_state(is_selected)
.style_for(state);
Flex::row() Flex::row()
.with_child( .with_child(
Label::new(layer.language.name().to_string(), style.text.clone()) Label::new(layer.language.name().to_string(), style.text.clone())

View file

@ -33,7 +33,7 @@ const JSON_RPC_VERSION: &str = "2.0";
const CONTENT_LEN_HEADER: &str = "Content-Length: "; const CONTENT_LEN_HEADER: &str = "Content-Length: ";
type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>; type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>; type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
type IoHandler = Box<dyn Send + FnMut(bool, &str)>; type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
pub struct LanguageServer { pub struct LanguageServer {
@ -302,9 +302,9 @@ impl LanguageServer {
if let Some(error) = error { if let Some(error) = error {
handler(Err(error)); handler(Err(error));
} else if let Some(result) = result { } else if let Some(result) = result {
handler(Ok(result.get())); handler(Ok(result.get().into()));
} else { } else {
handler(Ok("null")); handler(Ok("null".into()));
} }
} }
} else { } else {
@ -457,11 +457,13 @@ impl LanguageServer {
let response_handlers = self.response_handlers.clone(); let response_handlers = self.response_handlers.clone();
let next_id = AtomicUsize::new(self.next_id.load(SeqCst)); let next_id = AtomicUsize::new(self.next_id.load(SeqCst));
let outbound_tx = self.outbound_tx.clone(); let outbound_tx = self.outbound_tx.clone();
let executor = self.executor.clone();
let mut output_done = self.output_done_rx.lock().take().unwrap(); let mut output_done = self.output_done_rx.lock().take().unwrap();
let shutdown_request = Self::request_internal::<request::Shutdown>( let shutdown_request = Self::request_internal::<request::Shutdown>(
&next_id, &next_id,
&response_handlers, &response_handlers,
&outbound_tx, &outbound_tx,
&executor,
(), (),
); );
let exit = Self::notify_internal::<notification::Exit>(&outbound_tx, ()); let exit = Self::notify_internal::<notification::Exit>(&outbound_tx, ());
@ -658,6 +660,7 @@ impl LanguageServer {
&self.next_id, &self.next_id,
&self.response_handlers, &self.response_handlers,
&self.outbound_tx, &self.outbound_tx,
&self.executor,
params, params,
) )
} }
@ -666,6 +669,7 @@ impl LanguageServer {
next_id: &AtomicUsize, next_id: &AtomicUsize,
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>, response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
outbound_tx: &channel::Sender<String>, outbound_tx: &channel::Sender<String>,
executor: &Arc<executor::Background>,
params: T::Params, params: T::Params,
) -> impl 'static + Future<Output = Result<T::Result>> ) -> impl 'static + Future<Output = Result<T::Result>>
where where
@ -686,15 +690,20 @@ impl LanguageServer {
.as_mut() .as_mut()
.ok_or_else(|| anyhow!("server shut down")) .ok_or_else(|| anyhow!("server shut down"))
.map(|handlers| { .map(|handlers| {
let executor = executor.clone();
handlers.insert( handlers.insert(
id, id,
Box::new(move |result| { Box::new(move |result| {
let response = match result { executor
Ok(response) => serde_json::from_str(response) .spawn(async move {
.context("failed to deserialize response"), let response = match result {
Err(error) => Err(anyhow!("{}", error.message)), Ok(response) => serde_json::from_str(&response)
}; .context("failed to deserialize response"),
let _ = tx.send(response); Err(error) => Err(anyhow!("{}", error.message)),
};
let _ = tx.send(response);
})
.detach();
}), }),
); );
}); });

View file

@ -204,7 +204,7 @@ impl PickerDelegate for OutlineViewDelegate {
cx: &AppContext, cx: &AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix]; let string_match = &self.matches[ix];
let outline_item = &self.outline.items[string_match.candidate_id]; let outline_item = &self.outline.items[string_match.candidate_id];

View file

@ -1254,7 +1254,10 @@ impl ProjectPanel {
let show_editor = details.is_editing && !details.is_processing; let show_editor = details.is_editing && !details.is_processing;
MouseEventHandler::<Self, _>::new(entry_id.to_usize(), cx, |state, cx| { MouseEventHandler::<Self, _>::new(entry_id.to_usize(), cx, |state, cx| {
let mut style = entry_style.style_for(state, details.is_selected).clone(); let mut style = entry_style
.in_state(details.is_selected)
.style_for(state)
.clone();
if cx if cx
.global::<DragAndDrop<Workspace>>() .global::<DragAndDrop<Workspace>>()
@ -1265,7 +1268,7 @@ impl ProjectPanel {
.filter(|destination| details.path.starts_with(destination)) .filter(|destination| details.path.starts_with(destination))
.is_some() .is_some()
{ {
style = entry_style.active.clone().unwrap(); style = entry_style.active_state().default.clone();
} }
let row_container_style = if show_editor { let row_container_style = if show_editor {
@ -1406,9 +1409,11 @@ impl View for ProjectPanel {
let button_style = theme.open_project_button.clone(); let button_style = theme.open_project_button.clone();
let context_menu_item_style = theme::current(cx).context_menu.item.clone(); let context_menu_item_style = theme::current(cx).context_menu.item.clone();
move |state, cx| { move |state, cx| {
let button_style = button_style.style_for(state, false).clone(); let button_style = button_style.style_for(state).clone();
let context_menu_item = let context_menu_item = context_menu_item_style
context_menu_item_style.style_for(state, true).clone(); .active_state()
.style_for(state)
.clone();
theme::ui::keystroke_label( theme::ui::keystroke_label(
"Open a project", "Open a project",

View file

@ -196,7 +196,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx); let theme = theme::current(cx);
let style = &theme.picker.item; let style = &theme.picker.item;
let current_style = style.style_for(mouse_state, selected); let current_style = style.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix]; let string_match = &self.matches[ix];
let symbol = &self.symbols[string_match.candidate_id]; let symbol = &self.symbols[string_match.candidate_id];
@ -229,7 +229,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
.with_child( .with_child(
// Avoid styling the path differently when it is selected, since // Avoid styling the path differently when it is selected, since
// the symbol's syntax highlighting doesn't change when selected. // the symbol's syntax highlighting doesn't change when selected.
Label::new(path.to_string(), style.default.label.clone()), Label::new(
path.to_string(),
style.inactive_state().default.label.clone(),
),
) )
.contained() .contained()
.with_style(current_style.container) .with_style(current_style.container)

View file

@ -173,7 +173,7 @@ impl PickerDelegate for RecentProjectsDelegate {
cx: &gpui::AppContext, cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix]; let string_match = &self.matches[ix];

View file

@ -328,7 +328,11 @@ impl BufferSearchBar {
Some( Some(
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| { MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, is_active); let style = theme
.search
.option_button
.in_state(is_active)
.style_for(state);
Label::new(icon, style.text.clone()) Label::new(icon, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -371,7 +375,7 @@ impl BufferSearchBar {
enum NavButton {} enum NavButton {}
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| { MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, false); let style = theme.search.option_button.inactive_state().style_for(state);
Label::new(icon, style.text.clone()) Label::new(icon, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -403,7 +407,7 @@ impl BufferSearchBar {
enum CloseButton {} enum CloseButton {}
MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| { MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false); let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg") Svg::new("icons/x_mark_8.svg")
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()

View file

@ -896,7 +896,7 @@ impl ProjectSearchBar {
enum NavButton {} enum NavButton {}
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| { MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, false); let style = theme.search.option_button.inactive_state().style_for(state);
Label::new(icon, style.text.clone()) Label::new(icon, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -927,7 +927,11 @@ impl ProjectSearchBar {
let is_active = self.is_option_enabled(option, cx); let is_active = self.is_option_enabled(option, cx);
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| { MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, is_active); let style = theme
.search
.option_button
.in_state(is_active)
.style_for(state);
Label::new(icon, style.text.clone()) Label::new(icon, style.text.clone())
.contained() .contained()
.with_style(style.container) .with_style(style.container)

View file

@ -8,6 +8,7 @@ use gpui::{
fonts::{HighlightStyle, TextStyle}, fonts::{HighlightStyle, TextStyle},
platform, AppContext, AssetSource, Border, MouseState, platform, AppContext, AssetSource, Border, MouseState,
}; };
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value; use serde_json::Value;
use settings::SettingsStore; use settings::SettingsStore;
@ -36,7 +37,7 @@ pub fn init(source: impl AssetSource, cx: &mut AppContext) {
.detach(); .detach();
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct Theme { pub struct Theme {
#[serde(default)] #[serde(default)]
pub meta: ThemeMeta, pub meta: ThemeMeta,
@ -67,7 +68,7 @@ pub struct Theme {
pub color_scheme: ColorScheme, pub color_scheme: ColorScheme,
} }
#[derive(Deserialize, Default, Clone)] #[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct ThemeMeta { pub struct ThemeMeta {
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
pub id: usize, pub id: usize,
@ -75,7 +76,7 @@ pub struct ThemeMeta {
pub is_light: bool, pub is_light: bool,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct Workspace { pub struct Workspace {
pub background: Color, pub background: Color,
pub blank_pane: BlankPaneStyle, pub blank_pane: BlankPaneStyle,
@ -102,7 +103,7 @@ pub struct Workspace {
pub drop_target_overlay_color: Color, pub drop_target_overlay_color: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct BlankPaneStyle { pub struct BlankPaneStyle {
pub logo: SvgStyle, pub logo: SvgStyle,
pub logo_shadow: SvgStyle, pub logo_shadow: SvgStyle,
@ -112,7 +113,7 @@ pub struct BlankPaneStyle {
pub keyboard_hint_width: f32, pub keyboard_hint_width: f32,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Titlebar { pub struct Titlebar {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -128,16 +129,16 @@ pub struct Titlebar {
pub leader_avatar: AvatarStyle, pub leader_avatar: AvatarStyle,
pub follower_avatar: AvatarStyle, pub follower_avatar: AvatarStyle,
pub inactive_avatar_grayscale: bool, pub inactive_avatar_grayscale: bool,
pub sign_in_prompt: Interactive<ContainedText>, pub sign_in_prompt: Toggleable<Interactive<ContainedText>>,
pub outdated_warning: ContainedText, pub outdated_warning: ContainedText,
pub share_button: Interactive<ContainedText>, pub share_button: Toggleable<Interactive<ContainedText>>,
pub call_control: Interactive<IconButton>, pub call_control: Interactive<IconButton>,
pub toggle_contacts_button: Interactive<IconButton>, pub toggle_contacts_button: Toggleable<Interactive<IconButton>>,
pub user_menu_button: Interactive<IconButton>, pub user_menu_button: Toggleable<Interactive<IconButton>>,
pub toggle_contacts_badge: ContainerStyle, pub toggle_contacts_badge: ContainerStyle,
} }
#[derive(Copy, Clone, Deserialize, Default)] #[derive(Copy, Clone, Deserialize, Default, JsonSchema)]
pub struct AvatarStyle { pub struct AvatarStyle {
#[serde(flatten)] #[serde(flatten)]
pub image: ImageStyle, pub image: ImageStyle,
@ -145,14 +146,14 @@ pub struct AvatarStyle {
pub outer_corner_radius: f32, pub outer_corner_radius: f32,
} }
#[derive(Deserialize, Default, Clone)] #[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct Copilot { pub struct Copilot {
pub out_link_icon: Interactive<IconStyle>, pub out_link_icon: Interactive<IconStyle>,
pub modal: ModalStyle, pub modal: ModalStyle,
pub auth: CopilotAuth, pub auth: CopilotAuth,
} }
#[derive(Deserialize, Default, Clone)] #[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct CopilotAuth { pub struct CopilotAuth {
pub content_width: f32, pub content_width: f32,
pub prompting: CopilotAuthPrompting, pub prompting: CopilotAuthPrompting,
@ -162,14 +163,14 @@ pub struct CopilotAuth {
pub header: IconStyle, pub header: IconStyle,
} }
#[derive(Deserialize, Default, Clone)] #[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct CopilotAuthPrompting { pub struct CopilotAuthPrompting {
pub subheading: ContainedText, pub subheading: ContainedText,
pub hint: ContainedText, pub hint: ContainedText,
pub device_code: DeviceCode, pub device_code: DeviceCode,
} }
#[derive(Deserialize, Default, Clone)] #[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct DeviceCode { pub struct DeviceCode {
pub text: TextStyle, pub text: TextStyle,
pub cta: ButtonStyle, pub cta: ButtonStyle,
@ -179,19 +180,19 @@ pub struct DeviceCode {
pub right_container: Interactive<ContainerStyle>, pub right_container: Interactive<ContainerStyle>,
} }
#[derive(Deserialize, Default, Clone)] #[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct CopilotAuthNotAuthorized { pub struct CopilotAuthNotAuthorized {
pub subheading: ContainedText, pub subheading: ContainedText,
pub warning: ContainedText, pub warning: ContainedText,
} }
#[derive(Deserialize, Default, Clone)] #[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct CopilotAuthAuthorized { pub struct CopilotAuthAuthorized {
pub subheading: ContainedText, pub subheading: ContainedText,
pub hint: ContainedText, pub hint: ContainedText,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ContactsPopover { pub struct ContactsPopover {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -199,17 +200,17 @@ pub struct ContactsPopover {
pub width: f32, pub width: f32,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ContactList { pub struct ContactList {
pub user_query_editor: FieldEditor, pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32, pub user_query_editor_height: f32,
pub add_contact_button: IconButton, pub add_contact_button: IconButton,
pub header_row: Interactive<ContainedText>, pub header_row: Toggleable<Interactive<ContainedText>>,
pub leave_call: Interactive<ContainedText>, pub leave_call: Interactive<ContainedText>,
pub contact_row: Interactive<ContainerStyle>, pub contact_row: Toggleable<Interactive<ContainerStyle>>,
pub row_height: f32, pub row_height: f32,
pub project_row: Interactive<ProjectRow>, pub project_row: Toggleable<Interactive<ProjectRow>>,
pub tree_branch: Interactive<TreeBranch>, pub tree_branch: Toggleable<Interactive<TreeBranch>>,
pub contact_avatar: ImageStyle, pub contact_avatar: ImageStyle,
pub contact_status_free: ContainerStyle, pub contact_status_free: ContainerStyle,
pub contact_status_busy: ContainerStyle, pub contact_status_busy: ContainerStyle,
@ -221,7 +222,7 @@ pub struct ContactList {
pub calling_indicator: ContainedText, pub calling_indicator: ContainedText,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ProjectRow { pub struct ProjectRow {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -229,13 +230,13 @@ pub struct ProjectRow {
pub name: ContainedText, pub name: ContainedText,
} }
#[derive(Deserialize, Default, Clone, Copy)] #[derive(Deserialize, Default, Clone, Copy, JsonSchema)]
pub struct TreeBranch { pub struct TreeBranch {
pub width: f32, pub width: f32,
pub color: Color, pub color: Color,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ContactFinder { pub struct ContactFinder {
pub picker: Picker, pub picker: Picker,
pub row_height: f32, pub row_height: f32,
@ -245,17 +246,17 @@ pub struct ContactFinder {
pub disabled_contact_button: IconButton, pub disabled_contact_button: IconButton,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct DropdownMenu { pub struct DropdownMenu {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub header: Interactive<DropdownMenuItem>, pub header: Interactive<DropdownMenuItem>,
pub section_header: ContainedText, pub section_header: ContainedText,
pub item: Interactive<DropdownMenuItem>, pub item: Toggleable<Interactive<DropdownMenuItem>>,
pub row_height: f32, pub row_height: f32,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct DropdownMenuItem { pub struct DropdownMenuItem {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -266,11 +267,11 @@ pub struct DropdownMenuItem {
pub secondary_text_spacing: f32, pub secondary_text_spacing: f32,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct TabBar { pub struct TabBar {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub pane_button: Interactive<IconButton>, pub pane_button: Toggleable<Interactive<IconButton>>,
pub pane_button_container: ContainerStyle, pub pane_button_container: ContainerStyle,
pub active_pane: TabStyles, pub active_pane: TabStyles,
pub inactive_pane: TabStyles, pub inactive_pane: TabStyles,
@ -294,13 +295,13 @@ impl TabBar {
} }
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct TabStyles { pub struct TabStyles {
pub active_tab: Tab, pub active_tab: Tab,
pub inactive_tab: Tab, pub inactive_tab: Tab,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct AvatarRibbon { pub struct AvatarRibbon {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -308,7 +309,7 @@ pub struct AvatarRibbon {
pub height: f32, pub height: f32,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct OfflineIcon { pub struct OfflineIcon {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -316,7 +317,7 @@ pub struct OfflineIcon {
pub color: Color, pub color: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Tab { pub struct Tab {
pub height: f32, pub height: f32,
#[serde(flatten)] #[serde(flatten)]
@ -333,7 +334,7 @@ pub struct Tab {
pub icon_conflict: Color, pub icon_conflict: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Toolbar { pub struct Toolbar {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -342,14 +343,14 @@ pub struct Toolbar {
pub nav_button: Interactive<IconButton>, pub nav_button: Interactive<IconButton>,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Notifications { pub struct Notifications {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub width: f32, pub width: f32,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Search { pub struct Search {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -359,14 +360,14 @@ pub struct Search {
pub include_exclude_editor: FindEditor, pub include_exclude_editor: FindEditor,
pub invalid_include_exclude_editor: ContainerStyle, pub invalid_include_exclude_editor: ContainerStyle,
pub include_exclude_inputs: ContainedText, pub include_exclude_inputs: ContainedText,
pub option_button: Interactive<ContainedText>, pub option_button: Toggleable<Interactive<ContainedText>>,
pub match_background: Color, pub match_background: Color,
pub match_index: ContainedText, pub match_index: ContainedText,
pub results_status: TextStyle, pub results_status: TextStyle,
pub dismiss_button: Interactive<IconButton>, pub dismiss_button: Interactive<IconButton>,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct FindEditor { pub struct FindEditor {
#[serde(flatten)] #[serde(flatten)]
pub input: FieldEditor, pub input: FieldEditor,
@ -374,7 +375,7 @@ pub struct FindEditor {
pub max_width: f32, pub max_width: f32,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct StatusBar { pub struct StatusBar {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -390,15 +391,15 @@ pub struct StatusBar {
pub diagnostic_message: Interactive<ContainedText>, pub diagnostic_message: Interactive<ContainedText>,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct StatusBarPanelButtons { pub struct StatusBarPanelButtons {
pub group_left: ContainerStyle, pub group_left: ContainerStyle,
pub group_bottom: ContainerStyle, pub group_bottom: ContainerStyle,
pub group_right: ContainerStyle, pub group_right: ContainerStyle,
pub button: Interactive<PanelButton>, pub button: Toggleable<Interactive<PanelButton>>,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct StatusBarDiagnosticSummary { pub struct StatusBarDiagnosticSummary {
pub container_ok: ContainerStyle, pub container_ok: ContainerStyle,
pub container_warning: ContainerStyle, pub container_warning: ContainerStyle,
@ -413,7 +414,7 @@ pub struct StatusBarDiagnosticSummary {
pub summary_spacing: f32, pub summary_spacing: f32,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct StatusBarLspStatus { pub struct StatusBarLspStatus {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -424,14 +425,14 @@ pub struct StatusBarLspStatus {
pub message: TextStyle, pub message: TextStyle,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct Dock { pub struct Dock {
pub left: ContainerStyle, pub left: ContainerStyle,
pub bottom: ContainerStyle, pub bottom: ContainerStyle,
pub right: ContainerStyle, pub right: ContainerStyle,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct PanelButton { pub struct PanelButton {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -440,20 +441,20 @@ pub struct PanelButton {
pub label: ContainedText, pub label: ContainedText,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ProjectPanel { pub struct ProjectPanel {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub entry: Interactive<ProjectPanelEntry>, pub entry: Toggleable<Interactive<ProjectPanelEntry>>,
pub dragged_entry: ProjectPanelEntry, pub dragged_entry: ProjectPanelEntry,
pub ignored_entry: Interactive<ProjectPanelEntry>, pub ignored_entry: Toggleable<Interactive<ProjectPanelEntry>>,
pub cut_entry: Interactive<ProjectPanelEntry>, pub cut_entry: Toggleable<Interactive<ProjectPanelEntry>>,
pub filename_editor: FieldEditor, pub filename_editor: FieldEditor,
pub indent_width: f32, pub indent_width: f32,
pub open_project_button: Interactive<ContainedText>, pub open_project_button: Interactive<ContainedText>,
} }
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ProjectPanelEntry { pub struct ProjectPanelEntry {
pub height: f32, pub height: f32,
#[serde(flatten)] #[serde(flatten)]
@ -465,28 +466,28 @@ pub struct ProjectPanelEntry {
pub status: EntryStatus, pub status: EntryStatus,
} }
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct EntryStatus { pub struct EntryStatus {
pub git: GitProjectStatus, pub git: GitProjectStatus,
} }
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct GitProjectStatus { pub struct GitProjectStatus {
pub modified: Color, pub modified: Color,
pub inserted: Color, pub inserted: Color,
pub conflict: Color, pub conflict: Color,
} }
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ContextMenu { pub struct ContextMenu {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub item: Interactive<ContextMenuItem>, pub item: Toggleable<Interactive<ContextMenuItem>>,
pub keystroke_margin: f32, pub keystroke_margin: f32,
pub separator: ContainerStyle, pub separator: ContainerStyle,
} }
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ContextMenuItem { pub struct ContextMenuItem {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -496,13 +497,13 @@ pub struct ContextMenuItem {
pub icon_spacing: f32, pub icon_spacing: f32,
} }
#[derive(Debug, Deserialize, Default)] #[derive(Debug, Deserialize, Default, JsonSchema)]
pub struct CommandPalette { pub struct CommandPalette {
pub key: Interactive<ContainedLabel>, pub key: Toggleable<ContainedLabel>,
pub keystroke_spacing: f32, pub keystroke_spacing: f32,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct InviteLink { pub struct InviteLink {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -511,7 +512,7 @@ pub struct InviteLink {
pub icon: Icon, pub icon: Icon,
} }
#[derive(Deserialize, Clone, Copy, Default)] #[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
pub struct Icon { pub struct Icon {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -519,7 +520,7 @@ pub struct Icon {
pub width: f32, pub width: f32,
} }
#[derive(Deserialize, Clone, Copy, Default)] #[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
pub struct IconButton { pub struct IconButton {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -528,7 +529,7 @@ pub struct IconButton {
pub button_width: f32, pub button_width: f32,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ChatMessage { pub struct ChatMessage {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -537,7 +538,7 @@ pub struct ChatMessage {
pub timestamp: ContainedText, pub timestamp: ContainedText,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ChannelSelect { pub struct ChannelSelect {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -549,7 +550,7 @@ pub struct ChannelSelect {
pub menu: ContainerStyle, pub menu: ContainerStyle,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ChannelName { pub struct ChannelName {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -557,7 +558,7 @@ pub struct ChannelName {
pub name: TextStyle, pub name: TextStyle,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Picker { pub struct Picker {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -565,10 +566,10 @@ pub struct Picker {
pub input_editor: FieldEditor, pub input_editor: FieldEditor,
pub empty_input_editor: FieldEditor, pub empty_input_editor: FieldEditor,
pub no_matches: ContainedLabel, pub no_matches: ContainedLabel,
pub item: Interactive<ContainedLabel>, pub item: Toggleable<Interactive<ContainedLabel>>,
} }
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ContainedText { pub struct ContainedText {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -576,7 +577,7 @@ pub struct ContainedText {
pub text: TextStyle, pub text: TextStyle,
} }
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ContainedLabel { pub struct ContainedLabel {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -584,7 +585,7 @@ pub struct ContainedLabel {
pub label: LabelStyle, pub label: LabelStyle,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct ProjectDiagnostics { pub struct ProjectDiagnostics {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -594,7 +595,7 @@ pub struct ProjectDiagnostics {
pub tab_summary_spacing: f32, pub tab_summary_spacing: f32,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ContactNotification { pub struct ContactNotification {
pub header_avatar: ImageStyle, pub header_avatar: ImageStyle,
pub header_message: ContainedText, pub header_message: ContainedText,
@ -604,21 +605,21 @@ pub struct ContactNotification {
pub dismiss_button: Interactive<IconButton>, pub dismiss_button: Interactive<IconButton>,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct UpdateNotification { pub struct UpdateNotification {
pub message: ContainedText, pub message: ContainedText,
pub action_message: Interactive<ContainedText>, pub action_message: Interactive<ContainedText>,
pub dismiss_button: Interactive<IconButton>, pub dismiss_button: Interactive<IconButton>,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct MessageNotification { pub struct MessageNotification {
pub message: ContainedText, pub message: ContainedText,
pub action_message: Interactive<ContainedText>, pub action_message: Interactive<ContainedText>,
pub dismiss_button: Interactive<IconButton>, pub dismiss_button: Interactive<IconButton>,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ProjectSharedNotification { pub struct ProjectSharedNotification {
pub window_height: f32, pub window_height: f32,
pub window_width: f32, pub window_width: f32,
@ -635,7 +636,7 @@ pub struct ProjectSharedNotification {
pub dismiss_button: ContainedText, pub dismiss_button: ContainedText,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
pub struct IncomingCallNotification { pub struct IncomingCallNotification {
pub window_height: f32, pub window_height: f32,
pub window_width: f32, pub window_width: f32,
@ -652,7 +653,7 @@ pub struct IncomingCallNotification {
pub decline_button: ContainedText, pub decline_button: ContainedText,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Editor { pub struct Editor {
pub text_color: Color, pub text_color: Color,
#[serde(default)] #[serde(default)]
@ -693,7 +694,7 @@ pub struct Editor {
pub whitespace: Color, pub whitespace: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Scrollbar { pub struct Scrollbar {
pub track: ContainerStyle, pub track: ContainerStyle,
pub thumb: ContainerStyle, pub thumb: ContainerStyle,
@ -702,14 +703,14 @@ pub struct Scrollbar {
pub git: GitDiffColors, pub git: GitDiffColors,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct GitDiffColors { pub struct GitDiffColors {
pub inserted: Color, pub inserted: Color,
pub modified: Color, pub modified: Color,
pub deleted: Color, pub deleted: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct DiagnosticPathHeader { pub struct DiagnosticPathHeader {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -718,7 +719,7 @@ pub struct DiagnosticPathHeader {
pub text_scale_factor: f32, pub text_scale_factor: f32,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct DiagnosticHeader { pub struct DiagnosticHeader {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -729,7 +730,7 @@ pub struct DiagnosticHeader {
pub icon_width_factor: f32, pub icon_width_factor: f32,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct DiagnosticStyle { pub struct DiagnosticStyle {
pub message: LabelStyle, pub message: LabelStyle,
#[serde(default)] #[serde(default)]
@ -737,7 +738,7 @@ pub struct DiagnosticStyle {
pub text_scale_factor: f32, pub text_scale_factor: f32,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct AutocompleteStyle { pub struct AutocompleteStyle {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -747,13 +748,13 @@ pub struct AutocompleteStyle {
pub match_highlight: HighlightStyle, pub match_highlight: HighlightStyle,
} }
#[derive(Clone, Copy, Default, Deserialize)] #[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
pub struct SelectionStyle { pub struct SelectionStyle {
pub cursor: Color, pub cursor: Color,
pub selection: Color, pub selection: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct FieldEditor { pub struct FieldEditor {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
@ -763,21 +764,21 @@ pub struct FieldEditor {
pub selection: SelectionStyle, pub selection: SelectionStyle,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct InteractiveColor { pub struct InteractiveColor {
pub color: Color, pub color: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct CodeActions { pub struct CodeActions {
#[serde(default)] #[serde(default)]
pub indicator: Interactive<InteractiveColor>, pub indicator: Toggleable<Interactive<InteractiveColor>>,
pub vertical_scale: f32, pub vertical_scale: f32,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Folds { pub struct Folds {
pub indicator: Interactive<InteractiveColor>, pub indicator: Toggleable<Interactive<InteractiveColor>>,
pub ellipses: FoldEllipses, pub ellipses: FoldEllipses,
pub fold_background: Color, pub fold_background: Color,
pub icon_margin_scale: f32, pub icon_margin_scale: f32,
@ -785,14 +786,14 @@ pub struct Folds {
pub foldable_icon: String, pub foldable_icon: String,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct FoldEllipses { pub struct FoldEllipses {
pub text_color: Color, pub text_color: Color,
pub background: Interactive<InteractiveColor>, pub background: Interactive<InteractiveColor>,
pub corner_radius_factor: f32, pub corner_radius_factor: f32,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct DiffStyle { pub struct DiffStyle {
pub inserted: Color, pub inserted: Color,
pub modified: Color, pub modified: Color,
@ -802,41 +803,49 @@ pub struct DiffStyle {
pub corner_radius: f32, pub corner_radius: f32,
} }
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy, JsonSchema)]
pub struct Interactive<T> { pub struct Interactive<T> {
pub default: T, pub default: T,
pub hover: Option<T>, pub hovered: Option<T>,
pub hover_and_active: Option<T>,
pub clicked: Option<T>, pub clicked: Option<T>,
pub click_and_active: Option<T>,
pub active: Option<T>,
pub disabled: Option<T>, pub disabled: Option<T>,
} }
impl<T> Interactive<T> { #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T { pub struct Toggleable<T> {
active: T,
inactive: T,
}
impl<T> Toggleable<T> {
pub fn new(active: T, inactive: T) -> Self {
Self { active, inactive }
}
pub fn in_state(&self, active: bool) -> &T {
if active { if active {
if state.hovered() { &self.active
self.hover_and_active } else {
.as_ref() &self.inactive
.unwrap_or(self.active.as_ref().unwrap_or(&self.default)) }
} else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() }
{ pub fn active_state(&self) -> &T {
self.click_and_active self.in_state(true)
.as_ref() }
.unwrap_or(self.active.as_ref().unwrap_or(&self.default)) pub fn inactive_state(&self) -> &T {
} else { self.in_state(false)
self.active.as_ref().unwrap_or(&self.default) }
} }
} else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
impl<T> Interactive<T> {
pub fn style_for(&self, state: &mut MouseState) -> &T {
if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
self.clicked.as_ref().unwrap() self.clicked.as_ref().unwrap()
} else if state.hovered() { } else if state.hovered() {
self.hover.as_ref().unwrap_or(&self.default) self.hovered.as_ref().unwrap_or(&self.default)
} else { } else {
&self.default &self.default
} }
} }
pub fn disabled_style(&self) -> &T { pub fn disabled_style(&self) -> &T {
self.disabled.as_ref().unwrap_or(&self.default) self.disabled.as_ref().unwrap_or(&self.default)
} }
@ -849,13 +858,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
{ {
#[derive(Deserialize)] #[derive(Deserialize)]
struct Helper { struct Helper {
#[serde(flatten)]
default: Value, default: Value,
hover: Option<Value>, hovered: Option<Value>,
hover_and_active: Option<Value>,
clicked: Option<Value>, clicked: Option<Value>,
click_and_active: Option<Value>,
active: Option<Value>,
disabled: Option<Value>, disabled: Option<Value>,
} }
@ -880,21 +885,15 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
} }
}; };
let hover = deserialize_state(json.hover)?; let hovered = deserialize_state(json.hovered)?;
let hover_and_active = deserialize_state(json.hover_and_active)?;
let clicked = deserialize_state(json.clicked)?; let clicked = deserialize_state(json.clicked)?;
let click_and_active = deserialize_state(json.click_and_active)?;
let active = deserialize_state(json.active)?;
let disabled = deserialize_state(json.disabled)?; let disabled = deserialize_state(json.disabled)?;
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?; let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
Ok(Interactive { Ok(Interactive {
default, default,
hover, hovered,
hover_and_active,
clicked, clicked,
click_and_active,
active,
disabled, disabled,
}) })
} }
@ -911,7 +910,7 @@ impl Editor {
} }
} }
#[derive(Default)] #[derive(Default, JsonSchema)]
pub struct SyntaxTheme { pub struct SyntaxTheme {
pub highlights: Vec<(String, HighlightStyle)>, pub highlights: Vec<(String, HighlightStyle)>,
} }
@ -945,7 +944,7 @@ impl<'de> Deserialize<'de> for SyntaxTheme {
} }
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct HoverPopover { pub struct HoverPopover {
pub container: ContainerStyle, pub container: ContainerStyle,
pub info_container: ContainerStyle, pub info_container: ContainerStyle,
@ -957,7 +956,7 @@ pub struct HoverPopover {
pub highlight: Color, pub highlight: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct TerminalStyle { pub struct TerminalStyle {
pub black: Color, pub black: Color,
pub red: Color, pub red: Color,
@ -991,7 +990,7 @@ pub struct TerminalStyle {
pub dim_foreground: Color, pub dim_foreground: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct AssistantStyle { pub struct AssistantStyle {
pub container: ContainerStyle, pub container: ContainerStyle,
pub hamburger_button: IconStyle, pub hamburger_button: IconStyle,
@ -1014,15 +1013,14 @@ pub struct AssistantStyle {
pub saved_conversation: SavedConversation, pub saved_conversation: SavedConversation,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct SavedConversation { pub struct SavedConversation {
#[serde(flatten)]
pub container: Interactive<ContainerStyle>, pub container: Interactive<ContainerStyle>,
pub saved_at: ContainedText, pub saved_at: ContainedText,
pub title: ContainedText, pub title: ContainedText,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct FeedbackStyle { pub struct FeedbackStyle {
pub submit_button: Interactive<ContainedText>, pub submit_button: Interactive<ContainedText>,
pub button_margin: f32, pub button_margin: f32,
@ -1031,7 +1029,7 @@ pub struct FeedbackStyle {
pub link_text_hover: ContainedText, pub link_text_hover: ContainedText,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct WelcomeStyle { pub struct WelcomeStyle {
pub page_width: f32, pub page_width: f32,
pub logo: SvgStyle, pub logo: SvgStyle,
@ -1045,7 +1043,7 @@ pub struct WelcomeStyle {
pub checkbox_group: ContainerStyle, pub checkbox_group: ContainerStyle,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct ColorScheme { pub struct ColorScheme {
pub name: String, pub name: String,
pub is_light: bool, pub is_light: bool,
@ -1060,13 +1058,13 @@ pub struct ColorScheme {
pub players: Vec<Player>, pub players: Vec<Player>,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Player { pub struct Player {
pub cursor: Color, pub cursor: Color,
pub selection: Color, pub selection: Color,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct RampSet { pub struct RampSet {
pub neutral: Vec<Color>, pub neutral: Vec<Color>,
pub red: Vec<Color>, pub red: Vec<Color>,
@ -1079,7 +1077,7 @@ pub struct RampSet {
pub magenta: Vec<Color>, pub magenta: Vec<Color>,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Layer { pub struct Layer {
pub base: StyleSet, pub base: StyleSet,
pub variant: StyleSet, pub variant: StyleSet,
@ -1090,7 +1088,7 @@ pub struct Layer {
pub negative: StyleSet, pub negative: StyleSet,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct StyleSet { pub struct StyleSet {
pub default: Style, pub default: Style,
pub active: Style, pub active: Style,
@ -1100,7 +1098,7 @@ pub struct StyleSet {
pub inverted: Style, pub inverted: Style,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Style { pub struct Style {
pub background: Color, pub background: Color,
pub border: Color, pub border: Color,

View file

@ -14,12 +14,13 @@ use util::ResultExt as _;
const MIN_FONT_SIZE: f32 = 6.0; const MIN_FONT_SIZE: f32 = 6.0;
#[derive(Clone)] #[derive(Clone, JsonSchema)]
pub struct ThemeSettings { pub struct ThemeSettings {
pub buffer_font_family_name: String, pub buffer_font_family_name: String,
pub buffer_font_features: fonts::Features, pub buffer_font_features: fonts::Features,
pub buffer_font_family: FamilyId, pub buffer_font_family: FamilyId,
pub(crate) buffer_font_size: f32, pub(crate) buffer_font_size: f32,
#[serde(skip)]
pub theme: Arc<Theme>, pub theme: Arc<Theme>,
} }

View file

@ -12,11 +12,12 @@ use gpui::{
scene::MouseClick, scene::MouseClick,
Action, Element, EventContext, MouseState, View, ViewContext, Action, Element, EventContext, MouseState, View, ViewContext,
}; };
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use crate::{ContainedText, Interactive}; use crate::{ContainedText, Interactive};
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct CheckboxStyle { pub struct CheckboxStyle {
pub icon: SvgStyle, pub icon: SvgStyle,
pub label: ContainedText, pub label: ContainedText,
@ -100,7 +101,7 @@ pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
.with_height(style.dimensions.height) .with_height(style.dimensions.height)
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct IconStyle { pub struct IconStyle {
pub icon: SvgStyle, pub icon: SvgStyle,
pub container: ContainerStyle, pub container: ContainerStyle,
@ -150,7 +151,7 @@ where
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static, F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
{ {
MouseEventHandler::<Tag, V>::new(0, cx, |state, _| { MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
let style = style.style_for(state, false); let style = style.style_for(state);
Label::new(label, style.text.to_owned()) Label::new(label, style.text.to_owned())
.aligned() .aligned()
.contained() .contained()
@ -162,7 +163,7 @@ where
.with_cursor_style(platform::CursorStyle::PointingHand) .with_cursor_style(platform::CursorStyle::PointingHand)
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct ModalStyle { pub struct ModalStyle {
close_icon: Interactive<IconStyle>, close_icon: Interactive<IconStyle>,
container: ContainerStyle, container: ContainerStyle,
@ -200,13 +201,13 @@ where
title, title,
style style
.title_text .title_text
.style_for(&mut MouseState::default(), false) .style_for(&mut MouseState::default())
.clone(), .clone(),
)) ))
.with_child( .with_child(
// FIXME: Get a better tag type // FIXME: Get a better tag type
MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| { MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
let style = style.close_icon.style_for(state, false); let style = style.close_icon.style_for(state);
icon(style) icon(style)
}) })
.on_click(platform::MouseButton::Left, move |_, _, cx| { .on_click(platform::MouseButton::Left, move |_, _, cx| {

View file

@ -208,7 +208,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
cx: &AppContext, cx: &AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let theme_match = &self.matches[ix]; let theme_match = &self.matches[ix];
Label::new(theme_match.string.clone(), style.label.clone()) Label::new(theme_match.string.clone(), style.label.clone())

View file

@ -1,19 +0,0 @@
[package]
name = "theme_testbench"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/theme_testbench.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }
theme = { path = "../theme" }
settings = { path = "../settings" }
workspace = { path = "../workspace" }
project = { path = "../project" }
smallvec.workspace = true

View file

@ -1,300 +0,0 @@
use gpui::{
actions,
color::Color,
elements::{
AnyElement, Canvas, Container, ContainerStyle, Flex, Label, Margin, MouseEventHandler,
Padding, ParentElement,
},
fonts::TextStyle,
AppContext, Border, Element, Entity, ModelHandle, Quad, Task, View, ViewContext, ViewHandle,
WeakViewHandle,
};
use project::Project;
use theme::{ColorScheme, Layer, Style, StyleSet, ThemeSettings};
use workspace::{item::Item, register_deserializable_item, Pane, Workspace};
actions!(theme, [DeployThemeTestbench]);
pub fn init(cx: &mut AppContext) {
cx.add_action(ThemeTestbench::deploy);
register_deserializable_item::<ThemeTestbench>(cx)
}
pub struct ThemeTestbench {}
impl ThemeTestbench {
pub fn deploy(
workspace: &mut Workspace,
_: &DeployThemeTestbench,
cx: &mut ViewContext<Workspace>,
) {
let view = cx.add_view(|_| ThemeTestbench {});
workspace.add_item(Box::new(view), cx);
}
fn render_ramps(color_scheme: &ColorScheme) -> Flex<Self> {
fn display_ramp(ramp: &Vec<Color>) -> AnyElement<ThemeTestbench> {
Flex::row()
.with_children(ramp.iter().cloned().map(|color| {
Canvas::new(move |scene, bounds, _, _, _| {
scene.push_quad(Quad {
bounds,
background: Some(color),
..Default::default()
});
})
.flex(1.0, false)
}))
.flex(1.0, false)
.into_any()
}
Flex::column()
.with_child(display_ramp(&color_scheme.ramps.neutral))
.with_child(display_ramp(&color_scheme.ramps.red))
.with_child(display_ramp(&color_scheme.ramps.orange))
.with_child(display_ramp(&color_scheme.ramps.yellow))
.with_child(display_ramp(&color_scheme.ramps.green))
.with_child(display_ramp(&color_scheme.ramps.cyan))
.with_child(display_ramp(&color_scheme.ramps.blue))
.with_child(display_ramp(&color_scheme.ramps.violet))
.with_child(display_ramp(&color_scheme.ramps.magenta))
}
fn render_layer(
layer_index: usize,
layer: &Layer,
cx: &mut ViewContext<Self>,
) -> Container<Self> {
Flex::column()
.with_child(
Self::render_button_set(0, layer_index, "base", &layer.base, cx).flex(1., false),
)
.with_child(
Self::render_button_set(1, layer_index, "variant", &layer.variant, cx)
.flex(1., false),
)
.with_child(
Self::render_button_set(2, layer_index, "on", &layer.on, cx).flex(1., false),
)
.with_child(
Self::render_button_set(3, layer_index, "accent", &layer.accent, cx)
.flex(1., false),
)
.with_child(
Self::render_button_set(4, layer_index, "positive", &layer.positive, cx)
.flex(1., false),
)
.with_child(
Self::render_button_set(5, layer_index, "warning", &layer.warning, cx)
.flex(1., false),
)
.with_child(
Self::render_button_set(6, layer_index, "negative", &layer.negative, cx)
.flex(1., false),
)
.contained()
.with_style(ContainerStyle {
margin: Margin {
top: 10.,
bottom: 10.,
left: 10.,
right: 10.,
},
background_color: Some(layer.base.default.background),
..Default::default()
})
}
fn render_button_set(
set_index: usize,
layer_index: usize,
set_name: &'static str,
style_set: &StyleSet,
cx: &mut ViewContext<Self>,
) -> Flex<Self> {
Flex::row()
.with_child(Self::render_button(
set_index * 6,
layer_index,
set_name,
&style_set,
None,
cx,
))
.with_child(Self::render_button(
set_index * 6 + 1,
layer_index,
"hovered",
&style_set,
Some(|style_set| &style_set.hovered),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 2,
layer_index,
"pressed",
&style_set,
Some(|style_set| &style_set.pressed),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 3,
layer_index,
"active",
&style_set,
Some(|style_set| &style_set.active),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 4,
layer_index,
"disabled",
&style_set,
Some(|style_set| &style_set.disabled),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 5,
layer_index,
"inverted",
&style_set,
Some(|style_set| &style_set.inverted),
cx,
))
}
fn render_button(
button_index: usize,
layer_index: usize,
text: &'static str,
style_set: &StyleSet,
style_override: Option<fn(&StyleSet) -> &Style>,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum TestBenchButton {}
MouseEventHandler::<TestBenchButton, _>::new(layer_index + button_index, cx, |state, cx| {
let style = if let Some(style_override) = style_override {
style_override(&style_set)
} else if state.clicked().is_some() {
&style_set.pressed
} else if state.hovered() {
&style_set.hovered
} else {
&style_set.default
};
Self::render_label(text.to_string(), style, cx)
.contained()
.with_style(ContainerStyle {
margin: Margin {
top: 4.,
bottom: 4.,
left: 4.,
right: 4.,
},
padding: Padding {
top: 4.,
bottom: 4.,
left: 4.,
right: 4.,
},
background_color: Some(style.background),
border: Border {
width: 1.,
color: style.border,
overlay: false,
top: true,
bottom: true,
left: true,
right: true,
},
corner_radius: 2.,
..Default::default()
})
})
.flex(1., true)
.into_any()
}
fn render_label(text: String, style: &Style, cx: &mut ViewContext<Self>) -> Label {
let settings = settings::get::<ThemeSettings>(cx);
let font_cache = cx.font_cache();
let family_id = settings.buffer_font_family;
let font_size = settings.buffer_font_size(cx);
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let text_style = TextStyle {
color: style.foreground,
font_family_id: family_id,
font_family_name: font_cache.family_name(family_id).unwrap(),
font_id,
font_size,
font_properties: Default::default(),
underline: Default::default(),
};
Label::new(text, text_style)
}
}
impl Entity for ThemeTestbench {
type Event = ();
}
impl View for ThemeTestbench {
fn ui_name() -> &'static str {
"ThemeTestbench"
}
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
let color_scheme = &theme::current(cx).clone().color_scheme;
Flex::row()
.with_child(
Self::render_ramps(color_scheme)
.contained()
.with_margin_right(10.)
.flex(0.1, false),
)
.with_child(
Flex::column()
.with_child(Self::render_layer(100, &color_scheme.lowest, cx).flex(1., true))
.with_child(Self::render_layer(200, &color_scheme.middle, cx).flex(1., true))
.with_child(Self::render_layer(300, &color_scheme.highest, cx).flex(1., true))
.flex(1., false),
)
.into_any()
}
}
impl Item for ThemeTestbench {
fn tab_content<T: View>(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &AppContext,
) -> AnyElement<T> {
Label::new("Theme Testbench", style.label.clone())
.aligned()
.contained()
.into_any()
}
fn serialized_item_kind() -> Option<&'static str> {
Some("ThemeTestBench")
}
fn deserialize(
_project: ModelHandle<Project>,
_workspace: WeakViewHandle<Workspace>,
_workspace_id: workspace::WorkspaceId,
_item_id: workspace::ItemId,
cx: &mut ViewContext<Pane>,
) -> Task<gpui::anyhow::Result<ViewHandle<Self>>> {
Task::ready(Ok(cx.add_view(|_| Self {})))
}
}

View file

@ -98,3 +98,14 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps"); assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
}) })
} }
#[gpui::test]
async fn test_count_down(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal);
cx.simulate_keystrokes(["2", "down"]);
cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee");
cx.simulate_keystrokes(["9", "down"]);
cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe");
}

View file

@ -141,7 +141,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
) -> gpui::AnyElement<Picker<Self>> { ) -> gpui::AnyElement<Picker<Self>> {
let theme = &theme::current(cx); let theme = &theme::current(cx);
let keymap_match = &self.matches[ix]; let keymap_match = &self.matches[ix];
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.in_state(selected).style_for(mouse_state);
Label::new(keymap_match.string.clone(), style.label.clone()) Label::new(keymap_match.string.clone(), style.label.clone())
.with_highlights(keymap_match.positions.clone()) .with_highlights(keymap_match.positions.clone())

View file

@ -498,7 +498,9 @@ impl View for PanelButtons {
Stack::new() Stack::new()
.with_child( .with_child(
MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| { MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
let style = button_style.style_for(state, is_active); let style = button_style.in_state(is_active);
let style = style.style_for(state);
Flex::row() Flex::row()
.with_child( .with_child(
Svg::new(view.icon_path(cx)) Svg::new(view.icon_path(cx))

View file

@ -291,7 +291,7 @@ pub mod simple_message_notification {
) )
.with_child( .with_child(
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| { MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false); let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg") Svg::new("icons/x_mark_8.svg")
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()
@ -323,7 +323,7 @@ pub mod simple_message_notification {
0, 0,
cx, cx,
|state, _| { |state, _| {
let style = theme.action_message.style_for(state, false); let style = theme.action_message.style_for(state);
Flex::row() Flex::row()
.with_child( .with_child(

View file

@ -1410,7 +1410,7 @@ impl Pane {
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>( pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
index: usize, index: usize,
icon: &'static str, icon: &'static str,
active: bool, is_active: bool,
tooltip: Option<(String, Option<Box<dyn Action>>)>, tooltip: Option<(String, Option<Box<dyn Action>>)>,
cx: &mut ViewContext<Pane>, cx: &mut ViewContext<Pane>,
on_click: F, on_click: F,
@ -1420,7 +1420,7 @@ impl Pane {
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| { let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar; let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
let style = theme.pane_button.style_for(mouse_state, active); let style = theme.pane_button.in_state(is_active).style_for(mouse_state);
Svg::new(icon) Svg::new(icon)
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()

View file

@ -231,7 +231,7 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
) -> AnyElement<Toolbar> { ) -> AnyElement<Toolbar> {
MouseEventHandler::<A, _>::new(0, cx, |state, _| { MouseEventHandler::<A, _>::new(0, cx, |state, _| {
let style = if enabled { let style = if enabled {
style.style_for(state, false) style.style_for(state)
} else { } else {
style.disabled_style() style.disabled_style()
}; };

View file

@ -140,9 +140,11 @@ pub struct OpenPaths {
#[derive(Clone, Deserialize, PartialEq)] #[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePane(pub usize); pub struct ActivatePane(pub usize);
#[derive(Deserialize)]
pub struct Toast { pub struct Toast {
id: usize, id: usize,
msg: Cow<'static, str>, msg: Cow<'static, str>,
#[serde(skip)]
on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>, on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
} }
@ -183,9 +185,9 @@ impl Clone for Toast {
} }
} }
pub type WorkspaceId = i64; impl_actions!(workspace, [ActivatePane, Toast]);
impl_actions!(workspace, [ActivatePane]); pub type WorkspaceId = i64;
pub fn init_settings(cx: &mut AppContext) { pub fn init_settings(cx: &mut AppContext) {
settings::register::<WorkspaceSettings>(cx); settings::register::<WorkspaceSettings>(cx);

13
crates/xtask/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"
clap = {version = "4.0", features = ["derive"]}
theme = {path = "../theme"}
serde_json.workspace = true
schemars.workspace = true

23
crates/xtask/src/cli.rs Normal file
View file

@ -0,0 +1,23 @@
use clap::{Parser, Subcommand};
use std::path::PathBuf;
/// Common utilities for Zed developers.
// For more information, see [matklad's repository README](https://github.com/matklad/cargo-xtask/)
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
/// Command to run.
#[derive(Subcommand)]
pub enum Commands {
/// Builds theme types for interop with Typescript.
BuildThemeTypes {
#[clap(short, long, default_value = "schemas")]
out_dir: PathBuf,
#[clap(short, long, default_value = "theme.json")]
file_name: PathBuf,
},
}

29
crates/xtask/src/main.rs Normal file
View file

@ -0,0 +1,29 @@
mod cli;
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use schemars::schema_for;
use theme::Theme;
fn build_themes(out_dir: PathBuf, file_name: PathBuf) -> Result<()> {
let theme = schema_for!(Theme);
let output = serde_json::to_string_pretty(&theme)?;
std::fs::create_dir(&out_dir)?;
let mut file_path = out_dir;
file_path.push(file_name);
std::fs::write(file_path, output)?;
Ok(())
}
fn main() -> Result<()> {
let args = cli::Cli::parse();
match args.command {
cli::Commands::BuildThemeTypes { out_dir, file_name } => build_themes(out_dir, file_name),
}
}

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor." description = "The fast, collaborative code editor."
edition = "2021" edition = "2021"
name = "zed" name = "zed"
version = "0.92.0" version = "0.93.0"
publish = false publish = false
[lib] [lib]
@ -62,7 +62,6 @@ text = { path = "../text" }
terminal_view = { path = "../terminal_view" } terminal_view = { path = "../terminal_view" }
theme = { path = "../theme" } theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" } theme_selector = { path = "../theme_selector" }
theme_testbench = { path = "../theme_testbench" }
util = { path = "../util" } util = { path = "../util" }
vim = { path = "../vim" } vim = { path = "../vim" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }

View file

@ -154,7 +154,6 @@ fn main() {
search::init(cx); search::init(cx);
vim::init(cx); vim::init(cx);
terminal_view::init(cx); terminal_view::init(cx);
theme_testbench::init(cx);
copilot::init(http.clone(), node_runtime, cx); copilot::init(http.clone(), node_runtime, cx);
ai::init(cx); ai::init(cx);

View file

@ -0,0 +1,79 @@
# Syntax Highlighting in Zed
This doc is a work in progress!
## Defining syntax highlighting rules
We use tree-sitter queries to match certian properties to highlight.
### Simple Example:
```scheme
(property_identifier) @property
```
```ts
const font: FontFamily = {
weight: "normal",
underline: false,
italic: false,
}
```
Match a property identifier and highlight it using the identifier `@property`. In the above example, `weight`, `underline`, and `italic` would be highlighted.
### Complex example:
```scheme
(_
return_type: (type_annotation
[
(type_identifier) @type.return
(generic_type
name: (type_identifier) @type.return)
]))
```
```ts
function buildDefaultSyntax(colorScheme: ColorScheme): Partial<Syntax> {
// ...
}
```
Match a function return type, and highlight the type using the identifier `@type.return`. In the above example, `Partial` would be highlighted.
### Example - Typescript
Here is an example portion of our `highlights.scm` for TypeScript:
```scheme
; crates/zed/src/languages/typescript/highlights.scm
; Variables
(identifier) @variable
; Properties
(property_identifier) @property
; Function and method calls
(call_expression
function: (identifier) @function)
(call_expression
function: (member_expression
property: (property_identifier) @function.method))
; Function and method definitions
(function
name: (identifier) @function)
(function_declaration
name: (identifier) @function)
(method_definition
name: (property_identifier) @function.method)
; ...
```

1
styles/.gitignore vendored
View file

@ -1 +1,2 @@
node_modules/ node_modules/
coverage/

20
styles/.zed/settings.json Normal file
View file

@ -0,0 +1,20 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
{
"languages": {
"TypeScript": {
"tab_size": 4
},
"TSX": {
"tab_size": 4
},
"JavaScript": {
"tab_size": 4
},
"JSON": {
"tab_size": 4
}
}
}

1642
styles/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,9 @@
"scripts": { "scripts": {
"build": "ts-node ./src/buildThemes.ts", "build": "ts-node ./src/buildThemes.ts",
"build-licenses": "ts-node ./src/buildLicenses.ts", "build-licenses": "ts-node ./src/buildLicenses.ts",
"build-tokens": "ts-node ./src/buildTokens.ts" "build-tokens": "ts-node ./src/buildTokens.ts",
"build-types": "cd ../crates/theme && cargo test && cd ../../styles && ts-node ./src/buildTypes.ts",
"test": "vitest"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@ -19,13 +21,20 @@
"case-anything": "^2.1.10", "case-anything": "^2.1.10",
"chroma-js": "^2.4.2", "chroma-js": "^2.4.2",
"deepmerge": "^4.3.0", "deepmerge": "^4.3.0",
"json-schema-to-typescript": "^13.0.2",
"toml": "^3.0.0", "toml": "^3.0.0",
"ts-node": "^10.9.1" "ts-deepmerge": "^6.0.3",
"ts-node": "^10.9.1",
"utility-types": "^3.10.0",
"vitest": "^0.32.0"
}, },
"prettier": { "prettier": {
"semi": false, "semi": false,
"printWidth": 80, "printWidth": 80,
"htmlWhitespaceSensitivity": "strict", "htmlWhitespaceSensitivity": "strict",
"tabWidth": 4 "tabWidth": 4
},
"devDependencies": {
"@vitest/coverage-v8": "^0.32.0"
} }
} }

View file

@ -1,13 +1,13 @@
import * as fs from "fs"; import * as fs from "fs"
import * as path from "path"; import * as path from "path"
import { ColorScheme, createColorScheme } from "./common"; import { ColorScheme, createColorScheme } from "./common"
import { themes } from "./themes"; import { themes } from "./themes"
import { slugify } from "./utils/slugify"; import { slugify } from "./utils/slugify"
import { colorSchemeTokens } from "./theme/tokens/colorScheme"; import { colorSchemeTokens } from "./theme/tokens/colorScheme"
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens"); const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json"); const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json"); const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json")
function clearTokens(tokensDirectory: string) { function clearTokens(tokensDirectory: string) {
if (!fs.existsSync(tokensDirectory)) { if (!fs.existsSync(tokensDirectory)) {
@ -22,64 +22,66 @@ function clearTokens(tokensDirectory: string) {
} }
type TokenSet = { type TokenSet = {
id: string; id: string
name: string; name: string
selectedTokenSets: { [key: string]: "enabled" }; selectedTokenSets: { [key: string]: "enabled" }
}; }
function buildTokenSetOrder(colorSchemes: ColorScheme[]): { tokenSetOrder: string[] } { function buildTokenSetOrder(colorSchemes: ColorScheme[]): {
const tokenSetOrder: string[] = colorSchemes.map( tokenSetOrder: string[]
(scheme) => scheme.name.toLowerCase().replace(/\s+/g, "_") } {
); const tokenSetOrder: string[] = colorSchemes.map((scheme) =>
return { tokenSetOrder }; scheme.name.toLowerCase().replace(/\s+/g, "_")
)
return { tokenSetOrder }
} }
function buildThemesIndex(colorSchemes: ColorScheme[]): TokenSet[] { function buildThemesIndex(colorSchemes: ColorScheme[]): TokenSet[] {
const themesIndex: TokenSet[] = colorSchemes.map((scheme, index) => { const themesIndex: TokenSet[] = colorSchemes.map((scheme, index) => {
const id = `${scheme.isLight ? "light" : "dark"}_${scheme.name const id = `${scheme.isLight ? "light" : "dark"}_${scheme.name
.toLowerCase() .toLowerCase()
.replace(/\s+/g, "_")}_${index}`; .replace(/\s+/g, "_")}_${index}`
const selectedTokenSets: { [key: string]: "enabled" } = {}; const selectedTokenSets: { [key: string]: "enabled" } = {}
const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_"); const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_")
selectedTokenSets[tokenSet] = "enabled"; selectedTokenSets[tokenSet] = "enabled"
return { return {
id, id,
name: `${scheme.name} - ${scheme.isLight ? "Light" : "Dark"}`, name: `${scheme.name} - ${scheme.isLight ? "Light" : "Dark"}`,
selectedTokenSets, selectedTokenSets,
}; }
}); })
return themesIndex; return themesIndex
} }
function writeTokens(colorSchemes: ColorScheme[], tokensDirectory: string) { function writeTokens(colorSchemes: ColorScheme[], tokensDirectory: string) {
clearTokens(tokensDirectory); clearTokens(tokensDirectory)
for (const colorScheme of colorSchemes) { for (const colorScheme of colorSchemes) {
const fileName = slugify(colorScheme.name) + ".json"; const fileName = slugify(colorScheme.name) + ".json"
const tokens = colorSchemeTokens(colorScheme); const tokens = colorSchemeTokens(colorScheme)
const tokensJSON = JSON.stringify(tokens, null, 2); const tokensJSON = JSON.stringify(tokens, null, 2)
const outPath = path.join(tokensDirectory, fileName); const outPath = path.join(tokensDirectory, fileName)
fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 }); fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 })
console.log(`- ${outPath} created`); console.log(`- ${outPath} created`)
} }
const themeIndexData = buildThemesIndex(colorSchemes); const themeIndexData = buildThemesIndex(colorSchemes)
const themesJSON = JSON.stringify(themeIndexData, null, 2); const themesJSON = JSON.stringify(themeIndexData, null, 2)
fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 }); fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 })
console.log(`- ${TOKENS_FILE} created`); console.log(`- ${TOKENS_FILE} created`)
const tokenSetOrderData = buildTokenSetOrder(colorSchemes); const tokenSetOrderData = buildTokenSetOrder(colorSchemes)
const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2); const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2)
fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 }); fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 })
console.log(`- ${METADATA_FILE} created`); console.log(`- ${METADATA_FILE} created`)
} }
const colorSchemes: ColorScheme[] = themes.map((theme) => const colorSchemes: ColorScheme[] = themes.map((theme) =>
createColorScheme(theme) createColorScheme(theme)
); )
writeTokens(colorSchemes, TOKENS_DIRECTORY); writeTokens(colorSchemes, TOKENS_DIRECTORY)

64
styles/src/buildTypes.ts Normal file
View file

@ -0,0 +1,64 @@
import * as fs from "fs/promises"
import * as fsSync from "fs"
import * as path from "path"
import { compile } from "json-schema-to-typescript"
const BANNER = `/*
* This file is autogenerated
*/\n\n`
const dirname = __dirname
async function main() {
let schemasPath = path.join(dirname, "../../", "crates/theme/schemas")
let schemaFiles = (await fs.readdir(schemasPath)).filter((x) =>
x.endsWith(".json")
)
let compiledTypes = new Set()
for (let filename of schemaFiles) {
let filePath = path.join(schemasPath, filename)
const fileContents = await fs.readFile(filePath)
let schema = JSON.parse(fileContents.toString())
let compiled = await compile(schema, schema.title, {
bannerComment: "",
})
let eachType = compiled.split("export")
for (let type of eachType) {
if (!type) {
continue
}
compiledTypes.add("export " + type.trim())
}
}
let output = BANNER + Array.from(compiledTypes).join("\n\n")
let outputPath = path.join(dirname, "../../styles/src/types/zed.ts")
try {
let existing = await fs.readFile(outputPath)
if (existing.toString() == output) {
// Skip writing if it hasn't changed
console.log("Schemas are up to date")
return
}
} catch (e) {
// It's fine if there's no output from a previous run.
// @ts-ignore
if (e.code !== "ENOENT") {
throw e
}
}
const typesDic = path.dirname(outputPath)
if (!fsSync.existsSync(typesDic)) {
await fs.mkdir(typesDic)
}
await fs.writeFile(outputPath, output)
console.log(`Wrote Typescript types to ${outputPath}`)
}
main().catch((e) => {
console.error(e)
process.exit(1)
})

View file

@ -0,0 +1,4 @@
import { interactive } from "./interactive"
import { toggleable } from "./toggle"
export { interactive, toggleable }

View file

@ -0,0 +1,56 @@
import {
NOT_ENOUGH_STATES_ERROR,
NO_DEFAULT_OR_BASE_ERROR,
interactive,
} from "./interactive"
import { describe, it, expect } from "vitest"
describe("interactive", () => {
it("creates an Interactive<Element> with base properties and states", () => {
const result = interactive({
base: { fontSize: 10, color: "#FFFFFF" },
state: {
hovered: { color: "#EEEEEE" },
clicked: { color: "#CCCCCC" },
},
})
expect(result).toEqual({
default: { color: "#FFFFFF", fontSize: 10 },
hovered: { color: "#EEEEEE", fontSize: 10 },
clicked: { color: "#CCCCCC", fontSize: 10 },
})
})
it("creates an Interactive<Element> with no base properties", () => {
const result = interactive({
state: {
default: { color: "#FFFFFF", fontSize: 10 },
hovered: { color: "#EEEEEE" },
clicked: { color: "#CCCCCC" },
},
})
expect(result).toEqual({
default: { color: "#FFFFFF", fontSize: 10 },
hovered: { color: "#EEEEEE", fontSize: 10 },
clicked: { color: "#CCCCCC", fontSize: 10 },
})
})
it("throws error when both default and base are missing", () => {
const state = {
hovered: { color: "blue" },
}
expect(() => interactive({ state })).toThrow(NO_DEFAULT_OR_BASE_ERROR)
})
it("throws error when no other state besides default is present", () => {
const state = {
default: { fontSize: 10 },
}
expect(() => interactive({ state })).toThrow(NOT_ENOUGH_STATES_ERROR)
})
})

View file

@ -0,0 +1,97 @@
import merge from "ts-deepmerge"
import { DeepPartial } from "utility-types"
type InteractiveState =
| "default"
| "hovered"
| "clicked"
| "selected"
| "disabled"
type Interactive<T> = {
default: T
hovered?: T
clicked?: T
selected?: T
disabled?: T
}
export const NO_DEFAULT_OR_BASE_ERROR =
"An interactive object must have a default state, or a base property."
export const NOT_ENOUGH_STATES_ERROR =
"An interactive object must have a default and at least one other state."
interface InteractiveProps<T> {
base?: T
state: Partial<Record<InteractiveState, DeepPartial<T>>>
}
/**
* Helper function for creating Interactive<T> objects that works with Toggle<T>-like behavior.
* It takes a default object to be used as the value for `default` field and fills out other fields
* with fields from either `base` or from the `state` object which contains values for specific states.
* Notably, it does not touch `hover`, `clicked`, `selected` and `disabled` states if there are no modifications for them.
*
* @param defaultObj Object to be used as the value for the `default` field.
* @param base Optional object containing base fields to be included in the resulting object.
* @param state Object containing optional modified fields to be included in the resulting object for each state.
* @returns Interactive<T> object with fields from `base` and `state`.
*/
export function interactive<T extends Object>({
base,
state,
}: InteractiveProps<T>): Interactive<T> {
if (!base && !state.default) throw new Error(NO_DEFAULT_OR_BASE_ERROR)
let defaultState: T
if (state.default && base) {
defaultState = merge(base, state.default) as T
} else {
defaultState = base ? base : (state.default as T)
}
let interactiveObj: Interactive<T> = {
default: defaultState,
}
let stateCount = 0
if (state.hovered !== undefined) {
interactiveObj.hovered = merge(
interactiveObj.default,
state.hovered
) as T
stateCount++
}
if (state.clicked !== undefined) {
interactiveObj.clicked = merge(
interactiveObj.default,
state.clicked
) as T
stateCount++
}
if (state.selected !== undefined) {
interactiveObj.selected = merge(
interactiveObj.default,
state.selected
) as T
stateCount++
}
if (state.disabled !== undefined) {
interactiveObj.disabled = merge(
interactiveObj.default,
state.disabled
) as T
stateCount++
}
if (stateCount < 1) {
throw new Error(NOT_ENOUGH_STATES_ERROR)
}
return interactiveObj
}

View file

@ -0,0 +1,52 @@
import {
NO_ACTIVE_ERROR,
NO_INACTIVE_OR_BASE_ERROR,
toggleable,
} from "./toggle"
import { describe, it, expect } from "vitest"
describe("toggleable", () => {
it("creates a Toggleable<Element> with base properties and states", () => {
const result = toggleable({
base: { background: "#000000", color: "#CCCCCC" },
state: {
active: { color: "#FFFFFF" },
},
})
expect(result).toEqual({
inactive: { background: "#000000", color: "#CCCCCC" },
active: { background: "#000000", color: "#FFFFFF" },
})
})
it("creates a Toggleable<Element> with no base properties", () => {
const result = toggleable({
state: {
inactive: { background: "#000000", color: "#CCCCCC" },
active: { background: "#000000", color: "#FFFFFF" },
},
})
expect(result).toEqual({
inactive: { background: "#000000", color: "#CCCCCC" },
active: { background: "#000000", color: "#FFFFFF" },
})
})
it("throws error when both inactive and base are missing", () => {
const state = {
active: { background: "#000000", color: "#FFFFFF" },
}
expect(() => toggleable({ state })).toThrow(NO_INACTIVE_OR_BASE_ERROR)
})
it("throws error when no active state is present", () => {
const state = {
inactive: { background: "#000000", color: "#CCCCCC" },
}
expect(() => toggleable({ state })).toThrow(NO_ACTIVE_ERROR)
})
})

View file

@ -0,0 +1,47 @@
import merge from "ts-deepmerge"
import { DeepPartial } from "utility-types"
type ToggleState = "inactive" | "active"
type Toggleable<T> = Record<ToggleState, T>
export const NO_INACTIVE_OR_BASE_ERROR =
"A toggleable object must have an inactive state, or a base property."
export const NO_ACTIVE_ERROR = "A toggleable object must have an active state."
interface ToggleableProps<T> {
base?: T
state: Partial<Record<ToggleState, DeepPartial<T>>>
}
/**
* Helper function for creating Toggleable objects.
* @template T The type of the object being toggled.
* @param props Object containing the base (inactive) state and state modifications to create the active state.
* @returns A Toggleable object containing both the inactive and active states.
* @example
* ```
* toggleable({
* base: { background: "#000000", text: "#CCCCCC" },
* state: { active: { text: "#CCCCCC" } },
* })
* ```
*/
export function toggleable<T extends object>(
props: ToggleableProps<T>
): Toggleable<T> {
const { base, state } = props
if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
if (!state.active) throw new Error(NO_ACTIVE_ERROR)
const inactiveState = base
? ((state.inactive ? merge(base, state.inactive) : base) as T)
: (state.inactive as T)
const toggleObj: Toggleable<T> = {
inactive: inactiveState,
active: merge(base ?? {}, state.active) as T,
}
return toggleObj
}

View file

@ -1,4 +1,3 @@
import { text } from "./components"
import contactFinder from "./contactFinder" import contactFinder from "./contactFinder"
import contactsPopover from "./contactsPopover" import contactsPopover from "./contactsPopover"
import commandPalette from "./commandPalette" import commandPalette from "./commandPalette"

View file

@ -1,6 +1,7 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { text, border, background, foreground } from "./components" import { text, border, background, foreground } from "./components"
import editor from "./editor" import editor from "./editor"
import { interactive } from "../element"
export default function assistant(colorScheme: ColorScheme) { export default function assistant(colorScheme: ColorScheme) {
const layer = colorScheme.highest const layer = colorScheme.highest
@ -15,83 +16,104 @@ export default function assistant(colorScheme: ColorScheme) {
background: editor(colorScheme).background, background: editor(colorScheme).background,
}, },
hamburgerButton: { hamburgerButton: {
icon: { icon: {
color: text(layer, "sans", "default", { size: "sm" }).color, color: text(layer, "sans", "default", { size: "sm" }).color,
asset: "icons/hamburger_15.svg", asset: "icons/hamburger_15.svg",
dimensions: { dimensions: {
width: 15, width: 15,
height: 15, height: 15,
},
}, },
}, container: {
container: { margin: { left: 12 },
margin: { left: 12 }, }
}
}, },
zoomInButton: { zoomInButton: {
icon: { icon: {
color: text(layer, "sans", "default", { size: "sm" }).color, color: text(layer, "sans", "default", { size: "sm" }).color,
asset: "icons/maximize_8.svg", asset: "icons/maximize_8.svg",
dimensions: { dimensions: {
width: 12, width: 12,
height: 12, height: 12,
},
}, },
}, container: {
container: { margin: { right: 12 },
margin: { right: 12 }, }
}
}, },
zoomOutButton: { zoomOutButton: {
icon: { icon: {
color: text(layer, "sans", "default", { size: "sm" }).color, color: text(layer, "sans", "default", { size: "sm" }).color,
asset: "icons/minimize_8.svg", asset: "icons/minimize_8.svg",
dimensions: { dimensions: {
width: 12, width: 12,
height: 12, height: 12,
},
}, },
}, container: {
container: { margin: { right: 12 },
margin: { right: 12 }, }
}
}, },
plusButton: { plusButton: {
icon: { icon: {
color: text(layer, "sans", "default", { size: "sm" }).color, color: text(layer, "sans", "default", { size: "sm" }).color,
asset: "icons/plus_12.svg", asset: "icons/plus_12.svg",
dimensions: { dimensions: {
width: 12, width: 12,
height: 12, height: 12,
},
}, },
}, container: {
container: { margin: { right: 12 },
margin: { right: 12 }, }
}
}, },
title: { title: {
margin: { left: 12 }, margin: { left: 12 },
...text(layer, "sans", "default", { size: "sm" }) ...text(layer, "sans", "default", { size: "sm" })
}, },
savedConversation: { savedConversation: {
background: background(layer, "on"), container: interactive({
hover: { base: {
background: background(layer, "on", "hovered"), background: background(layer, "on"),
}, },
savedAt: { state: {
margin: { left: 8 }, hovered: {
...text(layer, "sans", "default", { size: "xs" }), background: background(layer, "on", "hovered"),
}, }
title: { },
margin: { left: 8 }, }),
...text(layer, "sans", "default", { size: "sm", weight: "bold" }), savedAt: {
} margin: { left: 8 },
...text(layer, "sans", "default", { size: "xs" }),
},
title: {
margin: { left: 8 },
...text(layer, "sans", "default", { size: "sm", weight: "bold" }),
}
}, },
userSender: { userSender: {
...text(layer, "sans", "default", { size: "sm", weight: "bold" }), default: {
...text(layer, "sans", "default", {
size: "sm",
weight: "bold",
}),
},
}, },
assistantSender: { assistantSender: {
...text(layer, "sans", "accent", { size: "sm", weight: "bold" }), default: {
...text(layer, "sans", "accent", {
size: "sm",
weight: "bold",
}),
},
}, },
systemSender: { systemSender: {
...text(layer, "sans", "variant", { size: "sm", weight: "bold" }), default: {
...text(layer, "sans", "variant", {
size: "sm",
weight: "bold",
}),
},
}, },
sentAt: { sentAt: {
margin: { top: 2, left: 8 }, margin: { top: 2, left: 8 },
@ -100,17 +122,21 @@ export default function assistant(colorScheme: ColorScheme) {
modelInfoContainer: { modelInfoContainer: {
margin: { right: 16, top: 4 }, margin: { right: 16, top: 4 },
}, },
model: { model: interactive({
background: background(layer, "on"), base: {
margin: { right: 8 }, background: background(layer, "on"),
padding: 4, margin: { right: 8 },
cornerRadius: 4, padding: 4,
...text(layer, "sans", "default", { size: "xs" }), cornerRadius: 4,
hover: { ...text(layer, "sans", "default", { size: "xs" }),
background: background(layer, "on", "hovered"),
border: border(layer, "on", { overlay: true }),
}, },
}, state: {
hovered: {
background: background(layer, "on", "hovered"),
border: border(layer, "on", { overlay: true }),
},
},
}),
remainingTokens: { remainingTokens: {
margin: { right: 12 }, margin: { right: 12 },
...text(layer, "sans", "positive", { size: "xs" }), ...text(layer, "sans", "positive", { size: "xs" }),

View file

@ -1,12 +1,13 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color" import { withOpacity } from "../theme/color"
import { text, background } from "./components" import { text, background } from "./components"
import { toggleable } from "../element"
export default function commandPalette(colorScheme: ColorScheme) { export default function commandPalette(colorScheme: ColorScheme) {
let layer = colorScheme.highest let layer = colorScheme.highest
return {
keystrokeSpacing: 8, const key = toggleable({
key: { base: {
text: text(layer, "mono", "variant", "default", { size: "xs" }), text: text(layer, "mono", "variant", "default", { size: "xs" }),
cornerRadius: 2, cornerRadius: 2,
background: background(layer, "on"), background: background(layer, "on"),
@ -21,10 +22,21 @@ export default function commandPalette(colorScheme: ColorScheme) {
bottom: 1, bottom: 1,
left: 2, left: 2,
}, },
},
state: {
active: { active: {
text: text(layer, "mono", "on", "default", { size: "xs" }), text: text(layer, "mono", "on", "default", { size: "xs" }),
background: withOpacity(background(layer, "on"), 0.2), background: withOpacity(background(layer, "on"), 0.2),
}, },
}, },
})
return {
keystrokeSpacing: 8,
// TODO: This should be a Toggle<ContainedText> on the rust side so we don't have to do this
key: {
inactive: { ...key.inactive },
active: key.active,
},
} }
} }

View file

@ -85,7 +85,7 @@ export function foreground(
return getStyle(layer, styleSetOrStyles, style).foreground return getStyle(layer, styleSetOrStyles, style).foreground
} }
interface Text { interface Text extends Object {
family: keyof typeof fontFamilies family: keyof typeof fontFamilies
color: string color: string
size: number size: number

View file

@ -1,6 +1,6 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { background, border, borderColor, foreground, text } from "./components" import { background, border, borderColor, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
export default function contactsPanel(colorScheme: ColorScheme) { export default function contactsPanel(colorScheme: ColorScheme) {
const nameMargin = 8 const nameMargin = 8
const sidePadding = 12 const sidePadding = 12
@ -71,47 +71,85 @@ export default function contactsPanel(colorScheme: ColorScheme) {
}, },
rowHeight: 28, rowHeight: 28,
sectionIconSize: 8, sectionIconSize: 8,
headerRow: { headerRow: toggleable({
...text(layer, "mono", { size: "sm" }), base: interactive({
margin: { top: 14 }, base: {
padding: { ...text(layer, "mono", { size: "sm" }),
left: sidePadding, margin: { top: 14 },
right: sidePadding, padding: {
left: sidePadding,
right: sidePadding,
},
background: background(layer, "default"), // posiewic: breaking change
},
state: {
hovered: {
background: background(layer, "hovered"),
},
clicked: {
background: background(layer, "pressed"),
},
}, // hack, we want headerRow to be interactive for whatever reason. It probably shouldn't be interactive in the first place.
}),
state: {
active: {
default: {
...text(layer, "mono", "active", { size: "sm" }),
background: background(layer, "active"),
},
hovered: {
background: background(layer, "hovered"),
},
clicked: {
background: background(layer, "pressed"),
},
},
}, },
active: { }),
...text(layer, "mono", "active", { size: "sm" }), leaveCall: interactive({
background: background(layer, "active"), base: {
background: background(layer),
border: border(layer),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(layer, "sans", "variant", { size: "xs" }),
}, },
}, state: {
leaveCall: { hovered: {
background: background(layer), ...text(layer, "sans", "hovered", { size: "xs" }),
border: border(layer), background: background(layer, "hovered"),
cornerRadius: 6, border: border(layer, "hovered"),
margin: { },
top: 1,
}, },
padding: { }),
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(layer, "sans", "variant", { size: "xs" }),
hover: {
...text(layer, "sans", "hovered", { size: "xs" }),
background: background(layer, "hovered"),
border: border(layer, "hovered"),
},
},
contactRow: { contactRow: {
padding: { inactive: {
left: sidePadding, default: {
right: sidePadding, padding: {
left: sidePadding,
right: sidePadding,
},
},
}, },
active: { active: {
background: background(layer, "active"), default: {
background: background(layer, "active"),
padding: {
left: sidePadding,
right: sidePadding,
},
},
}, },
}, },
contactAvatar: { contactAvatar: {
cornerRadius: 10, cornerRadius: 10,
width: 18, width: 18,
@ -135,12 +173,14 @@ export default function contactsPanel(colorScheme: ColorScheme) {
}, },
}, },
contactButtonSpacing: nameMargin, contactButtonSpacing: nameMargin,
contactButton: { contactButton: interactive({
...contactButton, base: { ...contactButton },
hover: { state: {
background: background(layer, "hovered"), hovered: {
background: background(layer, "hovered"),
},
}, },
}, }),
disabledButton: { disabledButton: {
...contactButton, ...contactButton,
background: background(layer, "on"), background: background(layer, "on"),
@ -149,34 +189,52 @@ export default function contactsPanel(colorScheme: ColorScheme) {
callingIndicator: { callingIndicator: {
...text(layer, "mono", "variant", { size: "xs" }), ...text(layer, "mono", "variant", { size: "xs" }),
}, },
treeBranch: { treeBranch: toggleable({
color: borderColor(layer), base: interactive({
width: 1, base: {
hover: { color: borderColor(layer),
color: borderColor(layer), width: 1,
},
state: {
hovered: {
color: borderColor(layer),
},
},
}),
state: {
active: {
default: {
color: borderColor(layer),
},
},
}, },
active: { }),
color: borderColor(layer), projectRow: toggleable({
base: interactive({
base: {
...projectRow,
background: background(layer),
icon: {
margin: { left: nameMargin },
color: foreground(layer, "variant"),
width: 12,
},
name: {
...projectRow.name,
...text(layer, "mono", { size: "sm" }),
},
},
state: {
hovered: {
background: background(layer, "hovered"),
},
},
}),
state: {
active: {
default: { background: background(layer, "active") },
},
}, },
}, }),
projectRow: {
...projectRow,
background: background(layer),
icon: {
margin: { left: nameMargin },
color: foreground(layer, "variant"),
width: 12,
},
name: {
...projectRow.name,
...text(layer, "mono", { size: "sm" }),
},
hover: {
background: background(layer, "hovered"),
},
active: {
background: background(layer, "active"),
},
},
} }
} }

View file

@ -1,6 +1,6 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { background, foreground, text } from "./components" import { background, foreground, text } from "./components"
import { interactive } from "../element"
const avatarSize = 12 const avatarSize = 12
const headerPadding = 8 const headerPadding = 8
@ -21,24 +21,32 @@ export default function contactNotification(colorScheme: ColorScheme): Object {
...text(layer, "sans", { size: "xs" }), ...text(layer, "sans", { size: "xs" }),
margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 }, margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
}, },
button: { button: interactive({
...text(layer, "sans", "on", { size: "xs" }), base: {
background: background(layer, "on"), ...text(layer, "sans", "on", { size: "xs" }),
padding: 4, background: background(layer, "on"),
cornerRadius: 6, padding: 4,
margin: { left: 6 }, cornerRadius: 6,
hover: { margin: { left: 6 },
background: background(layer, "on", "hovered"),
}, },
},
state: {
hovered: {
background: background(layer, "on", "hovered"),
},
},
}),
dismissButton: { dismissButton: {
color: foreground(layer, "variant"), default: {
iconWidth: 8, color: foreground(layer, "variant"),
iconHeight: 8, iconWidth: 8,
buttonWidth: 8, iconHeight: 8,
buttonHeight: 8, buttonWidth: 8,
hover: { buttonHeight: 8,
color: foreground(layer, "hovered"), hover: {
color: foreground(layer, "hovered"),
},
}, },
}, },
} }

View file

@ -1,5 +1,6 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { background, border, borderColor, text } from "./components" import { background, border, borderColor, text } from "./components"
import { interactive, toggleable } from "../element"
export default function contextMenu(colorScheme: ColorScheme) { export default function contextMenu(colorScheme: ColorScheme) {
let layer = colorScheme.middle let layer = colorScheme.middle
@ -10,37 +11,54 @@ export default function contextMenu(colorScheme: ColorScheme) {
shadow: colorScheme.popoverShadow, shadow: colorScheme.popoverShadow,
border: border(layer), border: border(layer),
keystrokeMargin: 30, keystrokeMargin: 30,
item: { item: toggleable({
iconSpacing: 8, base: interactive({
iconWidth: 14, base: {
padding: { left: 6, right: 6, top: 2, bottom: 2 }, iconSpacing: 8,
cornerRadius: 6, iconWidth: 14,
label: text(layer, "sans", { size: "sm" }), padding: { left: 6, right: 6, top: 2, bottom: 2 },
keystroke: { cornerRadius: 6,
...text(layer, "sans", "variant", { label: text(layer, "sans", { size: "sm" }),
size: "sm", keystroke: {
weight: "bold", ...text(layer, "sans", "variant", {
}), size: "sm",
padding: { left: 3, right: 3 }, weight: "bold",
}, }),
hover: { padding: { left: 3, right: 3 },
background: background(layer, "hovered"), },
label: text(layer, "sans", "hovered", { size: "sm" }), },
keystroke: { state: {
...text(layer, "sans", "hovered", { hovered: {
size: "sm", background: background(layer, "hovered"),
weight: "bold", label: text(layer, "sans", "hovered", { size: "sm" }),
}), keystroke: {
padding: { left: 3, right: 3 }, ...text(layer, "sans", "hovered", {
size: "sm",
weight: "bold",
}),
padding: { left: 3, right: 3 },
},
},
clicked: {
background: background(layer, "pressed"),
},
},
}),
state: {
active: {
default: {
background: background(layer, "active"),
},
hovered: {
background: background(layer, "hovered"),
},
clicked: {
background: background(layer, "pressed"),
},
}, },
}, },
active: { }),
background: background(layer, "active"),
},
activeHover: {
background: background(layer, "active"),
},
},
separator: { separator: {
background: borderColor(layer), background: borderColor(layer),
margin: { top: 2, bottom: 2 }, margin: { top: 2, bottom: 2 },

View file

@ -1,60 +1,69 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { background, border, foreground, svg, text } from "./components" import { background, border, foreground, svg, text } from "./components"
import { interactive } from "../element"
export default function copilot(colorScheme: ColorScheme) { export default function copilot(colorScheme: ColorScheme) {
let layer = colorScheme.middle let layer = colorScheme.middle
let content_width = 264 let content_width = 264
let ctaButton = { let ctaButton =
// Copied from welcome screen. FIXME: Move this into a ZDS component // Copied from welcome screen. FIXME: Move this into a ZDS component
background: background(layer), interactive({
border: border(layer, "default"), base: {
cornerRadius: 4, background: background(layer),
margin: { border: border(layer, "default"),
top: 4, cornerRadius: 4,
bottom: 4, margin: {
left: 8, top: 4,
right: 8, bottom: 4,
}, left: 8,
padding: { right: 8,
top: 3, },
bottom: 3, padding: {
left: 7, top: 3,
right: 7, bottom: 3,
}, left: 7,
...text(layer, "sans", "default", { size: "sm" }), right: 7,
hover: { },
...text(layer, "sans", "default", { size: "sm" }), ...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "hovered"), },
border: border(layer, "active"), state: {
}, hovered: {
} ...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "hovered"),
border: border(layer, "active"),
},
},
})
return { return {
outLinkIcon: { outLinkIcon: interactive({
icon: svg( base: {
foreground(layer, "variant"),
"icons/link_out_12.svg",
12,
12
),
container: {
cornerRadius: 6,
padding: { left: 6 },
},
hover: {
icon: svg( icon: svg(
foreground(layer, "hovered"), foreground(layer, "variant"),
"icons/link_out_12.svg", "icons/link_out_12.svg",
12, 12,
12 12
), ),
container: {
cornerRadius: 6,
padding: { left: 6 },
},
}, },
}, state: {
hovered: {
icon: {
color: foreground(layer, "hovered"),
},
},
},
}),
modal: { modal: {
titleText: { titleText: {
...text(layer, "sans", { size: "xs", weight: "bold" }), default: {
...text(layer, "sans", { size: "xs", weight: "bold" }),
},
}, },
titlebar: { titlebar: {
background: background(colorScheme.lowest), background: background(colorScheme.lowest),
@ -75,42 +84,46 @@ export default function copilot(colorScheme: ColorScheme) {
bottom: 8, bottom: 8,
}, },
}, },
closeIcon: { closeIcon: interactive({
icon: svg( base: {
foreground(layer, "variant"),
"icons/x_mark_8.svg",
8,
8
),
container: {
cornerRadius: 2,
padding: {
top: 4,
bottom: 4,
left: 4,
right: 4,
},
margin: {
right: 0,
},
},
hover: {
icon: svg( icon: svg(
foreground(layer, "on"), foreground(layer, "variant"),
"icons/x_mark_8.svg", "icons/x_mark_8.svg",
8, 8,
8 8
), ),
container: {
cornerRadius: 2,
padding: {
top: 4,
bottom: 4,
left: 4,
right: 4,
},
margin: {
right: 0,
},
},
}, },
clicked: { state: {
icon: svg( hovered: {
foreground(layer, "base"), icon: svg(
"icons/x_mark_8.svg", foreground(layer, "on"),
8, "icons/x_mark_8.svg",
8 8,
), 8
),
},
clicked: {
icon: svg(
foreground(layer, "base"),
"icons/x_mark_8.svg",
8,
8
),
},
}, },
}, }),
dimensions: { dimensions: {
width: 280, width: 280,
height: 280, height: 280,
@ -185,28 +198,32 @@ export default function copilot(colorScheme: ColorScheme) {
}, },
}, },
right: (content_width * 1) / 3, right: (content_width * 1) / 3,
rightContainer: { rightContainer: interactive({
border: border(colorScheme.lowest, "inverted", { base: {
bottom: false, border: border(colorScheme.lowest, "inverted", {
right: false,
top: false,
left: true,
}),
padding: {
top: 3,
bottom: 5,
left: 8,
right: 0,
},
hover: {
border: border(layer, "active", {
bottom: false, bottom: false,
right: false, right: false,
top: false, top: false,
left: true, left: true,
}), }),
padding: {
top: 3,
bottom: 5,
left: 8,
right: 0,
},
}, },
}, state: {
hovered: {
border: border(layer, "active", {
bottom: false,
right: false,
top: false,
left: true,
}),
},
},
}),
}, },
}, },

View file

@ -4,6 +4,7 @@ import { background, border, borderColor, foreground, text } from "./components"
import hoverPopover from "./hoverPopover" import hoverPopover from "./hoverPopover"
import { buildSyntax } from "../theme/syntax" import { buildSyntax } from "../theme/syntax"
import { interactive, toggleable } from "../element"
export default function editor(colorScheme: ColorScheme) { export default function editor(colorScheme: ColorScheme) {
const { isLight } = colorScheme const { isLight } = colorScheme
@ -48,46 +49,76 @@ export default function editor(colorScheme: ColorScheme) {
// Inline autocomplete suggestions, Co-pilot suggestions, etc. // Inline autocomplete suggestions, Co-pilot suggestions, etc.
suggestion: syntax.predictive, suggestion: syntax.predictive,
codeActions: { codeActions: {
indicator: { indicator: toggleable({
color: foreground(layer, "variant"), base: interactive({
base: {
color: foreground(layer, "variant"),
},
state: {
hovered: {
color: foreground(layer, "variant", "hovered"),
},
clicked: {
color: foreground(layer, "variant", "pressed"),
},
},
}),
state: {
active: {
default: {
color: foreground(layer, "accent"),
},
hovered: {
color: foreground(layer, "accent", "hovered"),
},
clicked: {
color: foreground(layer, "accent", "pressed"),
},
},
},
}),
clicked: {
color: foreground(layer, "base"),
},
hover: {
color: foreground(layer, "on"),
},
active: {
color: foreground(layer, "on"),
},
},
verticalScale: 0.55, verticalScale: 0.55,
}, },
folds: { folds: {
iconMarginScale: 2.5, iconMarginScale: 2.5,
foldedIcon: "icons/chevron_right_8.svg", foldedIcon: "icons/chevron_right_8.svg",
foldableIcon: "icons/chevron_down_8.svg", foldableIcon: "icons/chevron_down_8.svg",
indicator: { indicator: toggleable({
color: foreground(layer, "variant"), base: interactive({
base: {
clicked: { color: foreground(layer, "variant"),
color: foreground(layer, "base"), },
state: {
hovered: {
color: foreground(layer, "on"),
},
clicked: {
color: foreground(layer, "base"),
},
},
}),
state: {
active: {
default: {
color: foreground(layer, "default"),
},
hovered: {
color: foreground(layer, "variant"),
},
},
}, },
hover: { }),
color: foreground(layer, "on"),
},
active: {
color: foreground(layer, "on"),
},
},
ellipses: { ellipses: {
textColor: colorScheme.ramps.neutral(0.71).hex(), textColor: colorScheme.ramps.neutral(0.71).hex(),
cornerRadiusFactor: 0.15, cornerRadiusFactor: 0.15,
background: { background: {
// Copied from hover_popover highlight // Copied from hover_popover highlight
color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(), default: {
color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(),
},
hover: { hovered: {
color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(), color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(),
}, },
@ -223,21 +254,26 @@ export default function editor(colorScheme: ColorScheme) {
color: syntax.linkUri.color, color: syntax.linkUri.color,
underline: syntax.linkUri.underline, underline: syntax.linkUri.underline,
}, },
jumpIcon: { jumpIcon: interactive({
color: foreground(layer, "on"), base: {
iconWidth: 20, color: foreground(layer, "on"),
buttonWidth: 20, iconWidth: 20,
cornerRadius: 6, buttonWidth: 20,
padding: { cornerRadius: 6,
top: 6, padding: {
bottom: 6, top: 6,
left: 6, bottom: 6,
right: 6, left: 6,
right: 6,
},
}, },
hover: { state: {
background: background(layer, "on", "hovered"), hovered: {
background: background(layer, "on", "hovered"),
},
}, },
}, }),
scrollbar: { scrollbar: {
width: 12, width: 12,
minHeightFactor: 1.0, minHeightFactor: 1.0,

View file

@ -1,35 +1,40 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { background, border, text } from "./components" import { background, border, text } from "./components"
import { interactive } from "../element"
export default function feedback(colorScheme: ColorScheme) { export default function feedback(colorScheme: ColorScheme) {
let layer = colorScheme.highest let layer = colorScheme.highest
return { return {
submit_button: { submit_button: interactive({
...text(layer, "mono", "on"), base: {
background: background(layer, "on"), ...text(layer, "mono", "on"),
cornerRadius: 6, background: background(layer, "on"),
border: border(layer, "on"), cornerRadius: 6,
margin: { border: border(layer, "on"),
right: 4, margin: {
right: 4,
},
padding: {
bottom: 2,
left: 10,
right: 10,
top: 2,
},
}, },
padding: { state: {
bottom: 2, clicked: {
left: 10, ...text(layer, "mono", "on", "pressed"),
right: 10, background: background(layer, "on", "pressed"),
top: 2, border: border(layer, "on", "pressed"),
},
hovered: {
...text(layer, "mono", "on", "hovered"),
background: background(layer, "on", "hovered"),
border: border(layer, "on", "hovered"),
},
}, },
clicked: { }),
...text(layer, "mono", "on", "pressed"),
background: background(layer, "on", "pressed"),
border: border(layer, "on", "pressed"),
},
hover: {
...text(layer, "mono", "on", "hovered"),
background: background(layer, "on", "hovered"),
border: border(layer, "on", "hovered"),
},
},
button_margin: 8, button_margin: 8,
info_text_default: text(layer, "sans", "default", { size: "xs" }), info_text_default: text(layer, "sans", "default", { size: "xs" }),
link_text_default: text(layer, "sans", "default", { link_text_default: text(layer, "sans", "default", {

View file

@ -1,6 +1,7 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color" import { withOpacity } from "../theme/color"
import { background, border, text } from "./components" import { background, border, text } from "./components"
import { interactive, toggleable } from "../element"
export default function picker(colorScheme: ColorScheme): any { export default function picker(colorScheme: ColorScheme): any {
let layer = colorScheme.lowest let layer = colorScheme.lowest
@ -38,35 +39,65 @@ export default function picker(colorScheme: ColorScheme): any {
...container, ...container,
padding: {}, padding: {},
}, },
item: { item: toggleable({
padding: { base: interactive({
bottom: 4, base: {
left: 12, padding: {
right: 12, bottom: 4,
top: 4, left: 12,
right: 12,
top: 4,
},
margin: {
top: 1,
left: 4,
right: 4,
},
cornerRadius: 8,
text: text(layer, "sans", "variant"),
highlightText: text(layer, "sans", "accent", {
weight: "bold",
}),
},
state: {
hovered: {
background: withOpacity(
background(layer, "hovered"),
0.5
),
},
clicked: {
background: withOpacity(
background(layer, "pressed"),
0.5
),
},
},
}),
state: {
active: {
default: {
background: withOpacity(
background(layer, "base", "active"),
0.5
),
},
hovered: {
background: withOpacity(
background(layer, "hovered"),
0.5
),
},
clicked: {
background: withOpacity(
background(layer, "pressed"),
0.5
),
},
},
}, },
margin: { }),
top: 1,
left: 4,
right: 4,
},
cornerRadius: 8,
text: text(layer, "sans", "variant"),
highlightText: text(layer, "sans", "accent", { weight: "bold" }),
active: {
background: withOpacity(
background(layer, "base", "active"),
0.5
),
text: text(layer, "sans", "base", "active"),
highlightText: text(layer, "sans", "accent", {
weight: "bold",
}),
},
hover: {
background: withOpacity(background(layer, "hovered"), 0.5),
},
},
inputEditor, inputEditor,
emptyInputEditor, emptyInputEditor,
noMatches: { noMatches: {

View file

@ -1,7 +1,7 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color" import { withOpacity } from "../theme/color"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
export default function projectPanel(colorScheme: ColorScheme) { export default function projectPanel(colorScheme: ColorScheme) {
const { isLight } = colorScheme const { isLight } = colorScheme
@ -28,48 +28,79 @@ export default function projectPanel(colorScheme: ColorScheme) {
}, },
} }
let entry = { const default_entry = interactive({
...baseEntry, base: {
text: text(layer, "mono", "variant", { size: "sm" }), ...baseEntry,
hover: { text: text(layer, "mono", "variant", { size: "sm" }),
background: background(layer, "variant", "hovered"), status,
}, },
active: { state: {
background: colorScheme.isLight default: {
? withOpacity(background(layer, "active"), 0.5) background: background(layer),
: background(layer, "active"), },
text: text(layer, "mono", "active", { size: "sm" }), hovered: {
background: background(layer, "variant", "hovered"),
},
clicked: {
background: background(layer, "variant", "pressed"),
},
}, },
activeHover: { })
background: background(layer, "active"),
text: text(layer, "mono", "active", { size: "sm" }), let entry = toggleable({
base: default_entry,
state: {
active: interactive({
base: {
...default_entry,
},
state: {
default: {
background: background(colorScheme.lowest),
},
hovered: {
background: background(colorScheme.lowest, "hovered"),
},
clicked: {
background: background(colorScheme.lowest, "pressed"),
},
},
}),
}, },
status, })
}
return { return {
openProjectButton: { openProjectButton: interactive({
background: background(layer), base: {
border: border(layer, "active"), background: background(layer),
cornerRadius: 4,
margin: {
top: 16,
left: 16,
right: 16,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", { size: "sm" }),
hover: {
...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "hovered"),
border: border(layer, "active"), border: border(layer, "active"),
cornerRadius: 4,
margin: {
top: 16,
left: 16,
right: 16,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", { size: "sm" }),
}, },
}, state: {
hovered: {
...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "hovered"),
border: border(layer, "active"),
},
clicked: {
...text(layer, "sans", "default", { size: "sm" }),
background: background(layer, "pressed"),
border: border(layer, "active"),
},
},
}),
background: background(layer), background: background(layer),
padding: { left: 6, right: 6, top: 0, bottom: 6 }, padding: { left: 6, right: 6, top: 0, bottom: 6 },
indentWidth: 12, indentWidth: 12,
@ -94,8 +125,12 @@ export default function projectPanel(colorScheme: ColorScheme) {
...entry, ...entry,
text: text(layer, "mono", "disabled"), text: text(layer, "mono", "disabled"),
active: { active: {
background: background(layer, "active"), ...entry.active,
text: text(layer, "mono", "disabled", { size: "sm" }), default: {
...entry.active.default,
background: background(layer, "active"),
text: text(layer, "mono", "disabled", { size: "sm" }),
},
}, },
}, },
filenameEditor: { filenameEditor: {

View file

@ -1,6 +1,7 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color" import { withOpacity } from "../theme/color"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
export default function search(colorScheme: ColorScheme) { export default function search(colorScheme: ColorScheme) {
let layer = colorScheme.highest let layer = colorScheme.highest
@ -35,36 +36,50 @@ export default function search(colorScheme: ColorScheme) {
return { return {
// TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
matchBackground: withOpacity(foreground(layer, "accent"), 0.4), matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
optionButton: { optionButton: toggleable({
...text(layer, "mono", "on"), base: interactive({
background: background(layer, "on"), base: {
cornerRadius: 6, ...text(layer, "mono", "on"),
border: border(layer, "on"), background: background(layer, "on"),
margin: { cornerRadius: 6,
right: 4, border: border(layer, "on"),
margin: {
right: 4,
},
padding: {
bottom: 2,
left: 10,
right: 10,
top: 2,
},
},
state: {
hovered: {
...text(layer, "mono", "on", "hovered"),
background: background(layer, "on", "hovered"),
border: border(layer, "on", "hovered"),
},
clicked: {
...text(layer, "mono", "on", "pressed"),
background: background(layer, "on", "pressed"),
border: border(layer, "on", "pressed"),
},
},
}),
state: {
active: {
default: {
...text(layer, "mono", "accent"),
},
hovered: {
...text(layer, "mono", "accent", "hovered"),
},
clicked: {
...text(layer, "mono", "accent", "pressed"),
},
},
}, },
padding: { }),
bottom: 2,
left: 10,
right: 10,
top: 2,
},
active: {
...text(layer, "mono", "on", "inverted"),
background: background(layer, "on", "inverted"),
border: border(layer, "on", "inverted"),
},
clicked: {
...text(layer, "mono", "on", "pressed"),
background: background(layer, "on", "pressed"),
border: border(layer, "on", "pressed"),
},
hover: {
...text(layer, "mono", "on", "hovered"),
background: background(layer, "on", "hovered"),
border: border(layer, "on", "hovered"),
},
},
editor, editor,
invalidEditor: { invalidEditor: {
...editor, ...editor,
@ -97,17 +112,24 @@ export default function search(colorScheme: ColorScheme) {
...text(layer, "mono", "on"), ...text(layer, "mono", "on"),
size: 18, size: 18,
}, },
dismissButton: { dismissButton: interactive({
color: foreground(layer, "variant"), base: {
iconWidth: 12, color: foreground(layer, "variant"),
buttonWidth: 14, iconWidth: 12,
padding: { buttonWidth: 14,
left: 10, padding: {
right: 10, left: 10,
right: 10,
},
}, },
hover: { state: {
color: foreground(layer, "hovered"), hovered: {
color: foreground(layer, "hovered"),
},
clicked: {
color: foreground(layer, "pressed"),
},
}, },
}, }),
} }
} }

View file

@ -1,5 +1,6 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { interactive } from "../element"
const headerPadding = 8 const headerPadding = 8
@ -12,33 +13,41 @@ export default function simpleMessageNotification(
...text(layer, "sans", { size: "xs" }), ...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, right: headerPadding }, margin: { left: headerPadding, right: headerPadding },
}, },
actionMessage: { actionMessage: interactive({
...text(layer, "sans", { size: "xs" }), base: {
border: border(layer, "active"), ...text(layer, "sans", { size: "xs" }),
cornerRadius: 4,
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
margin: { left: headerPadding, top: 6, bottom: 6 },
hover: {
...text(layer, "sans", "default", { size: "xs" }),
background: background(layer, "hovered"),
border: border(layer, "active"), border: border(layer, "active"),
cornerRadius: 4,
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
margin: { left: headerPadding, top: 6, bottom: 6 },
}, },
}, state: {
dismissButton: { hovered: {
color: foreground(layer), ...text(layer, "sans", "default", { size: "xs" }),
iconWidth: 8, background: background(layer, "hovered"),
iconHeight: 8, border: border(layer, "active"),
buttonWidth: 8, },
buttonHeight: 8,
hover: {
color: foreground(layer, "hovered"),
}, },
}, }),
dismissButton: interactive({
base: {
color: foreground(layer),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
},
state: {
hovered: {
color: foreground(layer, "hovered"),
},
},
}),
} }
} }

View file

@ -1,6 +1,6 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
export default function statusBar(colorScheme: ColorScheme) { export default function statusBar(colorScheme: ColorScheme) {
let layer = colorScheme.lowest let layer = colorScheme.lowest
@ -25,95 +25,123 @@ export default function statusBar(colorScheme: ColorScheme) {
}, },
border: border(layer, { top: true, overlay: true }), border: border(layer, { top: true, overlay: true }),
cursorPosition: text(layer, "sans", "variant"), cursorPosition: text(layer, "sans", "variant"),
activeLanguage: { activeLanguage: interactive({
padding: { left: 6, right: 6 }, base: {
...text(layer, "sans", "variant"), padding: { left: 6, right: 6 },
hover: { ...text(layer, "sans", "variant"),
...text(layer, "sans", "on"),
}, },
}, state: {
hovered: {
...text(layer, "sans", "on"),
},
},
}),
autoUpdateProgressMessage: text(layer, "sans", "variant"), autoUpdateProgressMessage: text(layer, "sans", "variant"),
autoUpdateDoneMessage: text(layer, "sans", "variant"), autoUpdateDoneMessage: text(layer, "sans", "variant"),
lspStatus: { lspStatus: interactive({
...diagnosticStatusContainer, base: {
iconSpacing: 4, ...diagnosticStatusContainer,
iconWidth: 14, iconSpacing: 4,
height: 18, iconWidth: 14,
message: text(layer, "sans"), height: 18,
iconColor: foreground(layer),
hover: {
message: text(layer, "sans"), message: text(layer, "sans"),
iconColor: foreground(layer), iconColor: foreground(layer),
background: background(layer, "hovered"),
}, },
}, state: {
diagnosticMessage: { hovered: {
...text(layer, "sans"), message: text(layer, "sans"),
hover: text(layer, "sans", "hovered"), iconColor: foreground(layer),
}, background: background(layer, "hovered"),
diagnosticSummary: { },
height: 20,
iconWidth: 16,
iconSpacing: 2,
summarySpacing: 6,
text: text(layer, "sans", { size: "sm" }),
iconColorOk: foreground(layer, "variant"),
iconColorWarning: foreground(layer, "warning"),
iconColorError: foreground(layer, "negative"),
containerOk: {
cornerRadius: 6,
padding: { top: 3, bottom: 3, left: 7, right: 7 },
}, },
containerWarning: { }),
...diagnosticStatusContainer, diagnosticMessage: interactive({
background: background(layer, "warning"), base: {
border: border(layer, "warning"), ...text(layer, "sans"),
}, },
containerError: { state: { hovered: text(layer, "sans", "hovered") },
...diagnosticStatusContainer, }),
background: background(layer, "negative"), diagnosticSummary: interactive({
border: border(layer, "negative"), base: {
}, height: 20,
hover: { iconWidth: 16,
iconColorOk: foreground(layer, "on"), iconSpacing: 2,
summarySpacing: 6,
text: text(layer, "sans", { size: "sm" }),
iconColorOk: foreground(layer, "variant"),
iconColorWarning: foreground(layer, "warning"),
iconColorError: foreground(layer, "negative"),
containerOk: { containerOk: {
cornerRadius: 6, cornerRadius: 6,
padding: { top: 3, bottom: 3, left: 7, right: 7 }, padding: { top: 3, bottom: 3, left: 7, right: 7 },
background: background(layer, "on", "hovered"),
}, },
containerWarning: { containerWarning: {
...diagnosticStatusContainer, ...diagnosticStatusContainer,
background: background(layer, "warning", "hovered"), background: background(layer, "warning"),
border: border(layer, "warning", "hovered"), border: border(layer, "warning"),
}, },
containerError: { containerError: {
...diagnosticStatusContainer, ...diagnosticStatusContainer,
background: background(layer, "negative", "hovered"), background: background(layer, "negative"),
border: border(layer, "negative", "hovered"), border: border(layer, "negative"),
}, },
}, },
}, state: {
hovered: {
iconColorOk: foreground(layer, "on"),
containerOk: {
background: background(layer, "on", "hovered"),
},
containerWarning: {
background: background(layer, "warning", "hovered"),
border: border(layer, "warning", "hovered"),
},
containerError: {
background: background(layer, "negative", "hovered"),
border: border(layer, "negative", "hovered"),
},
},
},
}),
panelButtons: { panelButtons: {
groupLeft: {}, groupLeft: {},
groupBottom: {}, groupBottom: {},
groupRight: {}, groupRight: {},
button: { button: toggleable({
...statusContainer, base: interactive({
iconSize: 16, base: {
iconColor: foreground(layer, "variant"), ...statusContainer,
label: { iconSize: 16,
margin: { left: 6 }, iconColor: foreground(layer, "variant"),
...text(layer, "sans", { size: "sm" }), label: {
margin: { left: 6 },
...text(layer, "sans", { size: "sm" }),
},
},
state: {
hovered: {
iconColor: foreground(layer, "hovered"),
background: background(layer, "variant"),
},
},
}),
state: {
active: {
default: {
iconColor: foreground(layer, "active"),
background: background(layer, "active"),
},
hovered: {
iconColor: foreground(layer, "hovered"),
background: background(layer, "hovered"),
},
clicked: {
iconColor: foreground(layer, "pressed"),
background: background(layer, "pressed"),
},
},
}, },
hover: { }),
iconColor: foreground(layer, "hovered"),
background: background(layer, "variant"),
},
active: {
iconColor: foreground(layer, "active"),
background: background(layer, "active"),
},
},
badge: { badge: {
cornerRadius: 3, cornerRadius: 3,
padding: 2, padding: 2,

View file

@ -1,6 +1,7 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color" import { withOpacity } from "../theme/color"
import { text, border, background, foreground } from "./components" import { text, border, background, foreground } from "./components"
import { interactive, toggleable } from "../element"
export default function tabBar(colorScheme: ColorScheme) { export default function tabBar(colorScheme: ColorScheme) {
const height = 32 const height = 32
@ -87,17 +88,36 @@ export default function tabBar(colorScheme: ColorScheme) {
inactiveTab: inactivePaneInactiveTab, inactiveTab: inactivePaneInactiveTab,
}, },
draggedTab, draggedTab,
paneButton: { paneButton: toggleable({
color: foreground(layer, "variant"), base: interactive({
iconWidth: 12, base: {
buttonWidth: activePaneActiveTab.height, color: foreground(layer, "variant"),
hover: { iconWidth: 12,
color: foreground(layer, "hovered"), buttonWidth: activePaneActiveTab.height,
},
state: {
hovered: {
color: foreground(layer, "hovered"),
},
clicked: {
color: foreground(layer, "pressed"),
},
},
}),
state: {
active: {
default: {
color: foreground(layer, "accent"),
},
hovered: {
color: foreground(layer, "hovered"),
},
clicked: {
color: foreground(layer, "pressed"),
},
},
}, },
active: { }),
color: foreground(layer, "accent"),
},
},
paneButtonContainer: { paneButtonContainer: {
background: tab.background, background: tab.background,
border: { border: {

View file

@ -0,0 +1,47 @@
import merge from "ts-deepmerge"
type ToggleState = "inactive" | "active"
type Toggleable<T> = Record<ToggleState, T>
const NO_INACTIVE_OR_BASE_ERROR =
"A toggleable object must have an inactive state, or a base property."
const NO_ACTIVE_ERROR = "A toggleable object must have an active state."
interface ToggleableProps<T> {
base?: T
state: Partial<Record<ToggleState, T>>
}
/**
* Helper function for creating Toggleable objects.
* @template T The type of the object being toggled.
* @param props Object containing the base (inactive) state and state modifications to create the active state.
* @returns A Toggleable object containing both the inactive and active states.
* @example
* ```
* toggleable({
* base: { background: "#000000", text: "#CCCCCC" },
* state: { active: { text: "#CCCCCC" } },
* })
* ```
*/
export function toggleable<T extends object>(
props: ToggleableProps<T>
): Toggleable<T> {
const { base, state } = props
if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
if (!state.active) throw new Error(NO_ACTIVE_ERROR)
const inactiveState = base
? ((state.inactive ? merge(base, state.inactive) : base) as T)
: (state.inactive as T)
const toggleObj: Toggleable<T> = {
inactive: inactiveState,
active: merge(base ?? {}, state.active) as T,
}
return toggleObj
}

View file

@ -1,6 +1,6 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { background, border, text } from "./components" import { background, border, text } from "./components"
import { interactive, toggleable } from "../element"
export default function dropdownMenu(colorScheme: ColorScheme) { export default function dropdownMenu(colorScheme: ColorScheme) {
let layer = colorScheme.middle let layer = colorScheme.middle
@ -9,38 +9,56 @@ export default function dropdownMenu(colorScheme: ColorScheme) {
background: background(layer), background: background(layer),
border: border(layer), border: border(layer),
shadow: colorScheme.popoverShadow, shadow: colorScheme.popoverShadow,
header: { header: interactive({
...text(layer, "sans", { size: "sm" }), base: {
secondaryText: text(layer, "sans", { size: "sm", color: "#aaaaaa" }), ...text(layer, "sans", { size: "sm" }),
secondaryTextSpacing: 10, secondaryText: text(layer, "sans", {
padding: { left: 8, right: 8, top: 2, bottom: 2 }, size: "sm",
cornerRadius: 6, color: "#aaaaaa",
background: background(layer, "on"), }),
border: border(layer, "on", { overlay: true }), secondaryTextSpacing: 10,
hover: { padding: { left: 8, right: 8, top: 2, bottom: 2 },
background: background(layer, "hovered"), cornerRadius: 6,
...text(layer, "sans", "hovered", { size: "sm" }), background: background(layer, "on"),
} },
}, state: {
hovered: {
background: background(layer, "hovered"),
},
clicked: {
background: background(layer, "pressed"),
},
},
}),
sectionHeader: { sectionHeader: {
...text(layer, "sans", { size: "sm" }), ...text(layer, "sans", { size: "sm" }),
padding: { left: 8, right: 8, top: 8, bottom: 8 }, padding: { left: 8, right: 8, top: 8, bottom: 8 },
}, },
item: { item: toggleable({
...text(layer, "sans", { size: "sm" }), base: interactive({
secondaryTextSpacing: 10, base: {
secondaryText: text(layer, "sans", { size: "sm" }), ...text(layer, "sans", { size: "sm" }),
padding: { left: 18, right: 18, top: 2, bottom: 2 }, secondaryTextSpacing: 10,
hover: { secondaryText: text(layer, "sans", { size: "sm" }),
background: background(layer, "hovered"), padding: { left: 18, right: 18, top: 2, bottom: 2 },
...text(layer, "sans", "hovered", { size: "sm" }), },
state: {
hovered: {
background: background(layer, "hovered"),
...text(layer, "sans", "hovered", { size: "sm" }),
},
},
}),
state: {
active: {
default: {
background: background(layer, "active"),
},
hovered: {
background: background(layer, "hovered"),
},
},
}, },
active: { }),
background: background(layer, "active"),
},
activeHover: {
background: background(layer, "active"),
},
},
} }
} }

View file

@ -1,5 +1,6 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { foreground, text } from "./components" import { foreground, text } from "./components"
import { interactive } from "../element"
const headerPadding = 8 const headerPadding = 8
@ -10,22 +11,30 @@ export default function updateNotification(colorScheme: ColorScheme): Object {
...text(layer, "sans", { size: "xs" }), ...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, right: headerPadding }, margin: { left: headerPadding, right: headerPadding },
}, },
actionMessage: { actionMessage: interactive({
...text(layer, "sans", { size: "xs" }), base: {
margin: { left: headerPadding, top: 6, bottom: 6 }, ...text(layer, "sans", { size: "xs" }),
hover: { margin: { left: headerPadding, top: 6, bottom: 6 },
color: foreground(layer, "hovered"),
}, },
}, state: {
dismissButton: { hovered: {
color: foreground(layer), color: foreground(layer, "hovered"),
iconWidth: 8, },
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: foreground(layer, "hovered"),
}, },
}, }),
dismissButton: interactive({
base: {
color: foreground(layer),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
},
state: {
hovered: {
color: foreground(layer, "hovered"),
},
},
}),
} }
} }

View file

@ -8,6 +8,7 @@ import {
TextProperties, TextProperties,
svg, svg,
} from "./components" } from "./components"
import { interactive } from "../element"
export default function welcome(colorScheme: ColorScheme) { export default function welcome(colorScheme: ColorScheme) {
let layer = colorScheme.highest let layer = colorScheme.highest
@ -63,27 +64,31 @@ export default function welcome(colorScheme: ColorScheme) {
bottom: 2, bottom: 2,
}, },
}, },
button: { button: interactive({
background: background(layer), base: {
border: border(layer, "active"), background: background(layer),
cornerRadius: 4,
margin: {
top: 4,
bottom: 4,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", interactive_text_size),
hover: {
...text(layer, "sans", "default", interactive_text_size),
background: background(layer, "hovered"),
border: border(layer, "active"), border: border(layer, "active"),
cornerRadius: 4,
margin: {
top: 4,
bottom: 4,
},
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
...text(layer, "sans", "default", interactive_text_size),
}, },
}, state: {
hovered: {
...text(layer, "sans", "default", interactive_text_size),
background: background(layer, "hovered"),
},
},
}),
usageNote: { usageNote: {
...text(layer, "sans", "variant", { size: "2xs" }), ...text(layer, "sans", "variant", { size: "2xs" }),
padding: { padding: {

View file

@ -1,5 +1,6 @@
import { ColorScheme } from "../theme/colorScheme" import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color" import { withOpacity } from "../theme/color"
import { toggleable } from "../element"
import { import {
background, background,
border, border,
@ -10,38 +11,53 @@ import {
} from "./components" } from "./components"
import statusBar from "./statusBar" import statusBar from "./statusBar"
import tabBar from "./tabBar" import tabBar from "./tabBar"
import { interactive } from "../element"
import merge from "ts-deepmerge"
export default function workspace(colorScheme: ColorScheme) { export default function workspace(colorScheme: ColorScheme) {
const layer = colorScheme.lowest const layer = colorScheme.lowest
const isLight = colorScheme.isLight const isLight = colorScheme.isLight
const itemSpacing = 8 const itemSpacing = 8
const titlebarButton = { const titlebarButton = toggleable({
cornerRadius: 6, base: interactive({
padding: { base: {
top: 1, cornerRadius: 6,
bottom: 1, padding: {
left: 8, top: 1,
right: 8, bottom: 1,
left: 8,
right: 8,
},
...text(layer, "sans", "variant", { size: "xs" }),
background: background(layer, "variant"),
border: border(layer),
},
state: {
hovered: {
...text(layer, "sans", "variant", "hovered", {
size: "xs",
}),
background: background(layer, "variant", "hovered"),
border: border(layer, "variant", "hovered"),
},
clicked: {
...text(layer, "sans", "variant", "pressed", {
size: "xs",
}),
background: background(layer, "variant", "pressed"),
border: border(layer, "variant", "pressed"),
},
},
}),
state: {
active: {
default: {
...text(layer, "sans", "variant", "active", { size: "xs" }),
background: background(layer, "variant", "active"),
border: border(layer, "variant", "active"),
},
},
}, },
...text(layer, "sans", "variant", { size: "xs" }), })
background: background(layer, "variant"),
border: border(layer),
hover: {
...text(layer, "sans", "variant", "hovered", { size: "xs" }),
background: background(layer, "variant", "hovered"),
border: border(layer, "variant", "hovered"),
},
clicked: {
...text(layer, "sans", "variant", "pressed", { size: "xs" }),
background: background(layer, "variant", "pressed"),
border: border(layer, "variant", "pressed"),
},
active: {
...text(layer, "sans", "variant", "active", { size: "xs" }),
background: background(layer, "variant", "active"),
border: border(layer, "variant", "active"),
},
}
const avatarWidth = 18 const avatarWidth = 18
const avatarOuterWidth = avatarWidth + 4 const avatarOuterWidth = avatarWidth + 4
const followerAvatarWidth = 14 const followerAvatarWidth = 14
@ -78,19 +94,24 @@ export default function workspace(colorScheme: ColorScheme) {
}, },
cornerRadius: 4, cornerRadius: 4,
}, },
keyboardHint: { keyboardHint: interactive({
...text(layer, "sans", "variant", { size: "sm" }), base: {
padding: { ...text(layer, "sans", "variant", { size: "sm" }),
top: 3, padding: {
left: 8, top: 3,
right: 8, left: 8,
bottom: 3, right: 8,
bottom: 3,
},
cornerRadius: 8,
}, },
cornerRadius: 8, state: {
hover: { hovered: {
...text(layer, "sans", "active", { size: "sm" }), ...text(layer, "sans", "active", { size: "sm" }),
},
}, },
}, }),
keyboardHintWidth: 320, keyboardHintWidth: 320,
}, },
joiningProjectAvatar: { joiningProjectAvatar: {
@ -201,12 +222,15 @@ export default function workspace(colorScheme: ColorScheme) {
// Sign in buttom // Sign in buttom
// FlatButton, Variant // FlatButton, Variant
signInPrompt: { signInPrompt: merge(titlebarButton, {
margin: { inactive: {
left: itemSpacing, default: {
margin: {
left: itemSpacing,
},
},
}, },
...titlebarButton, }),
},
// Offline Indicator // Offline Indicator
offlineIcon: { offlineIcon: {
@ -234,40 +258,68 @@ export default function workspace(colorScheme: ColorScheme) {
}, },
cornerRadius: 6, cornerRadius: 6,
}, },
callControl: { callControl: interactive({
cornerRadius: 6, base: {
color: foreground(layer, "variant"), cornerRadius: 6,
iconWidth: 12, color: foreground(layer, "variant"),
buttonWidth: 20, iconWidth: 12,
hover: { buttonWidth: 20,
background: background(layer, "variant", "hovered"), },
color: foreground(layer, "variant", "hovered"), state: {
hovered: {
background: background(layer, "variant", "hovered"),
color: foreground(layer, "variant", "hovered"),
},
},
}),
toggleContactsButton: toggleable({
base: interactive({
base: {
margin: { left: itemSpacing },
cornerRadius: 6,
color: foreground(layer, "variant"),
iconWidth: 14,
buttonWidth: 20,
},
state: {
clicked: {
background: background(layer, "variant", "pressed"),
},
hovered: {
background: background(layer, "variant", "hovered"),
},
},
}),
state: {
active: {
default: {
background: background(layer, "on", "default"),
},
hovered: {
background: background(layer, "on", "hovered"),
},
clicked: {
background: background(layer, "on", "pressed"),
},
},
},
}),
userMenuButton: merge(titlebarButton, {
inactive: {
default: {
buttonWidth: 20,
iconWidth: 12,
},
}, },
},
toggleContactsButton: {
margin: { left: itemSpacing },
cornerRadius: 6,
color: foreground(layer, "variant"),
iconWidth: 14,
buttonWidth: 20,
active: { active: {
background: background(layer, "variant", "active"), // posiewic: these properties are not currently set on main
color: foreground(layer, "variant", "active"), default: {
iconWidth: 12,
button_width: 20,
},
}, },
clicked: { }),
background: background(layer, "variant", "pressed"),
color: foreground(layer, "variant", "pressed"),
},
hover: {
background: background(layer, "variant", "hovered"),
color: foreground(layer, "variant", "hovered"),
},
},
userMenuButton: {
buttonWidth: 20,
iconWidth: 12,
...titlebarButton,
},
toggleContactsBadge: { toggleContactsBadge: {
cornerRadius: 3, cornerRadius: 3,
padding: 2, padding: 2,
@ -285,12 +337,45 @@ export default function workspace(colorScheme: ColorScheme) {
background: background(colorScheme.highest), background: background(colorScheme.highest),
border: border(colorScheme.highest, { bottom: true }), border: border(colorScheme.highest, { bottom: true }),
itemSpacing: 8, itemSpacing: 8,
navButton: { navButton: interactive({
color: foreground(colorScheme.highest, "on"), base: {
iconWidth: 12, color: foreground(colorScheme.highest, "on"),
buttonWidth: 24, iconWidth: 12,
buttonWidth: 24,
cornerRadius: 6,
},
state: {
hovered: {
color: foreground(colorScheme.highest, "on", "hovered"),
background: background(
colorScheme.highest,
"on",
"hovered"
),
},
disabled: {
color: foreground(
colorScheme.highest,
"on",
"disabled"
),
},
},
}),
padding: { left: 8, right: 8, top: 4, bottom: 4 },
},
breadcrumbHeight: 24,
breadcrumbs: interactive({
base: {
...text(colorScheme.highest, "sans", "variant"),
cornerRadius: 6, cornerRadius: 6,
hover: { padding: {
left: 6,
right: 6,
},
},
state: {
hovered: {
color: foreground(colorScheme.highest, "on", "hovered"), color: foreground(colorScheme.highest, "on", "hovered"),
background: background( background: background(
colorScheme.highest, colorScheme.highest,
@ -298,25 +383,8 @@ export default function workspace(colorScheme: ColorScheme) {
"hovered" "hovered"
), ),
}, },
disabled: {
color: foreground(colorScheme.highest, "on", "disabled"),
},
}, },
padding: { left: 8, right: 8, top: 4, bottom: 4 }, }),
},
breadcrumbHeight: 24,
breadcrumbs: {
...text(colorScheme.highest, "sans", "variant"),
cornerRadius: 6,
padding: {
left: 6,
right: 6,
},
hover: {
color: foreground(colorScheme.highest, "on", "hovered"),
background: background(colorScheme.highest, "on", "hovered"),
},
},
disconnectedOverlay: { disconnectedOverlay: {
...text(layer, "sans"), ...text(layer, "sans"),
background: withOpacity(background(layer), 0.8), background: withOpacity(background(layer), 0.8),

Some files were not shown because too many files have changed in this diff Show more