WIP: Work toward a decoupled example of UI vs app code

This commit is contained in:
Nathan Sobo 2023-12-29 11:20:15 -07:00
parent 1485bd3cfc
commit 84fb47d930
20 changed files with 687 additions and 377 deletions

31
Cargo.lock generated
View file

@ -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",

View file

@ -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",

14
crates/assets/Cargo.toml Normal file
View file

@ -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

View file

@ -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<std::borrow::Cow<[u8]>> {
Self::get(path)
.map(|f| f.data)
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
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();
}
}

View file

@ -109,7 +109,7 @@ pub struct Component<C> {
component: Option<C>,
}
pub struct CompositeElementState<C: RenderOnce> {
pub struct ComponentState<C: RenderOnce> {
rendered_element: Option<<C::Rendered as IntoElement>::Element>,
rendered_element_state: Option<<<C::Rendered as IntoElement>::Element as Element>::State>,
}
@ -123,7 +123,7 @@ impl<C> Component<C> {
}
impl<C: RenderOnce> Element for Component<C> {
type State = CompositeElementState<C>;
type State = ComponentState<C>;
fn layout(
&mut self,
@ -134,7 +134,7 @@ impl<C: RenderOnce> Element for Component<C> {
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<C: RenderOnce> Element for Component<C> {
} 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),
};

View file

@ -13,7 +13,7 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream {
{
type Element = gpui::Component<Self>;
fn element_id(&self) -> Option<ElementId> {
fn element_id(&self) -> Option<gpui::ElementId> {
None
}

View file

@ -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::<SettingsAssets>("settings/default.json")
}

View file

@ -109,6 +109,17 @@ pub trait Settings: 'static + Send + Sync {
{
cx.global_mut::<SettingsStore>().override_global(settings)
}
#[track_caller]
fn override_global_with<F>(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> {

View file

@ -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"

View file

@ -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<Cow<[u8]>> {
Self::get(path)
.map(|f| f.data)
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
Ok(Self::iter()
.filter(|p| p.starts_with(path))
.map(SharedString::from)
.collect())
}
}

View file

@ -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::<Vec<_>>();
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)
}

View file

@ -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::<Vec<String>>()
.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<LegacySurface> 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,
}

View file

@ -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<Item = ThemeFamily>) {
for family in families.into_iter() {
self.insert_themes(family.themes);

View file

@ -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" }

View file

@ -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<String>,
}
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>) -> Self::Element {
todo!()
}
}

View file

@ -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<Div>;
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::<Vec<_>>();
remote_participants.sort_by_key(|p| p.participant_index.0);
this.children(self.render_collaborator(
&current_user,
peer_id,
true,
room.is_speaking(),
room.is_muted(cx),
&room,
project_id,
&current_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,
&current_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<Self>) -> Option<impl Element> {
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<Self>) -> 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<Self>) -> Option<impl Element> {
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<User>,
peer_id: PeerId,
is_present: bool,
is_speaking: bool,
is_muted: bool,
room: &Room,
project_id: Option<u64>,
current_user: &Arc<User>,
) -> Option<FacePile> {
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<SharedString>,
}
pub struct Facepile {}
#[derive(Default, IntoElement)]
pub struct FacePile {
pub faces: Vec<Avatar>,
}
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!()
}
}

View file

@ -0,0 +1,3 @@
mod titlebar;
pub use titlebar::*;

View file

@ -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" }

View file

@ -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<String> {
.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<dyn fs::Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
use std::time::Duration;

View file

@ -1,5 +1,4 @@
mod app_menus;
mod assets;
pub mod languages;
mod only_instance;
mod open_listener;