Add channels panel with stubbed out information
co-authored-by: nate <nate@zed.dev>
This commit is contained in:
parent
e6f3e0ab9c
commit
ac35dae66e
17 changed files with 784 additions and 34 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -1254,6 +1254,31 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "channels"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"client",
|
||||||
|
"collections",
|
||||||
|
"context_menu",
|
||||||
|
"db",
|
||||||
|
"editor",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"menu",
|
||||||
|
"project",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"settings",
|
||||||
|
"theme",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.26"
|
version = "0.4.26"
|
||||||
|
@ -9857,6 +9882,7 @@ dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"breadcrumbs",
|
"breadcrumbs",
|
||||||
"call",
|
"call",
|
||||||
|
"channels",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cli",
|
"cli",
|
||||||
"client",
|
"client",
|
||||||
|
|
|
@ -6,6 +6,7 @@ members = [
|
||||||
"crates/auto_update",
|
"crates/auto_update",
|
||||||
"crates/breadcrumbs",
|
"crates/breadcrumbs",
|
||||||
"crates/call",
|
"crates/call",
|
||||||
|
"crates/channels",
|
||||||
"crates/cli",
|
"crates/cli",
|
||||||
"crates/client",
|
"crates/client",
|
||||||
"crates/clock",
|
"crates/clock",
|
||||||
|
|
|
@ -122,6 +122,12 @@
|
||||||
// Amount of indentation for nested items.
|
// Amount of indentation for nested items.
|
||||||
"indent_size": 20
|
"indent_size": 20
|
||||||
},
|
},
|
||||||
|
"channels_panel": {
|
||||||
|
// Where to dock channels panel. Can be 'left' or 'right'.
|
||||||
|
"dock": "left",
|
||||||
|
// Default width of the channels panel.
|
||||||
|
"default_width": 240
|
||||||
|
},
|
||||||
"assistant": {
|
"assistant": {
|
||||||
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
|
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
|
||||||
"dock": "right",
|
"dock": "right",
|
||||||
|
|
38
crates/channels/Cargo.toml
Normal file
38
crates/channels/Cargo.toml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
[package]
|
||||||
|
name = "channels"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/channels.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
context_menu = { path = "../context_menu" }
|
||||||
|
client = { path = "../client" }
|
||||||
|
db = { path = "../db" }
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
|
||||||
|
log.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_derive.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
client = { path = "../client", features = ["test-support"] }
|
||||||
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
serde_json.workspace = true
|
103
crates/channels/src/channels.rs
Normal file
103
crates/channels/src/channels.rs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
mod channels_panel;
|
||||||
|
mod channels_panel_settings;
|
||||||
|
|
||||||
|
pub use channels_panel::*;
|
||||||
|
use gpui::{AppContext, Entity};
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use client::Client;
|
||||||
|
|
||||||
|
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||||
|
let channels = cx.add_model(|cx| Channels::new(client, cx));
|
||||||
|
cx.set_global(channels);
|
||||||
|
channels_panel::init(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Channel {
|
||||||
|
id: u64,
|
||||||
|
name: String,
|
||||||
|
sub_channels: Vec<Channel>,
|
||||||
|
_room: Option<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Channel {
|
||||||
|
fn new(id: u64, name: impl AsRef<str>, members: Vec<Channel>) -> Channel {
|
||||||
|
Channel {
|
||||||
|
name: name.as_ref().to_string(),
|
||||||
|
id,
|
||||||
|
sub_channels: members,
|
||||||
|
_room: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn members(&self) -> &[Channel] {
|
||||||
|
&self.sub_channels
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Channels {
|
||||||
|
channels: Vec<Channel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Channels {
|
||||||
|
fn channels(&self) -> Vec<Channel> {
|
||||||
|
self.channels.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ChannelEvents {}
|
||||||
|
|
||||||
|
impl Entity for Channels {
|
||||||
|
type Event = ChannelEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Channels {
|
||||||
|
fn new(_client: Arc<Client>, _cx: &mut AppContext) -> Self {
|
||||||
|
//TODO: Subscribe to channel updates from the server
|
||||||
|
Channels {
|
||||||
|
channels: vec![Channel::new(
|
||||||
|
0,
|
||||||
|
"Zed Industries",
|
||||||
|
vec![
|
||||||
|
Channel::new(1, "#general", Vec::new()),
|
||||||
|
Channel::new(2, "#admiral", Vec::new()),
|
||||||
|
Channel::new(3, "#livestreaming", vec![]),
|
||||||
|
Channel::new(4, "#crdb", Vec::new()),
|
||||||
|
Channel::new(5, "#crdb-1", Vec::new()),
|
||||||
|
Channel::new(6, "#crdb-2", Vec::new()),
|
||||||
|
Channel::new(7, "#crdb-3", vec![]),
|
||||||
|
Channel::new(8, "#crdb-4", Vec::new()),
|
||||||
|
Channel::new(9, "#crdb-1", Vec::new()),
|
||||||
|
Channel::new(10, "#crdb-1", Vec::new()),
|
||||||
|
Channel::new(11, "#crdb-1", Vec::new()),
|
||||||
|
Channel::new(12, "#crdb-1", vec![]),
|
||||||
|
Channel::new(13, "#crdb-1", Vec::new()),
|
||||||
|
Channel::new(14, "#crdb-1", Vec::new()),
|
||||||
|
Channel::new(15, "#crdb-1", Vec::new()),
|
||||||
|
Channel::new(16, "#crdb-1", Vec::new()),
|
||||||
|
Channel::new(17, "#crdb", vec![]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Channel::new(
|
||||||
|
18,
|
||||||
|
"CRDB Consulting",
|
||||||
|
vec![
|
||||||
|
Channel::new(19, "#crdb 😭", Vec::new()),
|
||||||
|
Channel::new(20, "#crdb 😌", Vec::new()),
|
||||||
|
Channel::new(21, "#crdb 🦀", vec![]),
|
||||||
|
Channel::new(22, "#crdb 😤", Vec::new()),
|
||||||
|
Channel::new(23, "#crdb 😤", Vec::new()),
|
||||||
|
Channel::new(24, "#crdb 😤", Vec::new()),
|
||||||
|
Channel::new(25, "#crdb 😤", vec![]),
|
||||||
|
Channel::new(26, "#crdb 😤", Vec::new()),
|
||||||
|
],
|
||||||
|
)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
369
crates/channels/src/channels_panel.rs
Normal file
369
crates/channels/src/channels_panel.rs
Normal file
|
@ -0,0 +1,369 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
channels_panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings},
|
||||||
|
Channel, Channels,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use collections::HashMap;
|
||||||
|
use context_menu::ContextMenu;
|
||||||
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
elements::{ChildView, Empty, Flex, Label, MouseEventHandler, ParentElement, Stack},
|
||||||
|
serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
|
||||||
|
ViewContext, ViewHandle, WeakViewHandle,
|
||||||
|
};
|
||||||
|
use project::Fs;
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use theme::ChannelTreeStyle;
|
||||||
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
use workspace::{
|
||||||
|
dock::{DockPosition, Panel},
|
||||||
|
Workspace,
|
||||||
|
};
|
||||||
|
|
||||||
|
actions!(channels, [ToggleFocus]);
|
||||||
|
|
||||||
|
const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
settings::register::<ChannelsPanelSettings>(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChannelsPanel {
|
||||||
|
width: Option<f32>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
has_focus: bool,
|
||||||
|
pending_serialization: Task<Option<()>>,
|
||||||
|
channels: ModelHandle<Channels>,
|
||||||
|
context_menu: ViewHandle<ContextMenu>,
|
||||||
|
collapsed_channels: HashMap<u64, bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct SerializedChannelsPanel {
|
||||||
|
width: Option<f32>,
|
||||||
|
collapsed_channels: Option<HashMap<u64, bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
DockPositionChanged,
|
||||||
|
Focus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ChannelsPanel {
|
||||||
|
type Event = Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelsPanel {
|
||||||
|
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||||
|
cx.add_view(|cx| {
|
||||||
|
let view_id = cx.view_id();
|
||||||
|
let this = Self {
|
||||||
|
width: None,
|
||||||
|
has_focus: false,
|
||||||
|
fs: workspace.app_state().fs.clone(),
|
||||||
|
pending_serialization: Task::ready(None),
|
||||||
|
channels: cx.global::<ModelHandle<Channels>>().clone(),
|
||||||
|
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
||||||
|
collapsed_channels: HashMap::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the dock position when the setting changes.
|
||||||
|
let mut old_dock_position = this.position(cx);
|
||||||
|
cx.observe_global::<SettingsStore, _>(move |this: &mut ChannelsPanel, cx| {
|
||||||
|
let new_dock_position = this.position(cx);
|
||||||
|
if new_dock_position != old_dock_position {
|
||||||
|
old_dock_position = new_dock_position;
|
||||||
|
cx.emit(Event::DockPositionChanged);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
this
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(
|
||||||
|
workspace: WeakViewHandle<Workspace>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Task<Result<ViewHandle<Self>>> {
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let serialized_panel = if let Some(panel) = cx
|
||||||
|
.background()
|
||||||
|
.spawn(async move { KEY_VALUE_STORE.read_kvp(CHANNELS_PANEL_KEY) })
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
Some(serde_json::from_str::<SerializedChannelsPanel>(&panel)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
let panel = ChannelsPanel::new(workspace, cx);
|
||||||
|
if let Some(serialized_panel) = serialized_panel {
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.width = serialized_panel.width;
|
||||||
|
panel.collapsed_channels =
|
||||||
|
serialized_panel.collapsed_channels.unwrap_or_default();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
panel
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let width = self.width;
|
||||||
|
let collapsed_channels = self.collapsed_channels.clone();
|
||||||
|
self.pending_serialization = cx.background().spawn(
|
||||||
|
async move {
|
||||||
|
KEY_VALUE_STORE
|
||||||
|
.write_kvp(
|
||||||
|
CHANNELS_PANEL_KEY.into(),
|
||||||
|
serde_json::to_string(&SerializedChannelsPanel {
|
||||||
|
width,
|
||||||
|
collapsed_channels: Some(collapsed_channels),
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_channel(
|
||||||
|
&mut self,
|
||||||
|
depth: usize,
|
||||||
|
channel: &Channel,
|
||||||
|
style: &ChannelTreeStyle,
|
||||||
|
root: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> AnyElement<Self> {
|
||||||
|
let has_chilren = !channel.members().is_empty();
|
||||||
|
|
||||||
|
let sub_channel_details = has_chilren.then(|| {
|
||||||
|
let mut sub_channels = Flex::column();
|
||||||
|
let collapsed = self
|
||||||
|
.collapsed_channels
|
||||||
|
.get(&channel.id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_default();
|
||||||
|
if !collapsed {
|
||||||
|
for sub_channel in channel.members() {
|
||||||
|
sub_channels = sub_channels.with_child(self.render_channel(
|
||||||
|
depth + 1,
|
||||||
|
sub_channel,
|
||||||
|
style,
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(sub_channels, collapsed)
|
||||||
|
});
|
||||||
|
|
||||||
|
let channel_id = channel.id;
|
||||||
|
|
||||||
|
enum ChannelCollapser {}
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
Empty::new()
|
||||||
|
.constrained()
|
||||||
|
.with_width(depth as f32 * style.channel_indent),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
sub_channel_details
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, expanded)| {
|
||||||
|
MouseEventHandler::<ChannelCollapser, _>::new(
|
||||||
|
channel.id as usize,
|
||||||
|
cx,
|
||||||
|
|state, _cx| {
|
||||||
|
let icon =
|
||||||
|
style.channel_icon.style_for(!*expanded, state);
|
||||||
|
theme::ui::icon(icon)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_click(
|
||||||
|
gpui::platform::MouseButton::Left,
|
||||||
|
move |_, v, cx| {
|
||||||
|
let entry = v
|
||||||
|
.collapsed_channels
|
||||||
|
.entry(channel_id)
|
||||||
|
.or_default();
|
||||||
|
*entry = !*entry;
|
||||||
|
v.serialize(cx);
|
||||||
|
cx.notify();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
Empty::new()
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.channel_icon.default_style().width())
|
||||||
|
.into_any()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
channel.name().to_string(),
|
||||||
|
if root {
|
||||||
|
style.root_name.clone()
|
||||||
|
} else {
|
||||||
|
style.channel_name.clone()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_children(sub_channel_details.map(|(elements, _)| elements)),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ChannelsPanel {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ChannelsPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
if !self.has_focus {
|
||||||
|
self.has_focus = true;
|
||||||
|
cx.emit(Event::Focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||||
|
self.has_focus = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
|
||||||
|
let theme = theme::current(cx).clone();
|
||||||
|
|
||||||
|
let mut channels_column = Flex::column();
|
||||||
|
for channel in self.channels.read(cx).channels() {
|
||||||
|
channels_column = channels_column.with_child(self.render_channel(
|
||||||
|
0,
|
||||||
|
&channel,
|
||||||
|
&theme.channels_panel.channel_tree,
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let spacing = theme.channels_panel.spacing;
|
||||||
|
|
||||||
|
enum ChannelsPanelScrollTag {}
|
||||||
|
Stack::new()
|
||||||
|
.with_child(
|
||||||
|
// Full panel column
|
||||||
|
Flex::column()
|
||||||
|
.with_spacing(spacing)
|
||||||
|
.with_child(
|
||||||
|
// Channels section column
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Flex::row().with_child(
|
||||||
|
Label::new(
|
||||||
|
"Active Channels",
|
||||||
|
theme.editor.invalid_information_diagnostic.message.clone(),
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Channels list column
|
||||||
|
.with_child(channels_column),
|
||||||
|
)
|
||||||
|
// TODO: Replace with spacing implementation
|
||||||
|
.with_child(Empty::new().constrained().with_height(spacing))
|
||||||
|
.with_child(
|
||||||
|
Flex::column().with_child(
|
||||||
|
Flex::row().with_child(
|
||||||
|
Label::new(
|
||||||
|
"Contacts",
|
||||||
|
theme.editor.invalid_information_diagnostic.message.clone(),
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.scrollable::<ChannelsPanelScrollTag>(0, None, cx)
|
||||||
|
.expanded(),
|
||||||
|
)
|
||||||
|
.with_child(ChildView::new(&self.context_menu, cx))
|
||||||
|
.into_any_named("channels panel")
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for ChannelsPanel {
|
||||||
|
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||||
|
match settings::get::<ChannelsPanelSettings>(cx).dock {
|
||||||
|
ChannelsPanelDockPosition::Left => DockPosition::Left,
|
||||||
|
ChannelsPanelDockPosition::Right => DockPosition::Right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||||
|
matches!(position, DockPosition::Left | DockPosition::Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||||
|
settings::update_settings_file::<ChannelsPanelSettings>(
|
||||||
|
self.fs.clone(),
|
||||||
|
cx,
|
||||||
|
move |settings| {
|
||||||
|
let dock = match position {
|
||||||
|
DockPosition::Left | DockPosition::Bottom => ChannelsPanelDockPosition::Left,
|
||||||
|
DockPosition::Right => ChannelsPanelDockPosition::Right,
|
||||||
|
};
|
||||||
|
settings.dock = Some(dock);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self, cx: &gpui::WindowContext) -> f32 {
|
||||||
|
self.width
|
||||||
|
.unwrap_or_else(|| settings::get::<ChannelsPanelSettings>(cx).default_width)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
|
||||||
|
self.width = Some(size);
|
||||||
|
self.serialize(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_path(&self) -> &'static str {
|
||||||
|
"icons/bolt_16.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
||||||
|
("Channels Panel".to_string(), Some(Box::new(ToggleFocus)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_change_position_on_event(event: &Self::Event) -> bool {
|
||||||
|
matches!(event, Event::DockPositionChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
|
||||||
|
self.has_focus
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_focus_event(event: &Self::Event) -> bool {
|
||||||
|
matches!(event, Event::Focus)
|
||||||
|
}
|
||||||
|
}
|
37
crates/channels/src/channels_panel_settings.rs
Normal file
37
crates/channels/src/channels_panel_settings.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use anyhow;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use settings::Setting;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ChannelsPanelDockPosition {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ChannelsPanelSettings {
|
||||||
|
pub dock: ChannelsPanelDockPosition,
|
||||||
|
pub default_width: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||||
|
pub struct ChannelsPanelSettingsContent {
|
||||||
|
pub dock: Option<ChannelsPanelDockPosition>,
|
||||||
|
pub default_width: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Setting for ChannelsPanelSettings {
|
||||||
|
const KEY: Option<&'static str> = Some("channels_panel");
|
||||||
|
|
||||||
|
type FileContent = ChannelsPanelSettingsContent;
|
||||||
|
|
||||||
|
fn load(
|
||||||
|
default_value: &Self::FileContent,
|
||||||
|
user_values: &[&Self::FileContent],
|
||||||
|
_: &gpui::AppContext,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ pub struct Flex<V: View> {
|
||||||
children: Vec<AnyElement<V>>,
|
children: Vec<AnyElement<V>>,
|
||||||
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
||||||
child_alignment: f32,
|
child_alignment: f32,
|
||||||
|
spacing: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: View> Flex<V> {
|
impl<V: View> Flex<V> {
|
||||||
|
@ -31,6 +32,7 @@ impl<V: View> Flex<V> {
|
||||||
children: Default::default(),
|
children: Default::default(),
|
||||||
scroll_state: None,
|
scroll_state: None,
|
||||||
child_alignment: -1.,
|
child_alignment: -1.,
|
||||||
|
spacing: 0.,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +44,11 @@ impl<V: View> Flex<V> {
|
||||||
Self::new(Axis::Vertical)
|
Self::new(Axis::Vertical)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_spacing(mut self, spacing: f32) -> Self {
|
||||||
|
self.spacing = spacing;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Render children centered relative to the cross-axis of the parent flex.
|
/// Render children centered relative to the cross-axis of the parent flex.
|
||||||
///
|
///
|
||||||
/// If this is a flex row, children will be centered vertically. If this is a
|
/// If this is a flex row, children will be centered vertically. If this is a
|
||||||
|
|
|
@ -1649,22 +1649,6 @@ impl workspace::dock::Panel for ProjectPanel {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_zoom_in_on_event(_: &Self::Event) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_zoom_out_on_event(_: &Self::Event) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_zoomed(&self, _: &WindowContext) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_zoomed(&mut self, _: bool, _: &mut ViewContext<Self>) {}
|
|
||||||
|
|
||||||
fn set_active(&mut self, _: bool, _: &mut ViewContext<Self>) {}
|
|
||||||
|
|
||||||
fn icon_path(&self) -> &'static str {
|
fn icon_path(&self) -> &'static str {
|
||||||
"icons/folder_tree_16.svg"
|
"icons/folder_tree_16.svg"
|
||||||
}
|
}
|
||||||
|
@ -1677,14 +1661,6 @@ impl workspace::dock::Panel for ProjectPanel {
|
||||||
matches!(event, Event::DockPositionChanged)
|
matches!(event, Event::DockPositionChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_activate_on_event(_: &Self::Event) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_close_on_event(_: &Self::Event) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_focus(&self, _: &WindowContext) -> bool {
|
fn has_focus(&self, _: &WindowContext) -> bool {
|
||||||
self.has_focus
|
self.has_focus
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ pub struct Theme {
|
||||||
pub copilot: Copilot,
|
pub copilot: Copilot,
|
||||||
pub contact_finder: ContactFinder,
|
pub contact_finder: ContactFinder,
|
||||||
pub project_panel: ProjectPanel,
|
pub project_panel: ProjectPanel,
|
||||||
|
pub channels_panel: ChanelsPanelStyle,
|
||||||
pub command_palette: CommandPalette,
|
pub command_palette: CommandPalette,
|
||||||
pub picker: Picker,
|
pub picker: Picker,
|
||||||
pub editor: Editor,
|
pub editor: Editor,
|
||||||
|
@ -880,6 +881,16 @@ impl<T> Interactive<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Toggleable<Interactive<T>> {
|
||||||
|
pub fn style_for(&self, active: bool, state: &mut MouseState) -> &T {
|
||||||
|
self.in_state(active).style_for(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_style(&self) -> &T {
|
||||||
|
&self.inactive.default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
|
@ -1045,6 +1056,75 @@ pub struct AssistantStyle {
|
||||||
pub saved_conversation: SavedConversation,
|
pub saved_conversation: SavedConversation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct Contained<T> {
|
||||||
|
container: ContainerStyle,
|
||||||
|
contained: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct FlexStyle {
|
||||||
|
// Between item spacing
|
||||||
|
item_spacing: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ChannelProjectStyle {
|
||||||
|
// TODO: Implement Contained Flex
|
||||||
|
// ContainerStyle + Spacing between elements
|
||||||
|
// Negative spacing overlaps elements instead of spacing them out
|
||||||
|
pub container: Contained<FlexStyle>,
|
||||||
|
pub host: ImageStyle,
|
||||||
|
pub title: ContainedText,
|
||||||
|
pub members: Contained<FlexStyle>,
|
||||||
|
pub member: ImageStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ChanneltemStyle {
|
||||||
|
pub icon: IconStyle,
|
||||||
|
pub title: TextStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ChannelListStyle {
|
||||||
|
pub section_title: ContainedText,
|
||||||
|
pub channel: Toggleable<Contained<ChanneltemStyle>>,
|
||||||
|
pub project: ChannelProjectStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ContactItemStyle {
|
||||||
|
pub container: Contained<FlexStyle>,
|
||||||
|
pub avatar: IconStyle,
|
||||||
|
pub name: TextStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ContactsListStyle {
|
||||||
|
pub section_title: ContainedText,
|
||||||
|
pub contact: ContactItemStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ChannelTreeStyle {
|
||||||
|
pub channel_indent: f32,
|
||||||
|
pub channel_name: TextStyle,
|
||||||
|
pub root_name: TextStyle,
|
||||||
|
pub channel_icon: Toggleable<Interactive<IconStyle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ChanelsPanelStyle {
|
||||||
|
pub channel_tree: ChannelTreeStyle,
|
||||||
|
pub spacing: f32,
|
||||||
|
// TODO: Uncomment:
|
||||||
|
// pub container: ContainerStyle,
|
||||||
|
// pub channel_list: ChannelListStyle,
|
||||||
|
// pub contacts_list: ContactsListStyle
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
pub struct SavedConversation {
|
pub struct SavedConversation {
|
||||||
pub container: Interactive<ContainerStyle>,
|
pub container: Interactive<ContainerStyle>,
|
||||||
|
|
|
@ -107,6 +107,16 @@ pub struct IconStyle {
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IconStyle {
|
||||||
|
pub fn width(&self) -> f32 {
|
||||||
|
self.icon.dimensions.width
|
||||||
|
+ self.container.padding.left
|
||||||
|
+ self.container.padding.right
|
||||||
|
+ self.container.margin.left
|
||||||
|
+ self.container.margin.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
|
pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
|
||||||
svg(&style.icon).contained().with_style(style.container)
|
svg(&style.icon).contained().with_style(style.container)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,27 @@ pub trait Panel: View {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
fn should_change_position_on_event(_: &Self::Event) -> bool;
|
fn should_change_position_on_event(_: &Self::Event) -> bool;
|
||||||
fn should_zoom_in_on_event(_: &Self::Event) -> bool;
|
fn should_zoom_in_on_event(_: &Self::Event) -> bool {
|
||||||
fn should_zoom_out_on_event(_: &Self::Event) -> bool;
|
false
|
||||||
fn is_zoomed(&self, cx: &WindowContext) -> bool;
|
}
|
||||||
fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>);
|
fn should_zoom_out_on_event(_: &Self::Event) -> bool {
|
||||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>);
|
false
|
||||||
fn should_activate_on_event(_: &Self::Event) -> bool;
|
}
|
||||||
fn should_close_on_event(_: &Self::Event) -> bool;
|
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
fn should_activate_on_event(_: &Self::Event) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn should_close_on_event(_: &Self::Event) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||||
fn is_focus_event(_: &Self::Event) -> bool;
|
fn is_focus_event(_: &Self::Event) -> bool;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ activity_indicator = { path = "../activity_indicator" }
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
breadcrumbs = { path = "../breadcrumbs" }
|
breadcrumbs = { path = "../breadcrumbs" }
|
||||||
call = { path = "../call" }
|
call = { path = "../call" }
|
||||||
|
channels = { path = "../channels" }
|
||||||
cli = { path = "../cli" }
|
cli = { path = "../cli" }
|
||||||
collab_ui = { path = "../collab_ui" }
|
collab_ui = { path = "../collab_ui" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
|
|
|
@ -155,6 +155,7 @@ fn main() {
|
||||||
outline::init(cx);
|
outline::init(cx);
|
||||||
project_symbols::init(cx);
|
project_symbols::init(cx);
|
||||||
project_panel::init(Assets, cx);
|
project_panel::init(Assets, cx);
|
||||||
|
channels::init(client.clone(), cx);
|
||||||
diagnostics::init(cx);
|
diagnostics::init(cx);
|
||||||
search::init(cx);
|
search::init(cx);
|
||||||
semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
|
semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
|
||||||
|
|
|
@ -9,6 +9,7 @@ use ai::AssistantPanel;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use breadcrumbs::Breadcrumbs;
|
use breadcrumbs::Breadcrumbs;
|
||||||
|
use channels::ChannelsPanel;
|
||||||
pub use client;
|
pub use client;
|
||||||
use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
|
use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
@ -221,6 +222,11 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
||||||
workspace.toggle_panel_focus::<ProjectPanel>(cx);
|
workspace.toggle_panel_focus::<ProjectPanel>(cx);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
cx.add_action(
|
||||||
|
|workspace: &mut Workspace, _: &channels::ToggleFocus, cx: &mut ViewContext<Workspace>| {
|
||||||
|
workspace.toggle_panel_focus::<channels::ChannelsPanel>(cx);
|
||||||
|
},
|
||||||
|
);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace,
|
|workspace: &mut Workspace,
|
||||||
_: &terminal_panel::ToggleFocus,
|
_: &terminal_panel::ToggleFocus,
|
||||||
|
@ -339,9 +345,13 @@ pub fn initialize_workspace(
|
||||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
let (project_panel, terminal_panel, assistant_panel) =
|
let channels_panel = ChannelsPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
|
let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
|
||||||
|
project_panel,
|
||||||
|
terminal_panel,
|
||||||
|
assistant_panel,
|
||||||
|
channels_panel
|
||||||
|
)?;
|
||||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||||
let project_panel_position = project_panel.position(cx);
|
let project_panel_position = project_panel.position(cx);
|
||||||
workspace.add_panel_with_extra_event_handler(
|
workspace.add_panel_with_extra_event_handler(
|
||||||
|
@ -359,6 +369,7 @@ pub fn initialize_workspace(
|
||||||
);
|
);
|
||||||
workspace.add_panel(terminal_panel, cx);
|
workspace.add_panel(terminal_panel, cx);
|
||||||
workspace.add_panel(assistant_panel, cx);
|
workspace.add_panel(assistant_panel, cx);
|
||||||
|
workspace.add_panel(channels_panel, cx);
|
||||||
|
|
||||||
if !was_deserialized
|
if !was_deserialized
|
||||||
&& workspace
|
&& workspace
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { titlebar } from "./titlebar"
|
||||||
import editor from "./editor"
|
import editor from "./editor"
|
||||||
import feedback from "./feedback"
|
import feedback from "./feedback"
|
||||||
import { useTheme } from "../common"
|
import { useTheme } from "../common"
|
||||||
|
import channels_panel from "./channels_panel"
|
||||||
|
|
||||||
export default function app(): any {
|
export default function app(): any {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
@ -46,6 +47,7 @@ export default function app(): any {
|
||||||
editor: editor(),
|
editor: editor(),
|
||||||
project_diagnostics: project_diagnostics(),
|
project_diagnostics: project_diagnostics(),
|
||||||
project_panel: project_panel(),
|
project_panel: project_panel(),
|
||||||
|
channels_panel: channels_panel(),
|
||||||
contacts_popover: contacts_popover(),
|
contacts_popover: contacts_popover(),
|
||||||
contact_finder: contact_finder(),
|
contact_finder: contact_finder(),
|
||||||
contact_list: contact_list(),
|
contact_list: contact_list(),
|
||||||
|
|
68
styles/src/style_tree/channels_panel.ts
Normal file
68
styles/src/style_tree/channels_panel.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// import { with_opacity } from "../theme/color"
|
||||||
|
import {
|
||||||
|
// Border,
|
||||||
|
// TextStyle,
|
||||||
|
// background,
|
||||||
|
// border,
|
||||||
|
foreground,
|
||||||
|
text,
|
||||||
|
} from "./components"
|
||||||
|
import { interactive, toggleable } from "../element"
|
||||||
|
// import merge from "ts-deepmerge"
|
||||||
|
import { useTheme } from "../theme"
|
||||||
|
export default function channels_panel(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
// const { is_light } = theme
|
||||||
|
|
||||||
|
return {
|
||||||
|
spacing: 10,
|
||||||
|
channel_tree: {
|
||||||
|
channel_indent: 10,
|
||||||
|
channel_name: text(theme.middle, "sans", "variant", { size: "md" }),
|
||||||
|
root_name: text(theme.middle, "sans", "variant", { size: "lg", weight: "bold" }),
|
||||||
|
channel_icon: (() => {
|
||||||
|
const base_icon = (asset: any, color: any) => {
|
||||||
|
return {
|
||||||
|
icon: {
|
||||||
|
color,
|
||||||
|
asset,
|
||||||
|
dimensions: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
corner_radius: 4,
|
||||||
|
padding: {
|
||||||
|
top: 4, bottom: 4, left: 4, right: 4
|
||||||
|
},
|
||||||
|
margin: {
|
||||||
|
right: 4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toggleable({
|
||||||
|
state: {
|
||||||
|
inactive: interactive({
|
||||||
|
state: {
|
||||||
|
default: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "variant")),
|
||||||
|
hovered: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "hovered")),
|
||||||
|
clicked: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "active")),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
active: interactive({
|
||||||
|
state: {
|
||||||
|
default: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "variant")),
|
||||||
|
hovered: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "hovered")),
|
||||||
|
clicked: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "active")),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue