Detailed changes
@@ -1556,6 +1556,19 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "component_test"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "project",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
[[package]]
name = "concurrent-queue"
version = "2.2.0"
@@ -9653,6 +9666,7 @@ dependencies = [
"collab_ui",
"collections",
"command_palette",
+ "component_test",
"context_menu",
"copilot",
"copilot_button",
@@ -13,6 +13,7 @@ members = [
"crates/collab_ui",
"crates/collections",
"crates/command_palette",
+ "crates/component_test",
"crates/context_menu",
"crates/copilot",
"crates/copilot_button",
@@ -114,6 +114,16 @@ impl ChannelStore {
}
}
+ pub fn has_children(&self, channel_id: ChannelId) -> bool {
+ self.channel_paths.iter().any(|path| {
+ if let Some(ix) = path.iter().position(|id| *id == channel_id) {
+ path.len() > ix + 1
+ } else {
+ false
+ }
+ })
+ }
+
pub fn channel_count(&self) -> usize {
self.channel_paths.len()
}
@@ -16,8 +16,9 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions,
elements::{
- Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState,
- MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg,
+ Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
+ MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
+ Stack, Svg,
},
geometry::{
rect::RectF,
@@ -35,7 +36,7 @@ use serde_derive::{Deserialize, Serialize};
use settings::SettingsStore;
use staff_mode::StaffMode;
use std::{borrow::Cow, mem, sync::Arc};
-use theme::IconButton;
+use theme::{components::ComponentExt, IconButton};
use util::{iife, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel},
@@ -53,6 +54,11 @@ struct RemoveChannel {
channel_id: u64,
}
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct ToggleCollapse {
+ channel_id: u64,
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct NewChannel {
channel_id: u64,
@@ -73,7 +79,16 @@ struct RenameChannel {
channel_id: u64,
}
-actions!(collab_panel, [ToggleFocus, Remove, Secondary]);
+actions!(
+ collab_panel,
+ [
+ ToggleFocus,
+ Remove,
+ Secondary,
+ CollapseSelectedChannel,
+ ExpandSelectedChannel
+ ]
+);
impl_actions!(
collab_panel,
@@ -82,7 +97,8 @@ impl_actions!(
NewChannel,
InviteMembers,
ManageMembers,
- RenameChannel
+ RenameChannel,
+ ToggleCollapse
]
);
@@ -105,6 +121,9 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
cx.add_action(CollabPanel::manage_members);
cx.add_action(CollabPanel::rename_selected_channel);
cx.add_action(CollabPanel::rename_channel);
+ cx.add_action(CollabPanel::toggle_channel_collapsed);
+ cx.add_action(CollabPanel::collapse_selected_channel);
+ cx.add_action(CollabPanel::expand_selected_channel)
}
#[derive(Debug)]
@@ -147,6 +166,7 @@ pub struct CollabPanel {
list_state: ListState<Self>,
subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>,
+ collapsed_channels: Vec<ChannelId>,
workspace: WeakViewHandle<Workspace>,
context_menu_on_selected: bool,
}
@@ -398,6 +418,7 @@ impl CollabPanel {
subscriptions: Vec::default(),
match_candidates: Vec::default(),
collapsed_sections: vec![Section::Offline],
+ collapsed_channels: Vec::default(),
workspace: workspace.weak_handle(),
client: workspace.app_state().client.clone(),
context_menu_on_selected: true,
@@ -657,10 +678,24 @@ impl CollabPanel {
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
}
}
+ let mut collapse_depth = None;
for mat in matches {
let (depth, channel) =
channel_store.channel_at_index(mat.candidate_id).unwrap();
+ if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+ collapse_depth = Some(depth);
+ } else if let Some(collapsed_depth) = collapse_depth {
+ if depth > collapsed_depth {
+ continue;
+ }
+ if self.is_channel_collapsed(channel.id) {
+ collapse_depth = Some(depth);
+ } else {
+ collapse_depth = None;
+ }
+ }
+
match &self.channel_editing_state {
Some(ChannelEditingState::Create { parent_id, .. })
if *parent_id == Some(channel.id) =>
@@ -1332,7 +1367,7 @@ impl CollabPanel {
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if can_collapse {
- this.toggle_expanded(section, cx);
+ this.toggle_section_expanded(section, cx);
}
})
}
@@ -1479,6 +1514,11 @@ impl CollabPanel {
cx: &AppContext,
) -> AnyElement<Self> {
Flex::row()
+ .with_child(
+ Empty::new()
+ .constrained()
+ .with_width(theme.collab_panel.disclosure.button_space()),
+ )
.with_child(
Svg::new("icons/hash.svg")
.with_color(theme.collab_panel.channel_hash.color)
@@ -1537,6 +1577,10 @@ impl CollabPanel {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let channel_id = channel.id;
+ let has_children = self.channel_store.read(cx).has_children(channel_id);
+ let disclosed =
+ has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
+
let is_active = iife!({
let call_channel = ActiveCall::global(cx)
.read(cx)
@@ -1550,7 +1594,7 @@ impl CollabPanel {
const FACEPILE_LIMIT: usize = 3;
MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
- Flex::row()
+ Flex::<Self>::row()
.with_child(
Svg::new("icons/hash.svg")
.with_color(theme.channel_hash.color)
@@ -1599,6 +1643,11 @@ impl CollabPanel {
}
})
.align_children_center()
+ .styleable_component()
+ .disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
+ .with_id(channel_id as usize)
+ .with_style(theme.disclosure.clone())
+ .element()
.constrained()
.with_height(theme.row_height)
.contained()
@@ -1825,6 +1874,12 @@ impl CollabPanel {
OverlayPositionMode::Window
});
+ let expand_action_name = if self.is_channel_collapsed(channel_id) {
+ "Expand Subchannels"
+ } else {
+ "Collapse Subchannels"
+ };
+
context_menu.show(
position.unwrap_or_default(),
if self.context_menu_on_selected {
@@ -1833,6 +1888,7 @@ impl CollabPanel {
gpui::elements::AnchorCorner::BottomLeft
},
vec![
+ ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
ContextMenuItem::Separator,
ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }),
@@ -1912,7 +1968,7 @@ impl CollabPanel {
| Section::Online
| Section::Offline
| Section::ChannelInvites => {
- self.toggle_expanded(*section, cx);
+ self.toggle_section_expanded(*section, cx);
}
},
ListEntry::Contact { contact, calling } => {
@@ -2000,7 +2056,7 @@ impl CollabPanel {
}
}
- fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
+ fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
self.collapsed_sections.remove(ix);
} else {
@@ -2009,6 +2065,54 @@ impl CollabPanel {
self.update_entries(false, cx);
}
+ fn collapse_selected_channel(
+ &mut self,
+ _: &CollapseSelectedChannel,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+ return;
+ };
+
+ if self.is_channel_collapsed(channel_id) {
+ return;
+ }
+
+ self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+ }
+
+ fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
+ let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+ return;
+ };
+
+ if !self.is_channel_collapsed(channel_id) {
+ return;
+ }
+
+ self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+ }
+
+ fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
+ let channel_id = action.channel_id;
+
+ match self.collapsed_channels.binary_search(&channel_id) {
+ Ok(ix) => {
+ self.collapsed_channels.remove(ix);
+ }
+ Err(ix) => {
+ self.collapsed_channels.insert(ix, channel_id);
+ }
+ };
+ self.update_entries(true, cx);
+ cx.notify();
+ cx.focus_self();
+ }
+
+ fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
+ self.collapsed_channels.binary_search(&channel).is_ok()
+ }
+
fn leave_call(cx: &mut ViewContext<Self>) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
@@ -2048,6 +2152,8 @@ impl CollabPanel {
}
fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
+ self.collapsed_channels
+ .retain(|&channel| channel != action.channel_id);
self.channel_editing_state = Some(ChannelEditingState::Create {
parent_id: Some(action.channel_id),
pending_name: None,
@@ -0,0 +1,18 @@
+[package]
+name = "component_test"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/component_test.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+gpui = { path = "../gpui" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+project = { path = "../project" }
@@ -0,0 +1,121 @@
+use gpui::{
+ actions,
+ elements::{Component, Flex, ParentElement, SafeStylable},
+ AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use project::Project;
+use theme::components::{action_button::Button, label::Label, ComponentExt};
+use workspace::{
+ item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId,
+};
+
+pub fn init(cx: &mut AppContext) {
+ cx.add_action(ComponentTest::toggle_disclosure);
+ cx.add_action(ComponentTest::toggle_toggle);
+ cx.add_action(ComponentTest::deploy);
+ register_deserializable_item::<ComponentTest>(cx);
+}
+
+actions!(
+ test,
+ [NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest]
+);
+
+struct ComponentTest {
+ disclosed: bool,
+ toggled: bool,
+}
+
+impl ComponentTest {
+ fn new() -> Self {
+ Self {
+ disclosed: false,
+ toggled: false,
+ }
+ }
+
+ fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext<Workspace>) {
+ workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx);
+ }
+
+ fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext<Self>) {
+ self.disclosed = !self.disclosed;
+ cx.notify();
+ }
+
+ fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext<Self>) {
+ self.toggled = !self.toggled;
+ cx.notify();
+ }
+}
+
+impl Entity for ComponentTest {
+ type Event = ();
+}
+
+impl View for ComponentTest {
+ fn ui_name() -> &'static str {
+ "Component Test"
+ }
+
+ fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
+ let theme = theme::current(cx);
+
+ PaneBackdrop::new(
+ cx.view_id(),
+ Flex::column()
+ .with_spacing(10.)
+ .with_child(
+ Button::action(NoAction)
+ .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
+ .with_contents(Label::new("Click me!"))
+ .with_style(theme.component_test.button.clone())
+ .element(),
+ )
+ .with_child(
+ Button::action(ToggleToggle)
+ .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
+ .with_contents(Label::new("Toggle me!"))
+ .toggleable(self.toggled)
+ .with_style(theme.component_test.toggle.clone())
+ .element(),
+ )
+ .with_child(
+ Label::new("A disclosure")
+ .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure))
+ .with_style(theme.component_test.disclosure.clone())
+ .element(),
+ )
+ .constrained()
+ .with_width(200.)
+ .aligned()
+ .into_any(),
+ )
+ .into_any()
+ }
+}
+
+impl Item for ComponentTest {
+ fn tab_content<V: 'static>(
+ &self,
+ _: Option<usize>,
+ style: &theme::Tab,
+ _: &AppContext,
+ ) -> gpui::AnyElement<V> {
+ gpui::elements::Label::new("Component test", style.label.clone()).into_any()
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("ComponentTest")
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ _workspace_id: WorkspaceId,
+ _item_id: ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+ Task::ready(Ok(cx.add_view(|_| Self::new())))
+ }
+}
@@ -2,7 +2,7 @@ use button_component::Button;
use gpui::{
color::Color,
- elements::{Component, ContainerStyle, Flex, Label, ParentElement},
+ elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent},
fonts::{self, TextStyle},
platform::WindowOptions,
AnyElement, App, Element, Entity, View, ViewContext,
@@ -114,7 +114,7 @@ mod theme {
// Component creation:
mod toggleable_button {
use gpui::{
- elements::{Component, ContainerStyle, LabelStyle},
+ elements::{ContainerStyle, LabelStyle, StatefulComponent},
scene::MouseClick,
EventContext, View,
};
@@ -156,7 +156,7 @@ mod toggleable_button {
}
}
- impl<V: View> Component<V> for ToggleableButton<V> {
+ impl<V: View> StatefulComponent<V> for ToggleableButton<V> {
fn render(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
let button = if let Some(style) = self.style {
self.button.with_style(*style.style_for(self.active))
@@ -171,7 +171,7 @@ mod toggleable_button {
mod button_component {
use gpui::{
- elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler},
+ elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent},
platform::MouseButton,
scene::MouseClick,
AnyElement, Element, EventContext, TypeTag, View, ViewContext,
@@ -212,7 +212,7 @@ mod button_component {
}
}
- impl<V: View> Component<V> for Button<V> {
+ impl<V: View> StatefulComponent<V> for Button<V> {
fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
let click_handler = self.click_handler;
@@ -234,6 +234,27 @@ pub trait Element<V: 'static>: 'static {
{
MouseEventHandler::for_child::<Tag>(self.into_any(), region_id)
}
+
+ fn component(self) -> StatelessElementAdapter
+ where
+ Self: Sized,
+ {
+ StatelessElementAdapter::new(self.into_any())
+ }
+
+ fn stateful_component(self) -> StatefulElementAdapter<V>
+ where
+ Self: Sized,
+ {
+ StatefulElementAdapter::new(self.into_any())
+ }
+
+ fn styleable_component(self) -> StylableAdapter<StatelessElementAdapter>
+ where
+ Self: Sized,
+ {
+ StatelessElementAdapter::new(self.into_any()).stylable()
+ }
}
trait AnyElementState<V> {
@@ -1,47 +1,96 @@
-use std::marker::PhantomData;
+use std::{any::Any, marker::PhantomData};
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use crate::{
- AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
- ViewContext,
+ AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
};
use super::Empty;
-pub trait GeneralComponent {
- fn render<V: View>(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
- fn element<V: View>(self) -> ComponentAdapter<V, Self>
+/// The core stateless component trait, simply rendering an element tree
+pub trait Component {
+ fn render<V: 'static>(self, cx: &mut ViewContext<V>) -> AnyElement<V>;
+
+ fn element<V: 'static>(self) -> ComponentAdapter<V, Self>
where
Self: Sized,
{
ComponentAdapter::new(self)
}
+
+ fn stylable(self) -> StylableAdapter<Self>
+ where
+ Self: Sized,
+ {
+ StylableAdapter::new(self)
+ }
+
+ fn stateful<V: 'static>(self) -> StatefulAdapter<Self, V>
+ where
+ Self: Sized,
+ {
+ StatefulAdapter::new(self)
+ }
}
-pub trait StyleableComponent {
+/// Allows a a component's styles to be rebound in a simple way.
+pub trait Stylable: Component {
type Style: Clone;
- type Output: GeneralComponent;
+
+ fn with_style(self, style: Self::Style) -> Self;
+}
+
+/// This trait models the typestate pattern for a component's style,
+/// enforcing at compile time that a component is only usable after
+/// it has been styled while still allowing for late binding of the
+/// styling information
+pub trait SafeStylable {
+ type Style: Clone;
+ type Output: Component;
fn with_style(self, style: Self::Style) -> Self::Output;
}
-impl GeneralComponent for () {
- fn render<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
- Empty::new().into_any()
+/// All stylable components can trivially implement SafeStylable
+impl<C: Stylable> SafeStylable for C {
+ type Style = C::Style;
+
+ type Output = C;
+
+ fn with_style(self, style: Self::Style) -> Self::Output {
+ self.with_style(style)
+ }
+}
+
+/// Allows converting an unstylable component into a stylable one
+/// by using `()` as the style type
+pub struct StylableAdapter<C: Component> {
+ component: C,
+}
+
+impl<C: Component> StylableAdapter<C> {
+ pub fn new(component: C) -> Self {
+ Self { component }
}
}
-impl StyleableComponent for () {
+impl<C: Component> SafeStylable for StylableAdapter<C> {
type Style = ();
- type Output = ();
+
+ type Output = C;
fn with_style(self, _: Self::Style) -> Self::Output {
- ()
+ self.component
}
}
-pub trait Component<V: View> {
+/// This is a secondary trait for components that can be styled
+/// which rely on their view's state. This is useful for components that, for example,
+/// want to take click handler callbacks Unfortunately, the generic bound on the
+/// Component trait makes it incompatible with the stateless components above.
+// So let's just replicate them for now
+pub trait StatefulComponent<V: 'static> {
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
fn element(self) -> ComponentAdapter<V, Self>
@@ -50,21 +99,63 @@ pub trait Component<V: View> {
{
ComponentAdapter::new(self)
}
+
+ fn styleable(self) -> StatefulStylableAdapter<Self, V>
+ where
+ Self: Sized,
+ {
+ StatefulStylableAdapter::new(self)
+ }
+
+ fn stateless(self) -> StatelessElementAdapter
+ where
+ Self: Sized + 'static,
+ {
+ StatelessElementAdapter::new(self.element().into_any())
+ }
}
-impl<V: View, C: GeneralComponent> Component<V> for C {
- fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
- self.render(v, cx)
+/// It is trivial to convert stateless components to stateful components, so lets
+/// do so en masse. Note that the reverse is impossible without a helper.
+impl<V: 'static, C: Component> StatefulComponent<V> for C {
+ fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
+ self.render(cx)
}
}
-// StylableComponent -> GeneralComponent
-pub struct StylableComponentAdapter<C: Component<V>, V: View> {
+/// Same as stylable, but generic over a view type
+pub trait StatefulStylable<V: 'static>: StatefulComponent<V> {
+ type Style: Clone;
+
+ fn with_style(self, style: Self::Style) -> Self;
+}
+
+/// Same as SafeStylable, but generic over a view type
+pub trait StatefulSafeStylable<V: 'static> {
+ type Style: Clone;
+ type Output: StatefulComponent<V>;
+
+ fn with_style(self, style: Self::Style) -> Self::Output;
+}
+
+/// Converting from stateless to stateful
+impl<V: 'static, C: SafeStylable> StatefulSafeStylable<V> for C {
+ type Style = C::Style;
+
+ type Output = C::Output;
+
+ fn with_style(self, style: Self::Style) -> Self::Output {
+ self.with_style(style)
+ }
+}
+
+// A helper for converting stateless components into stateful ones
+pub struct StatefulAdapter<C, V> {
component: C,
phantom: std::marker::PhantomData<V>,
}
-impl<C: Component<V>, V: View> StylableComponentAdapter<C, V> {
+impl<C: Component, V: 'static> StatefulAdapter<C, V> {
pub fn new(component: C) -> Self {
Self {
component,
@@ -73,7 +164,31 @@ impl<C: Component<V>, V: View> StylableComponentAdapter<C, V> {
}
}
-impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapter<C, V> {
+impl<C: Component, V: 'static> StatefulComponent<V> for StatefulAdapter<C, V> {
+ fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
+ self.component.render(cx)
+ }
+}
+
+// A helper for converting stateful but style-less components into stylable ones
+// by using `()` as the style type
+pub struct StatefulStylableAdapter<C: StatefulComponent<V>, V: 'static> {
+ component: C,
+ phantom: std::marker::PhantomData<V>,
+}
+
+impl<C: StatefulComponent<V>, V: 'static> StatefulStylableAdapter<C, V> {
+ pub fn new(component: C) -> Self {
+ Self {
+ component,
+ phantom: std::marker::PhantomData,
+ }
+ }
+}
+
+impl<C: StatefulComponent<V>, V: 'static> StatefulSafeStylable<V>
+ for StatefulStylableAdapter<C, V>
+{
type Style = ();
type Output = C;
@@ -83,13 +198,37 @@ impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapt
}
}
-// Element -> Component
-pub struct ElementAdapter<V: View> {
+/// A way of erasing the view generic from an element, useful
+/// for wrapping up an explicit element tree into stateless
+/// components
+pub struct StatelessElementAdapter {
+ element: Box<dyn Any>,
+}
+
+impl StatelessElementAdapter {
+ pub fn new<V: 'static>(element: AnyElement<V>) -> Self {
+ StatelessElementAdapter {
+ element: Box::new(element) as Box<dyn Any>,
+ }
+ }
+}
+
+impl Component for StatelessElementAdapter {
+ fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
+ *self
+ .element
+ .downcast::<AnyElement<V>>()
+ .expect("Don't move elements out of their view :(")
+ }
+}
+
+// For converting elements into stateful components
+pub struct StatefulElementAdapter<V: 'static> {
element: AnyElement<V>,
_phantom: std::marker::PhantomData<V>,
}
-impl<V: View> ElementAdapter<V> {
+impl<V: 'static> StatefulElementAdapter<V> {
pub fn new(element: AnyElement<V>) -> Self {
Self {
element,
@@ -98,20 +237,35 @@ impl<V: View> ElementAdapter<V> {
}
}
-impl<V: View> Component<V> for ElementAdapter<V> {
+impl<V: 'static> StatefulComponent<V> for StatefulElementAdapter<V> {
fn render(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
self.element
}
}
-// Component -> Element
-pub struct ComponentAdapter<V: View, E> {
+/// A convenient shorthand for creating an empty component.
+impl Component for () {
+ fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
+ Empty::new().into_any()
+ }
+}
+
+impl Stylable for () {
+ type Style = ();
+
+ fn with_style(self, _: Self::Style) -> Self {
+ ()
+ }
+}
+
+// For converting components back into Elements
+pub struct ComponentAdapter<V: 'static, E> {
component: Option<E>,
element: Option<AnyElement<V>>,
phantom: PhantomData<V>,
}
-impl<E, V: View> ComponentAdapter<V, E> {
+impl<E, V: 'static> ComponentAdapter<V, E> {
pub fn new(e: E) -> Self {
Self {
component: Some(e),
@@ -121,7 +275,7 @@ impl<E, V: View> ComponentAdapter<V, E> {
}
}
-impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
+impl<V: 'static, C: StatefulComponent<V> + 'static> Element<V> for ComponentAdapter<V, C> {
type LayoutState = ();
type PaintState = ();
@@ -184,6 +338,7 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
) -> serde_json::Value {
serde_json::json!({
"type": "ComponentAdapter",
+ "component": std::any::type_name::<C>(),
"child": self.element.as_ref().map(|el| el.debug(view, cx)),
})
}
@@ -44,6 +44,14 @@ impl ContainerStyle {
..Default::default()
}
}
+
+ pub fn additional_length(&self) -> f32 {
+ self.padding.left
+ + self.padding.right
+ + self.border.width * 2.
+ + self.margin.left
+ + self.margin.right
+ }
}
pub struct Container<V> {
@@ -22,6 +22,7 @@ pub struct Flex<V> {
children: Vec<AnyElement<V>>,
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
child_alignment: f32,
+ spacing: f32,
}
impl<V: 'static> Flex<V> {
@@ -31,6 +32,7 @@ impl<V: 'static> Flex<V> {
children: Default::default(),
scroll_state: None,
child_alignment: -1.,
+ spacing: 0.,
}
}
@@ -51,6 +53,11 @@ impl<V: 'static> Flex<V> {
self
}
+ pub fn with_spacing(mut self, spacing: f32) -> Self {
+ self.spacing = spacing;
+ self
+ }
+
pub fn scrollable<Tag>(
mut self,
element_id: usize,
@@ -81,7 +88,8 @@ impl<V: 'static> Flex<V> {
cx: &mut LayoutContext<V>,
) {
let cross_axis = self.axis.invert();
- for child in &mut self.children {
+ let last = self.children.len() - 1;
+ for (ix, child) in &mut self.children.iter_mut().enumerate() {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if let Some((flex, expanded)) = metadata.flex {
if expanded != layout_expanded {
@@ -93,6 +101,10 @@ impl<V: 'static> Flex<V> {
} else {
let space_per_flex = *remaining_space / *remaining_flex;
space_per_flex * flex
+ } - if ix == 0 || ix == last {
+ self.spacing / 2.
+ } else {
+ self.spacing
};
let child_min = if expanded { child_max } else { 0. };
let child_constraint = match self.axis {
@@ -137,7 +149,8 @@ impl<V: 'static> Element<V> for Flex<V> {
let cross_axis = self.axis.invert();
let mut cross_axis_max: f32 = 0.0;
- for child in &mut self.children {
+ let last = self.children.len().saturating_sub(1);
+ for (ix, child) in &mut self.children.iter_mut().enumerate() {
let metadata = child.metadata::<FlexParentData>();
contains_float |= metadata.map_or(false, |metadata| metadata.float);
@@ -155,7 +168,12 @@ impl<V: 'static> Element<V> for Flex<V> {
),
};
let size = child.layout(child_constraint, view, cx);
- fixed_space += size.along(self.axis);
+ fixed_space += size.along(self.axis)
+ + if ix == 0 || ix == last {
+ self.spacing / 2.
+ } else {
+ self.spacing
+ };
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
}
}
@@ -315,7 +333,8 @@ impl<V: 'static> Element<V> for Flex<V> {
}
}
- for child in &mut self.children {
+ let last = self.children.len().saturating_sub(1);
+ for (ix, child) in &mut self.children.iter_mut().enumerate() {
if remaining_space > 0. {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if metadata.float {
@@ -353,9 +372,11 @@ impl<V: 'static> Element<V> for Flex<V> {
child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
+ let spacing = if ix == last { 0. } else { self.spacing };
+
match self.axis {
- Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
- Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
+ Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0),
+ Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing),
}
}
@@ -2,13 +2,13 @@ use bitflags::bitflags;
pub use buffer_search::BufferSearchBar;
use gpui::{
actions,
- elements::{Component, StyleableComponent, TooltipStyle},
+ elements::{Component, SafeStylable, TooltipStyle},
Action, AnyElement, AppContext, Element, View,
};
pub use mode::SearchMode;
use project::search::SearchQuery;
pub use project_search::{ProjectSearchBar, ProjectSearchView};
-use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle};
+use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle};
pub mod buffer_search;
mod history;
@@ -89,15 +89,12 @@ impl SearchOptions {
tooltip_style: TooltipStyle,
button_style: ToggleIconButtonStyle,
) -> AnyElement<V> {
- ActionButton::new_dynamic(
- self.to_toggle_action(),
- format!("Toggle {}", self.label()),
- tooltip_style,
- )
- .with_contents(theme::components::svg::Svg::new(self.icon()))
- .toggleable(active)
- .with_style(button_style)
- .element()
- .into_any()
+ Button::dynamic_action(self.to_toggle_action())
+ .with_tooltip(format!("Toggle {}", self.label()), tooltip_style)
+ .with_contents(Svg::new(self.icon()))
+ .toggleable(active)
+ .with_style(button_style)
+ .element()
+ .into_any()
}
}
@@ -1,23 +1,143 @@
-use gpui::elements::StyleableComponent;
+use gpui::{elements::SafeStylable, Action};
use crate::{Interactive, Toggleable};
-use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle};
+use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle};
-pub type ToggleIconButtonStyle = Toggleable<Interactive<ButtonStyle<SvgStyle>>>;
+pub type IconButtonStyle = Interactive<ButtonStyle<SvgStyle>>;
+pub type ToggleIconButtonStyle = Toggleable<IconButtonStyle>;
-pub trait ComponentExt<C: StyleableComponent> {
+pub trait ComponentExt<C: SafeStylable> {
fn toggleable(self, active: bool) -> Toggle<C, ()>;
+ fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()>;
}
-impl<C: StyleableComponent> ComponentExt<C> for C {
+impl<C: SafeStylable> ComponentExt<C> for C {
fn toggleable(self, active: bool) -> Toggle<C, ()> {
Toggle::new(self, active)
}
+
+ /// Some(True) => disclosed => content is visible
+ /// Some(false) => closed => content is hidden
+ /// None => No disclosure button, but reserve disclosure spacing
+ fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()> {
+ Disclosable::new(disclosed, self, action)
+ }
+}
+
+pub mod disclosure {
+
+ use gpui::{
+ elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable},
+ Action, Element,
+ };
+ use schemars::JsonSchema;
+ use serde_derive::Deserialize;
+
+ use super::{action_button::Button, svg::Svg, IconButtonStyle};
+
+ #[derive(Clone, Default, Deserialize, JsonSchema)]
+ pub struct DisclosureStyle<S> {
+ pub button: IconButtonStyle,
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ pub spacing: f32,
+ #[serde(flatten)]
+ content: S,
+ }
+
+ impl<S> DisclosureStyle<S> {
+ pub fn button_space(&self) -> f32 {
+ self.spacing + self.button.button_width.unwrap()
+ }
+ }
+
+ pub struct Disclosable<C, S> {
+ disclosed: Option<bool>,
+ action: Box<dyn Action>,
+ id: usize,
+ content: C,
+ style: S,
+ }
+
+ impl Disclosable<(), ()> {
+ pub fn new<C>(
+ disclosed: Option<bool>,
+ content: C,
+ action: Box<dyn Action>,
+ ) -> Disclosable<C, ()> {
+ Disclosable {
+ disclosed,
+ content,
+ action,
+ id: 0,
+ style: (),
+ }
+ }
+ }
+
+ impl<C> Disclosable<C, ()> {
+ pub fn with_id(mut self, id: usize) -> Disclosable<C, ()> {
+ self.id = id;
+ self
+ }
+ }
+
+ impl<C: SafeStylable> SafeStylable for Disclosable<C, ()> {
+ type Style = DisclosureStyle<C::Style>;
+
+ type Output = Disclosable<C, Self::Style>;
+
+ fn with_style(self, style: Self::Style) -> Self::Output {
+ Disclosable {
+ disclosed: self.disclosed,
+ action: self.action,
+ content: self.content,
+ id: self.id,
+ style,
+ }
+ }
+ }
+
+ impl<C: SafeStylable> Component for Disclosable<C, DisclosureStyle<C::Style>> {
+ fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+ Flex::row()
+ .with_spacing(self.style.spacing)
+ .with_child(if let Some(disclosed) = self.disclosed {
+ Button::dynamic_action(self.action)
+ .with_id(self.id)
+ .with_contents(Svg::new(if disclosed {
+ "icons/file_icons/chevron_down.svg"
+ } else {
+ "icons/file_icons/chevron_right.svg"
+ }))
+ .with_style(self.style.button)
+ .element()
+ .into_any()
+ } else {
+ Empty::new()
+ .into_any()
+ .constrained()
+ // TODO: Why is this optional at all?
+ .with_width(self.style.button.button_width.unwrap())
+ .into_any()
+ })
+ .with_child(
+ self.content
+ .with_style(self.style.content)
+ .render(cx)
+ .flex(1., true),
+ )
+ .align_children_center()
+ .contained()
+ .with_style(self.style.container)
+ .into_any()
+ }
+ }
}
pub mod toggle {
- use gpui::elements::{GeneralComponent, StyleableComponent};
+ use gpui::elements::{Component, SafeStylable};
use crate::Toggleable;
@@ -27,7 +147,7 @@ pub mod toggle {
component: C,
}
- impl<C: StyleableComponent> Toggle<C, ()> {
+ impl<C: SafeStylable> Toggle<C, ()> {
pub fn new(component: C, active: bool) -> Self {
Toggle {
active,
@@ -37,7 +157,7 @@ pub mod toggle {
}
}
- impl<C: StyleableComponent> StyleableComponent for Toggle<C, ()> {
+ impl<C: SafeStylable> SafeStylable for Toggle<C, ()> {
type Style = Toggleable<C::Style>;
type Output = Toggle<C, Self::Style>;
@@ -51,15 +171,11 @@ pub mod toggle {
}
}
- impl<C: StyleableComponent> GeneralComponent for Toggle<C, Toggleable<C::Style>> {
- fn render<V: gpui::View>(
- self,
- v: &mut V,
- cx: &mut gpui::ViewContext<V>,
- ) -> gpui::AnyElement<V> {
+ impl<C: SafeStylable> Component for Toggle<C, Toggleable<C::Style>> {
+ fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
self.component
.with_style(self.style.in_state(self.active).clone())
- .render(v, cx)
+ .render(cx)
}
}
}
@@ -68,96 +184,103 @@ pub mod action_button {
use std::borrow::Cow;
use gpui::{
- elements::{
- ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle,
- },
+ elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle},
platform::{CursorStyle, MouseButton},
- Action, Element, TypeTag, View,
+ Action, Element, TypeTag,
};
use schemars::JsonSchema;
use serde_derive::Deserialize;
use crate::Interactive;
- pub struct ActionButton<C, S> {
- action: Box<dyn Action>,
- tooltip: Cow<'static, str>,
- tooltip_style: TooltipStyle,
- tag: TypeTag,
- contents: C,
- style: Interactive<S>,
- }
-
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct ButtonStyle<C> {
#[serde(flatten)]
- container: ContainerStyle,
- button_width: Option<f32>,
- button_height: Option<f32>,
+ pub container: ContainerStyle,
+ // TODO: These are incorrect for the intended usage of the buttons.
+ // The size should be constant, but putting them here duplicates them
+ // across the states the buttons can be in
+ pub button_width: Option<f32>,
+ pub button_height: Option<f32>,
#[serde(flatten)]
contents: C,
}
- impl ActionButton<(), ()> {
- pub fn new_dynamic(
- action: Box<dyn Action>,
- tooltip: impl Into<Cow<'static, str>>,
- tooltip_style: TooltipStyle,
- ) -> Self {
+ pub struct Button<C, S> {
+ action: Box<dyn Action>,
+ tooltip: Option<(Cow<'static, str>, TooltipStyle)>,
+ tag: TypeTag,
+ id: usize,
+ contents: C,
+ style: Interactive<S>,
+ }
+
+ impl Button<(), ()> {
+ pub fn dynamic_action(action: Box<dyn Action>) -> Button<(), ()> {
Self {
contents: (),
tag: action.type_tag(),
- style: Interactive::new_blank(),
- tooltip: tooltip.into(),
- tooltip_style,
action,
+ style: Interactive::new_blank(),
+ tooltip: None,
+ id: 0,
}
}
- pub fn new<A: Action + Clone>(
- action: A,
+ pub fn action<A: Action + Clone>(action: A) -> Self {
+ Self::dynamic_action(Box::new(action))
+ }
+
+ pub fn with_tooltip(
+ mut self,
tooltip: impl Into<Cow<'static, str>>,
tooltip_style: TooltipStyle,
) -> Self {
- Self::new_dynamic(Box::new(action), tooltip, tooltip_style)
+ self.tooltip = Some((tooltip.into(), tooltip_style));
+ self
+ }
+
+ pub fn with_id(mut self, id: usize) -> Self {
+ self.id = id;
+ self
}
- pub fn with_contents<C: StyleableComponent>(self, contents: C) -> ActionButton<C, ()> {
- ActionButton {
+ pub fn with_contents<C: SafeStylable>(self, contents: C) -> Button<C, ()> {
+ Button {
action: self.action,
tag: self.tag,
style: self.style,
tooltip: self.tooltip,
- tooltip_style: self.tooltip_style,
+ id: self.id,
contents,
}
}
}
- impl<C: StyleableComponent> StyleableComponent for ActionButton<C, ()> {
+ impl<C: SafeStylable> SafeStylable for Button<C, ()> {
type Style = Interactive<ButtonStyle<C::Style>>;
- type Output = ActionButton<C, ButtonStyle<C::Style>>;
+ type Output = Button<C, ButtonStyle<C::Style>>;
fn with_style(self, style: Self::Style) -> Self::Output {
- ActionButton {
+ Button {
action: self.action,
tag: self.tag,
contents: self.contents,
tooltip: self.tooltip,
- tooltip_style: self.tooltip_style,
+ id: self.id,
style,
}
}
}
- impl<C: StyleableComponent> GeneralComponent for ActionButton<C, ButtonStyle<C::Style>> {
- fn render<V: View>(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
- MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| {
+ impl<C: SafeStylable> Component for Button<C, ButtonStyle<C::Style>> {
+ fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+ let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| {
let style = self.style.style_for(state);
let mut contents = self
.contents
.with_style(style.contents.to_owned())
- .render(v, cx)
+ .render(cx)
.contained()
.with_style(style.container)
.constrained();
@@ -185,15 +308,15 @@ pub mod action_button {
}
})
.with_cursor_style(CursorStyle::PointingHand)
- .with_dynamic_tooltip(
- self.tag,
- 0,
- self.tooltip,
- Some(self.action),
- self.tooltip_style,
- cx,
- )
- .into_any()
+ .into_any();
+
+ if let Some((tooltip, style)) = self.tooltip {
+ button = button
+ .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx)
+ .into_any()
+ }
+
+ button
}
}
}
@@ -202,7 +325,7 @@ pub mod svg {
use std::borrow::Cow;
use gpui::{
- elements::{GeneralComponent, StyleableComponent},
+ elements::{Component, Empty, SafeStylable},
Element,
};
use schemars::JsonSchema;
@@ -225,6 +348,7 @@ pub mod svg {
pub enum IconSize {
IconSize { icon_size: f32 },
Dimensions { width: f32, height: f32 },
+ IconDimensions { icon_width: f32, icon_height: f32 },
}
#[derive(Deserialize)]
@@ -248,6 +372,14 @@ pub mod svg {
icon_height: height,
color,
},
+ IconSize::IconDimensions {
+ icon_width,
+ icon_height,
+ } => SvgStyle {
+ icon_width,
+ icon_height,
+ color,
+ },
};
Ok(result)
@@ -255,20 +387,27 @@ pub mod svg {
}
pub struct Svg<S> {
- path: Cow<'static, str>,
+ path: Option<Cow<'static, str>>,
style: S,
}
impl Svg<()> {
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
Self {
- path: path.into(),
+ path: Some(path.into()),
+ style: (),
+ }
+ }
+
+ pub fn optional(path: Option<impl Into<Cow<'static, str>>>) -> Self {
+ Self {
+ path: path.map(Into::into),
style: (),
}
}
}
- impl StyleableComponent for Svg<()> {
+ impl SafeStylable for Svg<()> {
type Style = SvgStyle;
type Output = Svg<SvgStyle>;
@@ -281,18 +420,19 @@ pub mod svg {
}
}
- impl GeneralComponent for Svg<SvgStyle> {
- fn render<V: gpui::View>(
- self,
- _: &mut V,
- _: &mut gpui::ViewContext<V>,
- ) -> gpui::AnyElement<V> {
- gpui::elements::Svg::new(self.path)
- .with_color(self.style.color)
- .constrained()
- .with_width(self.style.icon_width)
- .with_height(self.style.icon_height)
- .into_any()
+ impl Component for Svg<SvgStyle> {
+ fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+ if let Some(path) = self.path {
+ gpui::elements::Svg::new(path)
+ .with_color(self.style.color)
+ .constrained()
+ } else {
+ Empty::new().constrained()
+ }
+ .constrained()
+ .with_width(self.style.icon_width)
+ .with_height(self.style.icon_height)
+ .into_any()
}
}
}
@@ -301,7 +441,8 @@ pub mod label {
use std::borrow::Cow;
use gpui::{
- elements::{GeneralComponent, LabelStyle, StyleableComponent},
+ elements::{Component, LabelStyle, SafeStylable},
+ fonts::TextStyle,
Element,
};
@@ -319,25 +460,21 @@ pub mod label {
}
}
- impl StyleableComponent for Label<()> {
- type Style = LabelStyle;
+ impl SafeStylable for Label<()> {
+ type Style = TextStyle;
type Output = Label<LabelStyle>;
fn with_style(self, style: Self::Style) -> Self::Output {
Label {
text: self.text,
- style,
+ style: style.into(),
}
}
}
- impl GeneralComponent for Label<LabelStyle> {
- fn render<V: gpui::View>(
- self,
- _: &mut V,
- _: &mut gpui::ViewContext<V>,
- ) -> gpui::AnyElement<V> {
+ impl Component for Label<LabelStyle> {
+ fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
gpui::elements::Label::new(self.text, self.style).into_any()
}
}
@@ -3,7 +3,7 @@ mod theme_registry;
mod theme_settings;
pub mod ui;
-use components::ToggleIconButtonStyle;
+use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle};
use gpui::{
color::Color,
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@@ -14,7 +14,7 @@ use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use settings::SettingsStore;
-use std::{collections::HashMap, sync::Arc};
+use std::{collections::HashMap, ops::Deref, sync::Arc};
use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
pub use theme_registry::*;
@@ -66,6 +66,7 @@ pub struct Theme {
pub feedback: FeedbackStyle,
pub welcome: WelcomeStyle,
pub titlebar: Titlebar,
+ pub component_test: ComponentTest,
}
#[derive(Deserialize, Default, Clone, JsonSchema)]
@@ -221,6 +222,7 @@ pub struct CopilotAuthAuthorized {
pub struct CollabPanel {
#[serde(flatten)]
pub container: ContainerStyle,
+ pub disclosure: DisclosureStyle<()>,
pub list_empty_state: Toggleable<Interactive<ContainedText>>,
pub list_empty_icon: Icon,
pub list_empty_label_container: ContainerStyle,
@@ -259,6 +261,13 @@ pub struct CollabPanel {
pub face_overlap: f32,
}
+#[derive(Deserialize, Default, JsonSchema)]
+pub struct ComponentTest {
+ pub button: Interactive<ButtonStyle<TextStyle>>,
+ pub toggle: Toggleable<Interactive<ButtonStyle<TextStyle>>>,
+ pub disclosure: DisclosureStyle<TextStyle>,
+}
+
#[derive(Deserialize, Default, JsonSchema)]
pub struct TabbedModal {
pub tab_button: Toggleable<Interactive<ContainedText>>,
@@ -890,6 +899,14 @@ pub struct Interactive<T> {
pub disabled: Option<T>,
}
+impl<T> Deref for Interactive<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.default
+ }
+}
+
impl Interactive<()> {
pub fn new_blank() -> Self {
Self {
@@ -907,6 +924,14 @@ pub struct Toggleable<T> {
inactive: T,
}
+impl<T> Deref for Toggleable<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inactive
+ }
+}
+
impl Toggleable<()> {
pub fn new_blank() -> Self {
Self {
@@ -25,6 +25,7 @@ cli = { path = "../cli" }
collab_ui = { path = "../collab_ui" }
collections = { path = "../collections" }
command_palette = { path = "../command_palette" }
+component_test = { path = "../component_test" }
context_menu = { path = "../context_menu" }
client = { path = "../client" }
clock = { path = "../clock" }
@@ -166,6 +166,7 @@ fn main() {
terminal_view::init(cx);
copilot::init(http.clone(), node_runtime, cx);
ai::init(cx);
+ component_test::init(cx);
cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
@@ -44,10 +44,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO
}
const padding = {
- top: size === Button.size.Small ? 0 : 2,
- bottom: size === Button.size.Small ? 0 : 2,
- left: size === Button.size.Small ? 0 : 4,
- right: size === Button.size.Small ? 0 : 4,
+ top: size === Button.size.Small ? 2 : 2,
+ bottom: size === Button.size.Small ? 2 : 2,
+ left: size === Button.size.Small ? 2 : 4,
+ right: size === Button.size.Small ? 2 : 4,
}
return interactive({
@@ -55,10 +55,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO
corner_radius: 6,
padding: padding,
margin: m,
- icon_width: 14,
+ icon_width: 12,
icon_height: 14,
- button_width: 20,
- button_height: 16,
+ button_width: size === Button.size.Small ? 16 : 20,
+ button_height: 14,
},
state: {
default: {
@@ -12,7 +12,6 @@ import simple_message_notification from "./simple_message_notification"
import project_shared_notification from "./project_shared_notification"
import tooltip from "./tooltip"
import terminal from "./terminal"
-import contact_finder from "./contact_finder"
import collab_panel from "./collab_panel"
import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
import incoming_call_notification from "./incoming_call_notification"
@@ -22,6 +21,7 @@ import assistant from "./assistant"
import { titlebar } from "./titlebar"
import editor from "./editor"
import feedback from "./feedback"
+import component_test from "./component_test"
import { useTheme } from "../common"
export default function app(): any {
@@ -54,6 +54,7 @@ export default function app(): any {
tooltip: tooltip(),
terminal: terminal(),
assistant: assistant(),
- feedback: feedback()
+ feedback: feedback(),
+ component_test: component_test(),
}
}
@@ -14,6 +14,7 @@ import { indicator } from "../component/indicator"
export default function contacts_panel(): any {
const theme = useTheme()
+ const CHANNEL_SPACING = 4 as const
const NAME_MARGIN = 6 as const
const SPACING = 12 as const
const INDENT_SIZE = 8 as const
@@ -152,6 +153,10 @@ export default function contacts_panel(): any {
return {
...collab_modals(),
+ disclosure: {
+ button: icon_button({ variant: "ghost", size: "sm" }),
+ spacing: CHANNEL_SPACING,
+ },
log_in_button: interactive({
base: {
background: background(theme.middle),
@@ -194,7 +199,7 @@ export default function contacts_panel(): any {
add_channel_button: header_icon_button,
leave_call_button: header_icon_button,
row_height: ITEM_HEIGHT,
- channel_indent: INDENT_SIZE * 2,
+ channel_indent: INDENT_SIZE * 2 + 2,
section_icon_size: 14,
header_row: {
...text(layer, "sans", { size: "sm", weight: "bold" }),
@@ -264,7 +269,7 @@ export default function contacts_panel(): any {
channel_name: {
...text(layer, "sans", { size: "sm" }),
margin: {
- left: NAME_MARGIN,
+ left: CHANNEL_SPACING,
},
},
list_empty_label_container: {
@@ -0,0 +1,27 @@
+
+import { useTheme } from "../common"
+import { text_button } from "../component/text_button"
+import { icon_button } from "../component/icon_button"
+import { text } from "./components"
+import { toggleable } from "../element"
+
+export default function contacts_panel(): any {
+ const theme = useTheme()
+
+ return {
+ button: text_button({}),
+ toggle: toggleable({
+ base: text_button({}),
+ state: {
+ active: {
+ ...text_button({ color: "accent" })
+ }
+ }
+ }),
+ disclosure: {
+ ...text(theme.lowest, "sans", "base"),
+ button: icon_button({ variant: "ghost" }),
+ spacing: 4,
+ }
+ }
+}