diff --git a/assets/brand/default-bg.jpg b/assets/brand/default-bg.jpg new file mode 100644 index 00000000..acd7b90a Binary files /dev/null and b/assets/brand/default-bg.jpg differ diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 7325fcf8..a1946fd6 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -9,8 +9,8 @@ use common::{TimestampExt, emojis_from_tags}; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, - Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, - ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri, + Focusable, ImageSource, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, + MouseButton, ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri, StatefulInteractiveElement, Styled, StyledImage, Subscription, SystemNotification, SystemNotificationAction, Task, WeakEntity, Window, div, img, list, px, red, relative, retain_all, svg, white, @@ -1846,6 +1846,10 @@ impl Panel for ChatPanel { }), ] } + + fn background(&self, cx: &App) -> Option { + AppSettings::panel_background_source(cx) + } } impl EventEmitter for ChatPanel {} diff --git a/crates/nip29_ui/src/lib.rs b/crates/nip29_ui/src/lib.rs index 71c550e4..bf289cbd 100644 --- a/crates/nip29_ui/src/lib.rs +++ b/crates/nip29_ui/src/lib.rs @@ -5,7 +5,7 @@ use chat_ui::text::rendered_text; use gpui::prelude::FluentBuilder; use gpui::{ Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - FollowMode, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, + FollowMode, ImageSource, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, list, px, }; @@ -1172,6 +1172,10 @@ impl Panel for GroupPanel { Vec::new() } + fn background(&self, cx: &App) -> Option { + AppSettings::panel_background_source(cx) + } + fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) { let Some(group) = self.group.upgrade() else { return; diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 1ca73d86..a571f4f5 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,6 +1,8 @@ +use std::path::PathBuf; + use anyhow::{Error, anyhow}; use common::config_dir; -use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; +use gpui::{App, AppContext, Context, Entity, Global, ImageSource, Subscription, Task}; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; use smallvec::{SmallVec, smallvec}; @@ -12,6 +14,8 @@ pub fn init(cx: &mut App) { const DEFAULT_FILE_SERVER: &str = "https://nostr.download/"; const LEGACY_FILE_SERVER: &str = "blossom.band"; +/// Built-in image shown behind chat and group panels +pub const DEFAULT_BACKGROUND: &str = "brand/default-bg.jpg"; macro_rules! setting_accessors { ($(pub $field:ident: $type:ty),* $(,)?) => { @@ -56,6 +60,8 @@ setting_accessors! { pub screening: bool, pub nip4e: bool, pub file_server: Url, + pub panel_background: bool, + pub panel_background_path: Option, } setting_getters! { @@ -114,12 +120,8 @@ impl RoomConfig { } } -/// Settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Settings { - /// Which appearance to paint: follow the OS, or pin light or dark. - /// `alias` reads the pre-bezel `theme_mode` key so old config files keep - /// their pinned choice. #[serde(default, alias = "theme_mode")] pub appearance_mode: AppearanceMode, @@ -132,6 +134,14 @@ pub struct Settings { /// Enable decoupling encryption key pub nip4e: bool, + /// Show an image behind chat and group panels + #[serde(default)] + pub panel_background: bool, + + /// Custom image for the panel background; the built-in one when unset + #[serde(default)] + pub panel_background_path: Option, + /// Trusted relays; Coop will automatically authenticate with these relays pub trusted_relays: Vec, @@ -154,6 +164,8 @@ impl Default for Settings { hide_avatar: false, screening: true, nip4e: false, + panel_background: true, + panel_background_path: None, trusted_relays: vec![], file_server: Url::parse(DEFAULT_FILE_SERVER).unwrap(), pinned_rooms: vec![], @@ -272,6 +284,17 @@ impl AppSettings { self.inner.read(cx).nip4e } + /// The image to show behind chat and group panels, when enabled + pub fn panel_background_source(cx: &App) -> Option { + if !Self::get_panel_background(cx) { + return None; + } + Some(match Self::get_panel_background_path(cx) { + Some(path) => path.into(), + None => DEFAULT_BACKGROUND.into(), + }) + } + /// Check if the chat room is pinned pub fn pinned_room(&self, id: u64, cx: &App) -> bool { self.inner.read(cx).pinned_rooms.contains(&id) diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index 73a79e99..785c456a 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -6,10 +6,10 @@ use std::sync::Arc; use gpui::prelude::FluentBuilder as _; use gpui::{ AnyElement, AnyView, App, AppContext as _, Bounds, Context, Div, Empty, Entity, - GlobalElementId, InspectorElementId, InteractiveElement as _, IntoElement, LayoutId, - MouseButton, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, ScrollHandle, - SharedString, Stateful, StatefulInteractiveElement as _, Style, StyleRefinement, Styled as _, - WeakEntity, Window, actions, div, px, + GlobalElementId, ImageSource, InspectorElementId, InteractiveElement as _, IntoElement, + LayoutId, MouseButton, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, + ScrollHandle, SharedString, Stateful, StatefulInteractiveElement as _, Style, StyleRefinement, + Styled as _, WeakEntity, Window, actions, div, px, }; pub use gpui_base::dock::{DockArea, DockLayout, DockPlacement}; use gpui_base::dock::{ @@ -24,7 +24,7 @@ use crate::resizable::{resize_handle, resize_handle_appearance}; use crate::tab::Tab; use crate::tab::tab_bar::TabBar; use crate::title_bar::{TRAFFIC_LIGHT_PADDING, title_bar_drag_handlers, window_controls}; -use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex}; +use crate::{IconName, Selectable, Sizable, StyledExt, background, h_flex, v_flex}; mod panel; pub use panel::*; @@ -431,6 +431,11 @@ impl TabGroupSkin { let tabs_count = group.panels().len(); let is_title_bar = self.is_title_bar_group(group, cx); + let wallpaper = group + .active_panel() + .and_then(|panel| panel_background(panel, cx)) + .is_some(); + let displayed = group.active_panel().map(|panel| panel.panel_id(cx)); let displayed_ix = displayed.and_then(|displayed| { group @@ -467,7 +472,7 @@ impl TabGroupSkin { let bar = TabBar::new("tab-bar") .track_scroll(&self.scroll_handle) .h(TABBAR_HEIGHT) - .bg(cx.theme().bg) + .when(!wallpaper, |this| this.bg(cx.theme().bg)) .when(needs_traffic_light_padding, |this| { this.pl(px(TRAFFIC_LIGHT_PADDING)) }) @@ -569,7 +574,7 @@ impl TabGroupSkin { h_flex() .h(TABBAR_HEIGHT) .w_full() - .bg(cx.theme().bg) + .when(!wallpaper, |this| this.bg(cx.theme().bg)) .child(bar.flex_1()) .child(window_controls()) .into_any_element() @@ -647,23 +652,31 @@ impl TabGroupSkin { } impl TabGroupRenderer for TabGroupSkin { - fn frame(&self, group: &TabGroupContext, _: &mut Window, _cx: &mut App) -> Stateful
{ - div().id("tab-panel").when(!group.is_collapsed(), |this| { - this.on_action({ - let group = TabGroupContext::clone(group); - move |_: &ToggleZoom, window, cx| group.toggle_zoom(window, cx) + fn frame(&self, group: &TabGroupContext, _: &mut Window, cx: &mut App) -> Stateful
{ + let wallpaper = group + .active_panel() + .and_then(|panel| panel_background(panel, cx)); + + div() + .id("tab-panel") + .relative() + .when_some(wallpaper, |this, source| this.child(background(source, cx))) + .when(!group.is_collapsed(), |this| { + this.on_action({ + let group = TabGroupContext::clone(group); + move |_: &ToggleZoom, window, cx| group.toggle_zoom(window, cx) + }) + .on_action({ + let group = TabGroupContext::clone(group); + move |_: &ClosePanel, window, cx| { + let Some(panel) = group.active_panel() else { + return; + }; + let panel = panel.panel_id(cx); + group.close(panel, window, cx); + } + }) }) - .on_action({ - let group = TabGroupContext::clone(group); - move |_: &ClosePanel, window, cx| { - let Some(panel) = group.active_panel() else { - return; - }; - let panel = panel.panel_id(cx); - group.close(panel, window, cx); - } - }) - }) } fn render_tab_bar( @@ -710,6 +723,11 @@ impl TabGroupRenderer for TabGroupSkin { return Empty.into_any_element(); } + let wallpaper = group + .active_panel() + .and_then(|active| panel_background(active, cx)) + .is_some(); + v_flex() .id("tab-content") .group("") @@ -718,7 +736,7 @@ impl TabGroupRenderer for TabGroupSkin { .child( div() .size_full() - .bg(cx.theme().bg) + .when(!wallpaper, |this| this.bg(cx.theme().bg)) .overflow_hidden() .child(panel.cached(StyleRefinement::default().v_flex().size_full())), ) @@ -781,3 +799,7 @@ fn panel_title(panel: &Arc, cx: &App) -> AnyElem None => SharedString::from(panel.panel_name(cx)).into_any_element(), } } + +fn panel_background(panel: &Arc, cx: &App) -> Option { + PanelHandle::of(panel).and_then(|handle| handle.panel().background(cx)) +} diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock/panel.rs index 15cec04e..b9fb0636 100644 --- a/crates/ui/src/dock/panel.rs +++ b/crates/ui/src/dock/panel.rs @@ -2,8 +2,8 @@ use std::any::Any; use std::sync::Arc; use gpui::{ - AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render, - SharedString, Window, + AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, ImageSource, + Render, SharedString, Window, }; use gpui_base::dock::{PanelId, PanelState}; @@ -46,6 +46,11 @@ pub trait Panel: EventEmitter + Render + Focusable { fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec