mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-24 02:46:43 +00:00
Add disclosable component (#2868)
This PR adds a disclosable component, related wiring, and uses it to implement the collaboration panel's disclosure of subchannels. It also adds a component test page to make style development easier, and refactors components into v0.2, safe styles (as described in [TWAZ #16](https://zed.dev/blog/this-week-at-zed-16)) Release Notes: - N/A
This commit is contained in:
parent
95b0dab876
commit
cee6a2c4ce
20 changed files with 858 additions and 189 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -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,
|
||||
|
|
18
crates/component_test/Cargo.toml
Normal file
18
crates/component_test/Cargo.toml
Normal file
|
@ -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" }
|
121
crates/component_test/src/component_test.rs
Normal file
121
crates/component_test/src/component_test.rs
Normal file
|
@ -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,79 +1,81 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleableComponent for () {
|
||||
type Style = ();
|
||||
type Output = ();
|
||||
|
||||
fn with_style(self, _: Self::Style) -> Self::Output {
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Component<V: View> {
|
||||
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
||||
|
||||
fn element(self) -> ComponentAdapter<V, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
ComponentAdapter::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// StylableComponent -> GeneralComponent
|
||||
pub struct StylableComponentAdapter<C: Component<V>, V: View> {
|
||||
/// Allows converting an unstylable component into a stylable one
|
||||
/// by using `()` as the style type
|
||||
pub struct StylableAdapter<C: Component> {
|
||||
component: C,
|
||||
phantom: std::marker::PhantomData<V>,
|
||||
}
|
||||
|
||||
impl<C: Component<V>, V: View> StylableComponentAdapter<C, V> {
|
||||
impl<C: Component> StylableAdapter<C> {
|
||||
pub fn new(component: C) -> Self {
|
||||
Self {
|
||||
component,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
Self { component }
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapter<C, V> {
|
||||
impl<C: Component> SafeStylable for StylableAdapter<C> {
|
||||
type Style = ();
|
||||
|
||||
type Output = C;
|
||||
|
@ -83,13 +85,150 @@ impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapt
|
|||
}
|
||||
}
|
||||
|
||||
// Element -> Component
|
||||
pub struct ElementAdapter<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>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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: 'static> StatefulAdapter<C, V> {
|
||||
pub fn new(component: C) -> Self {
|
||||
Self {
|
||||
component,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
fn with_style(self, _: Self::Style) -> Self::Output {
|
||||
self.component
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct ButtonStyle<C> {
|
||||
#[serde(flatten)]
|
||||
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,
|
||||
}
|
||||
|
||||
pub struct Button<C, S> {
|
||||
action: Box<dyn Action>,
|
||||
tooltip: Cow<'static, str>,
|
||||
tooltip_style: TooltipStyle,
|
||||
tooltip: Option<(Cow<'static, str>, TooltipStyle)>,
|
||||
tag: TypeTag,
|
||||
id: usize,
|
||||
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>,
|
||||
#[serde(flatten)]
|
||||
contents: C,
|
||||
}
|
||||
|
||||
impl ActionButton<(), ()> {
|
||||
pub fn new_dynamic(
|
||||
action: Box<dyn Action>,
|
||||
tooltip: impl Into<Cow<'static, str>>,
|
||||
tooltip_style: TooltipStyle,
|
||||
) -> Self {
|
||||
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_contents<C: StyleableComponent>(self, contents: C) -> ActionButton<C, ()> {
|
||||
ActionButton {
|
||||
pub fn with_id(mut self, id: usize) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
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: {
|
||||
|
|
27
styles/src/style_tree/component_test.ts
Normal file
27
styles/src/style_tree/component_test.ts
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue