From 84fb47d9303655a39399af71b898cb7d39c8cbf8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 29 Dec 2023 11:20:15 -0700 Subject: [PATCH] WIP: Work toward a decoupled example of UI vs app code --- Cargo.lock | 31 ++ Cargo.toml | 2 + crates/assets/Cargo.toml | 14 + crates/assets/src/assets.rs | 63 +++ crates/gpui2/src/element.rs | 8 +- .../gpui2_macros/src/derive_into_element.rs | 2 +- crates/settings2/src/settings2.rs | 9 + crates/settings2/src/settings_store.rs | 11 + crates/storybook2/Cargo.toml | 1 + crates/storybook2/src/assets.rs | 30 -- crates/storybook2/src/storybook2.rs | 47 +- crates/theme2/src/bin/import_themes.rs | 267 ----------- crates/theme2/src/registry.rs | 6 +- crates/workspace2_ui/Cargo.toml | 24 + crates/workspace2_ui/examples/titlebar.rs | 62 +++ crates/workspace2_ui/src/titlebar.rs | 439 ++++++++++++++++++ crates/workspace2_ui/src/workspace2_ui.rs | 3 + crates/zed2/Cargo.toml | 1 + crates/zed2/src/main.rs | 43 +- crates/zed2/src/zed2.rs | 1 - 20 files changed, 687 insertions(+), 377 deletions(-) create mode 100644 crates/assets/Cargo.toml create mode 100644 crates/assets/src/assets.rs delete mode 100644 crates/storybook2/src/assets.rs delete mode 100644 crates/theme2/src/bin/import_themes.rs create mode 100644 crates/workspace2_ui/Cargo.toml create mode 100644 crates/workspace2_ui/examples/titlebar.rs create mode 100644 crates/workspace2_ui/src/titlebar.rs create mode 100644 crates/workspace2_ui/src/workspace2_ui.rs diff --git a/Cargo.lock b/Cargo.lock index f28027be89..053c9ba188 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,6 +351,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "assets" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui2", + "parking_lot 0.11.2", + "rust-embed", +] + [[package]] name = "assistant" version = "0.1.0" @@ -9301,6 +9311,7 @@ name = "storybook2" version = "0.1.0" dependencies = [ "anyhow", + "assets", "backtrace-on-stack-overflow", "chrono", "clap 4.4.4", @@ -11941,6 +11952,25 @@ dependencies = [ "uuid 1.4.1", ] +[[package]] +name = "workspace2_ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "assets", + "clap 3.2.25", + "gpui2", + "log", + "picker2", + "rust-embed", + "settings2", + "simplelog", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -12167,6 +12197,7 @@ dependencies = [ "activity_indicator2", "ai2", "anyhow", + "assets", "assistant2", "async-compression", "async-recursion 0.3.2", diff --git a/Cargo.toml b/Cargo.toml index c16209991f..6da9d36f98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/activity_indicator", "crates/activity_indicator2", "crates/ai", + "crates/assets", "crates/assistant", "crates/assistant2", "crates/audio", @@ -127,6 +128,7 @@ members = [ "crates/vcs_menu", "crates/vcs_menu2", "crates/workspace2", + "crates/workspace2_ui", "crates/welcome", "crates/welcome2", "crates/xtask", diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml new file mode 100644 index 0000000000..a5da137cfb --- /dev/null +++ b/crates/assets/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "assets" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/assets.rs" + +[dependencies] +anyhow.workspace = true +gpui = { path = "../gpui2", package = "gpui2" } +parking_lot.workspace = true +rust-embed.workspace = true diff --git a/crates/assets/src/assets.rs b/crates/assets/src/assets.rs new file mode 100644 index 0000000000..fdd70548b3 --- /dev/null +++ b/crates/assets/src/assets.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +use anyhow::anyhow; +use gpui::{AppContext, AssetSource, Result, SharedString}; +use parking_lot::Mutex; +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "../../assets"] +#[include = "fonts/**/*"] +#[include = "icons/**/*"] +#[include = "themes/**/*"] +#[exclude = "themes/src/*"] +#[include = "sounds/**/*"] +#[include = "*.md"] +#[exclude = "*.DS_Store"] +pub struct Assets; + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result> { + Self::get(path) + .map(|f| f.data) + .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) + } + + fn list(&self, path: &str) -> Result> { + Ok(Self::iter() + .filter_map(|p| { + if p.starts_with(path) { + Some(p.into()) + } else { + None + } + }) + .collect()) + } +} + +impl Assets { + pub fn load_embedded_fonts(&self, app: &AppContext) { + let font_paths = self.list("fonts").unwrap(); + let embedded_fonts = Mutex::new(Vec::new()); + + app.background_executor() + .block(app.background_executor().scoped(|scope| { + for font_path in &font_paths { + if !font_path.ends_with(".ttf") { + continue; + } + + scope.spawn(async { + let font_path = &*font_path; + let font_bytes = Assets.load(font_path).unwrap().to_vec(); + embedded_fonts.lock().push(Arc::from(font_bytes)); + }); + } + })); + + app.text_system() + .add_fonts(&embedded_fonts.into_inner()) + .unwrap(); + } +} diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 4201123a10..0e65eb4687 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -109,7 +109,7 @@ pub struct Component { component: Option, } -pub struct CompositeElementState { +pub struct ComponentState { rendered_element: Option<::Element>, rendered_element_state: Option<<::Element as Element>::State>, } @@ -123,7 +123,7 @@ impl Component { } impl Element for Component { - type State = CompositeElementState; + type State = ComponentState; fn layout( &mut self, @@ -134,7 +134,7 @@ impl Element for Component { if let Some(element_id) = element.element_id() { let layout_id = cx.with_element_state(element_id, |state, cx| element.layout(state, cx)); - let state = CompositeElementState { + let state = ComponentState { rendered_element: Some(element), rendered_element_state: None, }; @@ -142,7 +142,7 @@ impl Element for Component { } else { let (layout_id, state) = element.layout(state.and_then(|s| s.rendered_element_state), cx); - let state = CompositeElementState { + let state = ComponentState { rendered_element: Some(element), rendered_element_state: Some(state), }; diff --git a/crates/gpui2_macros/src/derive_into_element.rs b/crates/gpui2_macros/src/derive_into_element.rs index 12c6975e07..c3015d8c09 100644 --- a/crates/gpui2_macros/src/derive_into_element.rs +++ b/crates/gpui2_macros/src/derive_into_element.rs @@ -13,7 +13,7 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream { { type Element = gpui::Component; - fn element_id(&self) -> Option { + fn element_id(&self) -> Option { None } diff --git a/crates/settings2/src/settings2.rs b/crates/settings2/src/settings2.rs index a8fa37c77e..08ef6e86e1 100644 --- a/crates/settings2/src/settings2.rs +++ b/crates/settings2/src/settings2.rs @@ -2,6 +2,7 @@ mod keymap_file; mod settings_file; mod settings_store; +use gpui::AppContext; use rust_embed::RustEmbed; use std::{borrow::Cow, str}; use util::asset_str; @@ -17,6 +18,14 @@ pub use settings_store::{Settings, SettingsJsonSchemaParams, SettingsStore}; #[exclude = "*.DS_Store"] pub struct SettingsAssets; +pub fn init(cx: &mut AppContext) { + let mut store = SettingsStore::default(); + store + .set_default_settings(default_settings().as_ref(), cx) + .unwrap(); + cx.set_global(store); +} + pub fn default_settings() -> Cow<'static, str> { asset_str::("settings/default.json") } diff --git a/crates/settings2/src/settings_store.rs b/crates/settings2/src/settings_store.rs index 5bf10a518d..0b32d52a4a 100644 --- a/crates/settings2/src/settings_store.rs +++ b/crates/settings2/src/settings_store.rs @@ -109,6 +109,17 @@ pub trait Settings: 'static + Send + Sync { { cx.global_mut::().override_global(settings) } + + #[track_caller] + fn override_global_with(cx: &mut AppContext, f: F) + where + F: for<'a> FnOnce(&'a mut Self, &'a mut AppContext), + Self: Sized + Clone, + { + let mut settings = Self::get_global(cx).clone(); + f(&mut settings, cx); + Self::override_global(settings, cx); + } } pub struct SettingsJsonSchemaParams<'a> { diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index db79c9faca..0eef0b5b88 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -10,6 +10,7 @@ path = "src/storybook2.rs" [dependencies] anyhow.workspace = true +assets = { path = "../assets" } # TODO: Remove after diagnosing stack overflow. backtrace-on-stack-overflow = "0.3.0" chrono = "0.4" diff --git a/crates/storybook2/src/assets.rs b/crates/storybook2/src/assets.rs deleted file mode 100644 index 9fc71917b4..0000000000 --- a/crates/storybook2/src/assets.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::borrow::Cow; - -use anyhow::{anyhow, Result}; -use gpui::{AssetSource, SharedString}; -use rust_embed::RustEmbed; - -#[derive(RustEmbed)] -#[folder = "../../assets"] -#[include = "fonts/**/*"] -#[include = "icons/**/*"] -#[include = "themes/**/*"] -#[include = "sounds/**/*"] -#[include = "*.md"] -#[exclude = "*.DS_Store"] -pub struct Assets; - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result> { - Self::get(path) - .map(|f| f.data) - .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) - } - - fn list(&self, path: &str) -> Result> { - Ok(Self::iter() - .filter(|p| p.starts_with(path)) - .map(SharedString::from) - .collect()) - } -} diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 523e93cf52..e515bb8a75 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -1,26 +1,22 @@ -mod assets; mod stories; mod story_selector; - -use std::sync::Arc; - +use crate::story_selector::{ComponentStory, StorySelector}; +use assets::Assets; use clap::Parser; use dialoguer::FuzzySelect; use gpui::{ - div, px, size, AnyView, AppContext, Bounds, Div, Render, ViewContext, VisualContext, - WindowBounds, WindowOptions, + div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, + WindowOptions, }; +pub use indoc::indoc; use log::LevelFilter; -use settings2::{default_settings, Settings, SettingsStore}; +use settings2::Settings; use simplelog::SimpleLogger; +use std::sync::Arc; use strum::IntoEnumIterator; use theme2::{ThemeRegistry, ThemeSettings}; use ui::prelude::*; -use crate::assets::Assets; -use crate::story_selector::{ComponentStory, StorySelector}; -pub use indoc::indoc; - // gpui::actions! { // storybook, // [ToggleInspector] @@ -50,7 +46,7 @@ fn main() { let stories = ComponentStory::iter().collect::>(); let selection = FuzzySelect::new() - .with_prompt("Choose a story to run:") + .with_prompt("Choose a story to rungit :") .items(&stories) .interact() .unwrap(); @@ -59,16 +55,10 @@ fn main() { }); let theme_name = args.theme.unwrap_or("One Dark".to_string()); - let asset_source = Arc::new(Assets); - gpui::App::production(asset_source).run(move |cx| { - load_embedded_fonts(cx).unwrap(); - - let mut store = SettingsStore::default(); - store - .set_default_settings(default_settings().as_ref(), cx) - .unwrap(); - cx.set_global(store); - + let assets = Arc::new(Assets); + gpui::App::production(assets.clone()).run(move |cx| { + assets.load_embedded_fonts(cx); + settings2::init(cx); theme2::init(theme2::LoadThemes::All, cx); let selector = story_selector; @@ -124,16 +114,3 @@ impl Render for StoryWrapper { .child(self.story.clone()) } } - -fn load_embedded_fonts(cx: &AppContext) -> gpui::Result<()> { - let font_paths = cx.asset_source().list("fonts")?; - let mut embedded_fonts = Vec::new(); - for font_path in font_paths { - if font_path.ends_with(".ttf") { - let font_bytes = cx.asset_source().load(&font_path)?.to_vec(); - embedded_fonts.push(Arc::from(font_bytes)); - } - } - - cx.text_system().add_fonts(&embedded_fonts) -} diff --git a/crates/theme2/src/bin/import_themes.rs b/crates/theme2/src/bin/import_themes.rs deleted file mode 100644 index 8e2a08365d..0000000000 --- a/crates/theme2/src/bin/import_themes.rs +++ /dev/null @@ -1,267 +0,0 @@ -use gpui::Rgba; -use indoc::indoc; -use serde::Deserialize; -use theme::{FabricSurface, FabricSurfaceState, FabricTheme}; - -fn main() { - use std::fs::{self, DirEntry}; - use std::path::Path; - - let legacy_themes_path = Path::new(env!("PWD")).join("crates/theme2/legacy_themes"); - dbg!(&legacy_themes_path); - if legacy_themes_path.exists() { - let legacy_theme_files = - fs::read_dir(legacy_themes_path).expect("Failed to read legacy themes directory"); - let mut mods = Vec::new(); - - for entry in legacy_theme_files { - let entry: DirEntry = entry.expect("Failed to read directory entry"); - let path = entry.path(); - if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("json") { - let theme_json = - fs::read_to_string(&path).expect(&format!("Failed to read {:?}", path)); - let legacy_theme: LegacyTheme = serde_json::from_str(&theme_json).expect(&format!( - "Failed to parse JSON to LegacyTheme from {:?}", - path - )); - - let name = path.file_stem().unwrap().to_str().unwrap().to_string(); - let mod_name = format!( - "{}", - name.replace(" ", "_").to_lowercase().replace("é", "e") // Hack for Rosé Pine - ); - mods.push(mod_name.clone()); - - let theme = FabricTheme { - name, - - cotton: FabricSurface { - default: FabricSurfaceState { - background: legacy_theme.middle.base.default.background, - border: legacy_theme.middle.base.default.border, - foreground: legacy_theme.middle.base.default.foreground, - secondary_foreground: Some( - legacy_theme.middle.variant.default.foreground, - ), - }, - hovered: FabricSurfaceState { - background: legacy_theme.middle.base.hovered.background, - border: legacy_theme.middle.base.hovered.border, - foreground: legacy_theme.middle.base.hovered.foreground, - secondary_foreground: Some( - legacy_theme.middle.variant.hovered.foreground, - ), - }, - pressed: FabricSurfaceState { - background: legacy_theme.middle.base.pressed.background, - border: legacy_theme.middle.base.pressed.border, - foreground: legacy_theme.middle.base.pressed.foreground, - secondary_foreground: Some( - legacy_theme.middle.variant.pressed.foreground, - ), - }, - active: FabricSurfaceState { - background: legacy_theme.middle.base.active.background, - border: legacy_theme.middle.base.active.border, - foreground: legacy_theme.middle.base.active.foreground, - secondary_foreground: Some( - legacy_theme.middle.variant.active.foreground, - ), - }, - disabled: FabricSurfaceState { - background: legacy_theme.middle.base.disabled.background, - border: legacy_theme.middle.base.disabled.border, - foreground: legacy_theme.middle.base.disabled.foreground, - secondary_foreground: Some( - legacy_theme.middle.variant.disabled.foreground, - ), - }, - inverted: FabricSurfaceState { - background: legacy_theme.middle.base.inverted.background, - border: legacy_theme.middle.base.inverted.border, - foreground: legacy_theme.middle.base.inverted.foreground, - secondary_foreground: Some( - legacy_theme.middle.variant.inverted.foreground, - ), - }, - }, - linen: FabricSurface::from(legacy_theme.lowest.on.clone()), - denim: FabricSurface { - default: FabricSurfaceState { - background: legacy_theme.lowest.base.default.background, - border: legacy_theme.lowest.base.default.border, - foreground: legacy_theme.lowest.base.default.foreground, - secondary_foreground: Some( - legacy_theme.lowest.variant.default.foreground, - ), - }, - hovered: FabricSurfaceState { - background: legacy_theme.lowest.base.hovered.background, - border: legacy_theme.lowest.base.hovered.border, - foreground: legacy_theme.lowest.base.hovered.foreground, - secondary_foreground: Some( - legacy_theme.lowest.variant.hovered.foreground, - ), - }, - pressed: FabricSurfaceState { - background: legacy_theme.lowest.base.pressed.background, - border: legacy_theme.lowest.base.pressed.border, - foreground: legacy_theme.lowest.base.pressed.foreground, - secondary_foreground: Some( - legacy_theme.lowest.variant.pressed.foreground, - ), - }, - active: FabricSurfaceState { - background: legacy_theme.lowest.base.active.background, - border: legacy_theme.lowest.base.active.border, - foreground: legacy_theme.lowest.base.active.foreground, - secondary_foreground: Some( - legacy_theme.lowest.variant.active.foreground, - ), - }, - disabled: FabricSurfaceState { - background: legacy_theme.lowest.base.disabled.background, - border: legacy_theme.lowest.base.disabled.border, - foreground: legacy_theme.lowest.base.disabled.foreground, - secondary_foreground: Some( - legacy_theme.lowest.variant.disabled.foreground, - ), - }, - inverted: FabricSurfaceState { - background: legacy_theme.lowest.base.inverted.background, - border: legacy_theme.lowest.base.inverted.border, - foreground: legacy_theme.lowest.base.inverted.foreground, - secondary_foreground: Some( - legacy_theme.lowest.variant.inverted.foreground, - ), - }, - }, // Assuming silk maps to 'on' at middle elevation - silk: FabricSurface::from(legacy_theme.middle.on.clone()), - satin: FabricSurface::from(legacy_theme.lowest.accent.clone()), - positive: FabricSurface::from(legacy_theme.lowest.positive.clone()), - warning: FabricSurface::from(legacy_theme.lowest.warning.clone()), - negative: FabricSurface::from(legacy_theme.lowest.negative.clone()), - }; - - let indented_theme = format!("{:#?}", theme) - .lines() - .map(|line| format!(" {}", line)) - .collect::>() - .join("\n"); - - let module_source = format!( - indoc! {r#" - use crate::{{FabricSurface, FabricSurfaceState, FabricTheme}}; - use gpui::rgba; - - pub fn {}() -> FabricTheme {{ - {} - }} - "#}, - mod_name, indented_theme, - ); - - let module_path = Path::new(env!("PWD")) - .join(format!("crates/theme2/src/fabric_themes/{}.rs", mod_name)); - fs::write(&module_path, module_source) - .expect(&format!("Failed to write to {:?}", module_path)); - println!("Wrote FabricTheme to file {:?}", module_path); - } - } - - let mod_rs_path = Path::new(env!("PWD")).join("crates/theme2/src/fabric_themes/mod.rs"); - let mut mod_file_content = String::new(); - - for mod_name in mods.iter() { - mod_file_content.push_str(&format!("pub mod {};\n", mod_name)); - } - mod_file_content.push_str("\n"); - for mod_name in mods.iter() { - mod_file_content.push_str(&format!("pub use {}::{};\n", mod_name, mod_name)); - } - - fs::write(&mod_rs_path, mod_file_content) - .expect(&format!("Failed to write to {:?}", mod_rs_path)); - println!("Wrote module declarations to file {:?}", mod_rs_path); - } else { - eprintln!("Legacy themes directory does not exist"); - } -} - -impl From for FabricSurface { - fn from(legacy: LegacySurface) -> Self { - FabricSurface { - default: FabricSurfaceState { - background: legacy.default.background, - border: legacy.default.border, - foreground: legacy.default.foreground, - secondary_foreground: None, // Assuming no secondary_foreground in LegacySurface - }, - hovered: FabricSurfaceState { - background: legacy.hovered.background, - border: legacy.hovered.border, - foreground: legacy.hovered.foreground, - secondary_foreground: None, - }, - pressed: FabricSurfaceState { - background: legacy.pressed.background, - border: legacy.pressed.border, - foreground: legacy.pressed.foreground, - secondary_foreground: None, - }, - active: FabricSurfaceState { - background: legacy.active.background, - border: legacy.active.border, - foreground: legacy.active.foreground, - secondary_foreground: None, - }, - disabled: FabricSurfaceState { - background: legacy.disabled.background, - border: legacy.disabled.border, - foreground: legacy.disabled.foreground, - secondary_foreground: None, - }, - inverted: FabricSurfaceState { - background: legacy.inverted.background, - border: legacy.inverted.border, - foreground: legacy.inverted.foreground, - secondary_foreground: None, - }, - } - } -} - -#[derive(Default, Debug, Clone, Deserialize)] -pub struct LegacySurfaceState { - background: Rgba, - border: Rgba, - foreground: Rgba, -} - -#[derive(Default, Debug, Clone, Deserialize)] -pub struct LegacySurface { - default: LegacySurfaceState, - hovered: LegacySurfaceState, - pressed: LegacySurfaceState, - active: LegacySurfaceState, - disabled: LegacySurfaceState, - inverted: LegacySurfaceState, -} - -#[derive(Default, Debug, Clone, Deserialize)] -pub struct LegacyElevation { - base: LegacySurface, - variant: LegacySurface, - on: LegacySurface, - accent: LegacySurface, - positive: LegacySurface, - warning: LegacySurface, - negative: LegacySurface, -} - -#[derive(Default, Debug, Clone, Deserialize)] -pub struct LegacyTheme { - lowest: LegacyElevation, - middle: LegacyElevation, - highest: LegacyElevation, -} diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 015814e3b2..73ed3163bc 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use anyhow::{anyhow, Result}; -use gpui::{HighlightStyle, SharedString}; +use gpui::{AppContext, HighlightStyle, SharedString}; use refineable::Refineable; use crate::{ @@ -21,6 +21,10 @@ pub struct ThemeRegistry { } impl ThemeRegistry { + pub fn global(cx: &AppContext) -> &Self { + cx.global() + } + fn insert_theme_families(&mut self, families: impl IntoIterator) { for family in families.into_iter() { self.insert_themes(family.themes); diff --git a/crates/workspace2_ui/Cargo.toml b/crates/workspace2_ui/Cargo.toml new file mode 100644 index 0000000000..18477be5b7 --- /dev/null +++ b/crates/workspace2_ui/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "workspace2_ui" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/workspace2_ui.rs" + +[dependencies] +ui = { package = "ui2", path = "../ui2", features = ["stories"] } +gpui = { package = "gpui2", path = "../gpui2" } +theme = { path = "../theme2", package = "theme2" } + +[dev-dependencies] +anyhow = { workspace = true } +assets = { path = "../assets" } +clap = { version = "3.1", features = ["derive"] } +log = { workspace = true } +picker = { package = "picker2", path = "../picker2" } +rust-embed = { workspace = true } +settings = { path = "../settings2", package = "settings2" } +simplelog = "0.9" +util = { path = "../util" } +workspace = { path = "../workspace2", package = "workspace2" } diff --git a/crates/workspace2_ui/examples/titlebar.rs b/crates/workspace2_ui/examples/titlebar.rs new file mode 100644 index 0000000000..59e46ab894 --- /dev/null +++ b/crates/workspace2_ui/examples/titlebar.rs @@ -0,0 +1,62 @@ +use assets::Assets; +use clap::Parser; +use gpui::{px, App, Bounds, Render, Size, VisualContext, WindowBounds, WindowOptions}; +use log::LevelFilter; +use settings::Settings; +use simplelog::SimpleLogger; +use std::sync::Arc; +use theme::{LoadThemes, ThemeRegistry, ThemeSettings}; + +#[derive(Parser)] +struct Args { + theme: Option, +} + +fn main() { + SimpleLogger::init(LevelFilter::Info, Default::default()).unwrap(); + + let args = Args::parse(); + let theme_name = args.theme.unwrap_or("One Light".to_string()); + + let assets = Arc::new(Assets); + App::production(assets.clone()).run(move |cx| { + assets.load_embedded_fonts(cx); + settings::init(cx); + theme::init(LoadThemes::All, cx); + + ThemeSettings::override_global_with(cx, |settings, cx| { + settings.active_theme = ThemeRegistry::global(cx).get(&theme_name).unwrap() + }); + + cx.open_window( + WindowOptions { + bounds: WindowBounds::Fixed(Bounds { + origin: Default::default(), + size: Size { + width: px(1500.), + height: px(780.), + } + .into(), + }), + ..Default::default() + }, + move |cx| { + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + cx.set_rem_size(ui_font_size); + cx.build_view(|cx| TitlebarExample) + }, + ); + + cx.activate(true); + }) +} + +struct TitlebarExample; + +impl Render for TitlebarExample { + type Element = (); + + fn render(&mut self, cx: &mut ui::prelude::ViewContext) -> Self::Element { + todo!() + } +} diff --git a/crates/workspace2_ui/src/titlebar.rs b/crates/workspace2_ui/src/titlebar.rs new file mode 100644 index 0000000000..45c638598e --- /dev/null +++ b/crates/workspace2_ui/src/titlebar.rs @@ -0,0 +1,439 @@ +use gpui::{div, prelude::*, px, rems, Div, SharedString, Stateful, WindowContext}; +use theme::ActiveTheme; +use ui::Shape; + +#[derive(IntoElement)] +pub struct Titlebar { + full_screen: bool, +} + +// pub struct TitlebarCall { +// current_user: TitlebarUser, +// } + +// pub struct TitlebarUser {} + +impl RenderOnce for Titlebar { + type Rendered = Stateful
; + + fn render(self, cx: &mut ui::prelude::WindowContext) -> Self::Rendered { + div() + .flex() + .flex_col() + .id("titlebar") + .justify_between() + .w_full() + .h(rems(1.75)) + // Set a non-scaling min-height here to ensure the titlebar is + // always at least the height of the traffic lights. + .min_h(px(32.)) + .pl_2() + .when(self.full_screen, |this| { + // Use pixels here instead of a rem-based size because the macOS traffic + // lights are a static size, and don't scale with the rest of the UI. + this.pl(px(80.)) + }) + .bg(cx.theme().colors().title_bar_background) + .on_click(|event, cx| { + if event.up.click_count == 2 { + cx.zoom_window(); + } + }) + // left side + .child( + div() + .flex() + .flex_row() + .gap_1() + .children(self.render_project_host(cx)) + .child(self.render_project_name(cx)) + .children(self.render_project_branch(cx)) + .when_some( + current_user.clone().zip(client.peer_id()).zip(room.clone()), + |this, ((current_user, peer_id), room)| { + let player_colors = cx.theme().players(); + let room = room.read(cx); + let mut remote_participants = + room.remote_participants().values().collect::>(); + remote_participants.sort_by_key(|p| p.participant_index.0); + this.children(self.render_collaborator( + ¤t_user, + peer_id, + true, + room.is_speaking(), + room.is_muted(cx), + &room, + project_id, + ¤t_user, + )) + .children( + remote_participants.iter().filter_map(|collaborator| { + let is_present = project_id.map_or(false, |project_id| { + collaborator.location + == ParticipantLocation::SharedProject { project_id } + }); + let face_pile = self.render_collaborator( + &collaborator.user, + collaborator.peer_id, + is_present, + collaborator.speaking, + collaborator.muted, + &room, + project_id, + ¤t_user, + )?; + Some( + v_stack() + .id(("collaborator", collaborator.user.id)) + .child(face_pile) + .child(render_color_ribbon( + collaborator.participant_index, + player_colors, + )) + .cursor_pointer() + .on_click({ + let peer_id = collaborator.peer_id; + cx.listener(move |this, _, cx| { + this.workspace + .update(cx, |workspace, cx| { + workspace.follow(peer_id, cx); + }) + .ok(); + }) + }) + .tooltip({ + let login = collaborator.user.github_login.clone(); + move |cx| { + Tooltip::text(format!("Follow {login}"), cx) + } + }), + ) + }), + ) + }, + ), + ) + // right side + .child( + div() + .flex() + .flex_row() + .gap_1() + .pr_1() + .when_some(room, |this, room| { + let room = room.read(cx); + let project = self.project.read(cx); + let is_local = project.is_local(); + let is_shared = is_local && project.is_shared(); + let is_muted = room.is_muted(cx); + let is_deafened = room.is_deafened().unwrap_or(false); + let is_screen_sharing = room.is_screen_sharing(); + + this.when(is_local, |this| { + this.child( + Button::new( + "toggle_sharing", + if is_shared { "Unshare" } else { "Share" }, + ) + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .on_click(cx.listener( + move |this, _, cx| { + if is_shared { + this.unshare_project(&Default::default(), cx); + } else { + this.share_project(&Default::default(), cx); + } + }, + )), + ) + }) + .child( + IconButton::new("leave-call", ui::Icon::Exit) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .on_click(move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); + }), + ) + .child( + IconButton::new( + "mute-microphone", + if is_muted { + ui::Icon::MicMute + } else { + ui::Icon::Mic + }, + ) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .selected(is_muted) + .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), + ) + .child( + IconButton::new( + "mute-sound", + if is_deafened { + ui::Icon::AudioOff + } else { + ui::Icon::AudioOn + }, + ) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .selected(is_deafened) + .tooltip(move |cx| { + Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx) + }) + .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), + ) + .child( + IconButton::new("screen-share", ui::Icon::Screen) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .selected(is_screen_sharing) + .on_click(move |_, cx| { + crate::toggle_screen_sharing(&Default::default(), cx) + }), + ) + }) + .map(|el| { + let status = self.client.status(); + let status = &*status.borrow(); + if matches!(status, client::Status::Connected { .. }) { + el.child(self.render_user_menu_button(cx)) + } else { + el.children(self.render_connection_status(status, cx)) + .child(self.render_sign_in_button(cx)) + .child(self.render_user_menu_button(cx)) + } + }), + ) + } +} + +impl Titlebar { + pub fn render_project_host(&self, cx: &mut ViewContext) -> Option { + let host = self.project.read(cx).host()?; + let host = self.user_store.read(cx).get_cached_user(host.user_id)?; + let participant_index = self + .user_store + .read(cx) + .participant_indices() + .get(&host.id)?; + Some( + div().border().border_color(gpui::red()).child( + Button::new("project_owner_trigger", host.github_login.clone()) + .color(Color::Player(participant_index.0)) + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .tooltip(move |cx| Tooltip::text("Toggle following", cx)), + ), + ) + } + + pub fn render_project_name(&self, cx: &mut ViewContext) -> impl Element { + let name = { + let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| { + let worktree = worktree.read(cx); + worktree.root_name() + }); + + names.next().unwrap_or("") + }; + + let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); + let workspace = self.workspace.clone(); + popover_menu("project_name_trigger") + .trigger( + Button::new("project_name_trigger", name) + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), + ) + .menu(move |cx| Some(Self::render_project_popover(workspace.clone(), cx))) + } + + pub fn render_project_branch(&self, cx: &mut ViewContext) -> Option { + let entry = { + let mut names_and_branches = + self.project.read(cx).visible_worktrees(cx).map(|worktree| { + let worktree = worktree.read(cx); + worktree.root_git_entry() + }); + + names_and_branches.next().flatten() + }; + let workspace = self.workspace.upgrade()?; + let branch_name = entry + .as_ref() + .and_then(RepositoryEntry::branch) + .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?; + Some( + popover_menu("project_branch_trigger") + .trigger( + Button::new("project_branch_trigger", branch_name) + .color(Color::Muted) + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .tooltip(move |cx| { + Tooltip::with_meta( + "Recent Branches", + Some(&ToggleVcsMenu), + "Local branches only", + cx, + ) + }), + ) + .menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)), + ) + } + + fn render_collaborator( + &self, + user: &Arc, + peer_id: PeerId, + is_present: bool, + is_speaking: bool, + is_muted: bool, + room: &Room, + project_id: Option, + current_user: &Arc, + ) -> Option { + let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id)); + + let pile = FacePile::default() + .child( + Avatar::new(user.avatar_uri.clone()) + .grayscale(!is_present) + .border_color(if is_speaking { + gpui::blue() + } else if is_muted { + gpui::red() + } else { + Hsla::default() + }), + ) + .children(followers.iter().filter_map(|follower_peer_id| { + let follower = room + .remote_participants() + .values() + .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user)) + .or_else(|| { + (self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user) + })? + .clone(); + + Some(Avatar::new(follower.avatar_uri.clone())) + })); + + Some(pile) + } +} + +pub struct TitlebarParticipant { + avatar_uri: SharedString, + follower_avatar_uris: Vec, +} + +pub struct Facepile {} + +#[derive(Default, IntoElement)] +pub struct FacePile { + pub faces: Vec, +} + +impl RenderOnce for FacePile { + type Rendered = Div; + + fn render(self, _: &mut WindowContext) -> Self::Rendered { + let face_count = self.faces.len(); + div() + .p_1() + .flex() + .items_center() + .children(self.faces.into_iter().enumerate().map(|(ix, avatar)| { + let last_child = ix == face_count - 1; + div() + .z_index((face_count - ix) as u8) + .when(!last_child, |div| div.neg_mr_1()) + .child(avatar) + })) + } +} + +#[derive(IntoElement)] +pub struct Avatar { + pub image_uri: SharedString, + pub audio_status: AudioStatus, + pub shape: AvatarShape, +} + +pub enum AvatarShape { + Square, + Circle, +} + +impl RenderOnce for Avatar { + type Rendered = Div; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + div() + .map(|this| match self.shape { + AvatarShape::Square => this.rounded_md(), + AvatarShape::Circle => this.rounded_full(), + }) + .map(|this| match self.audio_status { + AudioStatus::None => this, + AudioStatus::Muted => todo!(), + AudioStatus::Speaking => todo!(), + }) + .size(cx.rem_size() + px(2.)); + + let size = cx.rem_size(); + + div() + .when_some(self.border_color, |this, color| { + this.border().border_color(color) + }) + .child( + self.image + .size(size) + // todo!(Pull the avatar fallback background from the theme.) + .bg(gpui::red()), + ) + .children(self.is_available.map(|is_free| { + // HACK: non-integer sizes result in oval indicators. + let indicator_size = (size * 0.4).round(); + + div() + .absolute() + .z_index(1) + .bg(if is_free { + cx.theme().status().created + } else { + cx.theme().status().deleted + }) + .size(indicator_size) + .rounded(indicator_size) + .bottom_0() + .right_0() + })) + } +} + +pub enum AudioStatus { + None, + Muted, + Speaking, +} + +impl RenderOnce for Avatar { + type Rendered = Div; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + todo!() + } +} diff --git a/crates/workspace2_ui/src/workspace2_ui.rs b/crates/workspace2_ui/src/workspace2_ui.rs new file mode 100644 index 0000000000..76f3f777d8 --- /dev/null +++ b/crates/workspace2_ui/src/workspace2_ui.rs @@ -0,0 +1,3 @@ +mod titlebar; + +pub use titlebar::*; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 3da22401e0..1918fd2904 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" [dependencies] ai = { package = "ai2", path = "../ai2"} +assets = { path = "../assets" } audio = { package = "audio2", path = "../audio2" } activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"} auto_update = { package = "auto_update2", path = "../auto_update2" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 2fbb9c101d..1d9e0dfd29 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -17,11 +17,8 @@ use language::LanguageRegistry; use log::LevelFilter; use node_runtime::RealNodeRuntime; -use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -use settings::{ - default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore, -}; +use settings::{handle_settings_file_changes, watch_config_file, Settings, SettingsStore}; use simplelog::ConfigBuilder; use smol::process::Command; use std::{ @@ -66,7 +63,8 @@ fn main() { } log::info!("========== starting zed =========="); - let app = App::production(Arc::new(Assets)); + let assets = Arc::new(Assets); + let app = App::production(assets.clone()); let (installation_id, existing_installation_id_found) = app .background_executor() @@ -116,16 +114,9 @@ fn main() { if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") { cx.set_global(AppCommitSha(build_sha.into())) } - cx.set_global(listener.clone()); - - load_embedded_fonts(cx); - - let mut store = SettingsStore::default(); - store - .set_default_settings(default_settings().as_ref(), cx) - .unwrap(); - cx.set_global(store); + assets.load_embedded_fonts(cx); + settings::init(cx); handle_settings_file_changes(user_settings_file_rx, cx); handle_keymap_file_changes(user_keymap_file_rx, cx); @@ -722,30 +713,6 @@ fn collect_url_args() -> Vec { .collect() } -fn load_embedded_fonts(cx: &AppContext) { - let asset_source = cx.asset_source(); - let font_paths = asset_source.list("fonts").unwrap(); - let embedded_fonts = Mutex::new(Vec::new()); - let executor = cx.background_executor(); - - executor.block(executor.scoped(|scope| { - for font_path in &font_paths { - if !font_path.ends_with(".ttf") { - continue; - } - - scope.spawn(async { - let font_bytes = asset_source.load(font_path).unwrap().to_vec(); - embedded_fonts.lock().push(Arc::from(font_bytes)); - }); - } - })); - - cx.text_system() - .add_fonts(&embedded_fonts.into_inner()) - .unwrap(); -} - #[cfg(debug_assertions)] async fn watch_languages(fs: Arc, languages: Arc) -> Option<()> { use std::time::Duration; diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 4abc81cf37..cd12a17051 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -1,5 +1,4 @@ mod app_menus; -mod assets; pub mod languages; mod only_instance; mod open_listener;