Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
Joseph T. Lyons
002cd36d87 v0.101.x stable 2023-08-30 13:40:36 -04:00
Max Brunsfeld
27216c0174 Disable flaky integration test 2023-08-29 16:12:49 -07:00
Max Brunsfeld
098f5499bd zed 0.101.1 2023-08-29 14:37:05 -07:00
Max Brunsfeld
65861ce580 Search UI polish (#2904)
This PR polishes the search bar UI, making the layout more dense, and
the spacing more consistent with the rest of the app. I've also
re-ordered the toolbar items to reflect some of @iamnbutler's original
search designs. The items related to the search query are on the left,
and the actions that navigate the buffer (next, prev, select all, result
count) are on the right.
2023-08-29 14:35:56 -07:00
Mikayla Maki
cee6a2c4ce 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
2023-08-29 14:35:53 -07:00
Max Brunsfeld
95b0dab876 Debounce code action and document highlight requests (#2905)
Lately, I've been finding Rust-analyzer unusably slow when editing large
files (like `editor_tests.rs`, or `integration_tests.rs`). When I
profile the Rust-analyzer process, I see that it sometimes saturates up
to 10 cores processing a queue of code actions requests.

Additionally, sometimes when collaborating on large files like these, we
see long delays in propagating buffer operations. I'm still not sure why
this is happening, but whenever I look at the server logs in Datadog, I
see that there are remote `CodeActions` and `DocumentHighlights`
messages being processed that take upwards of 30 seconds. I think what
may be happening is that many such requests are resolving at once, and
the responses are taking up too much of the host's bandwidth.

I think that both of these problems are caused by us sending way too
many code action and document highlight requests to rust-analyzer. This
PR adds a simple debounce between changing selections and making these
requests.

From my local testing, this debounce makes Rust-analyzer *much* more
responsive when moving the cursor around a large file like
`editor_tests.rs`.
2023-08-29 09:32:01 -07:00
Max Brunsfeld
d8fb9cb0f5 Fix bugs in autoscroll with 'fit' strategy (#2893)
This fixes a bug where text moved up and down by one pixel in the buffer
search query editor, while typing.

Release  notes:
* Fixed a bug where editors didn't auto-scroll when typing if all
cursors could not fit within the viewport.
2023-08-28 08:48:16 -07:00
Joseph T. Lyons
0fd971325f v0.101.x preview 2023-08-23 12:48:47 -04:00
41 changed files with 1369 additions and 753 deletions

16
Cargo.lock generated
View file

@ -1556,6 +1556,19 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "component_test"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"project",
"settings",
"theme",
"util",
"workspace",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.2.0" version = "2.2.0"
@ -9632,7 +9645,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.101.0" version = "0.101.1"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"ai", "ai",
@ -9653,6 +9666,7 @@ dependencies = [
"collab_ui", "collab_ui",
"collections", "collections",
"command_palette", "command_palette",
"component_test",
"context_menu", "context_menu",
"copilot", "copilot",
"copilot_button", "copilot_button",

View file

@ -13,6 +13,7 @@ members = [
"crates/collab_ui", "crates/collab_ui",
"crates/collections", "crates/collections",
"crates/command_palette", "crates/command_palette",
"crates/component_test",
"crates/context_menu", "crates/context_menu",
"crates/copilot", "crates/copilot",
"crates/copilot_button", "crates/copilot_button",

View file

@ -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 { pub fn channel_count(&self) -> usize {
self.channel_paths.len() self.channel_paths.len()
} }

View file

@ -5320,7 +5320,7 @@ async fn test_collaborating_with_code_actions(
.unwrap(); .unwrap();
let mut fake_language_server = fake_language_servers.next().await.unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server let mut requests = fake_language_server
.handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move { .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!( assert_eq!(
params.text_document.uri, params.text_document.uri,
@ -5329,9 +5329,9 @@ async fn test_collaborating_with_code_actions(
assert_eq!(params.range.start, lsp::Position::new(0, 0)); assert_eq!(params.range.start, lsp::Position::new(0, 0));
assert_eq!(params.range.end, lsp::Position::new(0, 0)); assert_eq!(params.range.end, lsp::Position::new(0, 0));
Ok(None) Ok(None)
}) });
.next() deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
.await; requests.next().await;
// Move cursor to a location that contains code actions. // Move cursor to a location that contains code actions.
editor_b.update(cx_b, |editor, cx| { editor_b.update(cx_b, |editor, cx| {
@ -5341,7 +5341,7 @@ async fn test_collaborating_with_code_actions(
cx.focus(&editor_b); cx.focus(&editor_b);
}); });
fake_language_server let mut requests = fake_language_server
.handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move { .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!( assert_eq!(
params.text_document.uri, params.text_document.uri,
@ -5393,9 +5393,9 @@ async fn test_collaborating_with_code_actions(
..Default::default() ..Default::default()
}, },
)])) )]))
}) });
.next() deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
.await; requests.next().await;
// Toggle code actions and wait for them to display. // Toggle code actions and wait for them to display.
editor_b.update(cx_b, |editor, cx| { editor_b.update(cx_b, |editor, cx| {
@ -7798,7 +7798,7 @@ async fn test_on_input_format_from_guest_to_host(
}); });
} }
#[gpui::test] #[allow(unused)]
async fn test_mutual_editor_inlay_hint_cache_update( async fn test_mutual_editor_inlay_hint_cache_update(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext, cx_a: &mut TestAppContext,
@ -7863,6 +7863,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
client_a.language_registry().add(Arc::clone(&language)); client_a.language_registry().add(Arc::clone(&language));
client_b.language_registry().add(language); client_b.language_registry().add(language);
// Client A opens a project.
client_a client_a
.fs() .fs()
.insert_tree( .insert_tree(
@ -7883,6 +7884,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.await .await
.unwrap(); .unwrap();
// Client B joins the project
let project_b = client_b.build_remote_project(project_id, cx_b).await; let project_b = client_b.build_remote_project(project_id, cx_b).await;
active_call_b active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
@ -7892,6 +7894,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
cx_a.foreground().start_waiting(); cx_a.foreground().start_waiting();
// The host opens a rust file.
let _buffer_a = project_a let _buffer_a = project_a
.update(cx_a, |project, cx| { .update(cx_a, |project, cx| {
project.open_local_buffer("/a/main.rs", cx) project.open_local_buffer("/a/main.rs", cx)
@ -7899,7 +7902,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.await .await
.unwrap(); .unwrap();
let fake_language_server = fake_language_servers.next().await.unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap();
let next_call_id = Arc::new(AtomicU32::new(0));
let editor_a = workspace_a let editor_a = workspace_a
.update(cx_a, |workspace, cx| { .update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx) workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@ -7908,6 +7910,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.unwrap() .unwrap()
.downcast::<Editor>() .downcast::<Editor>()
.unwrap(); .unwrap();
// Set up the language server to return an additional inlay hint on each request.
let next_call_id = Arc::new(AtomicU32::new(0));
fake_language_server fake_language_server
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| { .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let task_next_call_id = Arc::clone(&next_call_id); let task_next_call_id = Arc::clone(&next_call_id);
@ -7916,33 +7921,28 @@ async fn test_mutual_editor_inlay_hint_cache_update(
params.text_document.uri, params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(), lsp::Url::from_file_path("/a/main.rs").unwrap(),
); );
let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst); let call_count = task_next_call_id.fetch_add(1, SeqCst);
let mut new_hints = Vec::with_capacity(current_call_id as usize); Ok(Some(
loop { (0..=call_count)
new_hints.push(lsp::InlayHint { .map(|ix| lsp::InlayHint {
position: lsp::Position::new(0, current_call_id), position: lsp::Position::new(0, ix),
label: lsp::InlayHintLabel::String(current_call_id.to_string()), label: lsp::InlayHintLabel::String(ix.to_string()),
kind: None, kind: None,
text_edits: None, text_edits: None,
tooltip: None, tooltip: None,
padding_left: None, padding_left: None,
padding_right: None, padding_right: None,
data: None, data: None,
}); })
if current_call_id == 0 { .collect(),
break; ))
}
current_call_id -= 1;
}
Ok(Some(new_hints))
} }
}) })
.next() .next()
.await .await
.unwrap(); .unwrap();
cx_a.foreground().finish_waiting(); deterministic.run_until_parked();
cx_a.foreground().run_until_parked();
let mut edits_made = 1; let mut edits_made = 1;
editor_a.update(cx_a, |editor, _| { editor_a.update(cx_a, |editor, _| {
@ -7968,7 +7968,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.downcast::<Editor>() .downcast::<Editor>()
.unwrap(); .unwrap();
cx_b.foreground().run_until_parked(); deterministic.run_until_parked();
editor_b.update(cx_b, |editor, _| { editor_b.update(cx_b, |editor, _| {
assert_eq!( assert_eq!(
vec!["0".to_string(), "1".to_string()], vec!["0".to_string(), "1".to_string()],
@ -7989,18 +7989,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
cx.focus(&editor_b); cx.focus(&editor_b);
edits_made += 1; edits_made += 1;
}); });
cx_a.foreground().run_until_parked();
cx_b.foreground().run_until_parked(); deterministic.run_until_parked();
editor_a.update(cx_a, |editor, _| { editor_a.update(cx_a, |editor, _| {
assert_eq!(
vec!["0".to_string(), "1".to_string(), "2".to_string()],
extract_hint_labels(editor),
"Host should get hints from the 1st edit and 1st LSP query"
);
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version(), edits_made);
});
editor_b.update(cx_b, |editor, _| {
assert_eq!( assert_eq!(
vec![ vec![
"0".to_string(), "0".to_string(),
@ -8014,6 +8005,15 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version(), edits_made); assert_eq!(inlay_cache.version(), edits_made);
}); });
editor_b.update(cx_b, |editor, _| {
assert_eq!(
vec!["0".to_string(), "1".to_string(), "2".to_string(),],
extract_hint_labels(editor),
"Guest should get hints the 1st edit and 2nd LSP query"
);
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version(), edits_made);
});
editor_a.update(cx_a, |editor, cx| { editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13])); editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
@ -8021,8 +8021,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
cx.focus(&editor_a); cx.focus(&editor_a);
edits_made += 1; edits_made += 1;
}); });
cx_a.foreground().run_until_parked();
cx_b.foreground().run_until_parked(); deterministic.run_until_parked();
editor_a.update(cx_a, |editor, _| { editor_a.update(cx_a, |editor, _| {
assert_eq!( assert_eq!(
vec![ vec![
@ -8061,8 +8061,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.await .await
.expect("inlay refresh request failed"); .expect("inlay refresh request failed");
edits_made += 1; edits_made += 1;
cx_a.foreground().run_until_parked();
cx_b.foreground().run_until_parked(); deterministic.run_until_parked();
editor_a.update(cx_a, |editor, _| { editor_a.update(cx_a, |editor, _| {
assert_eq!( assert_eq!(
vec![ vec![

View file

@ -16,8 +16,9 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
actions, actions,
elements::{ elements::{
Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
Stack, Svg,
}, },
geometry::{ geometry::{
rect::RectF, rect::RectF,
@ -35,7 +36,7 @@ use serde_derive::{Deserialize, Serialize};
use settings::SettingsStore; use settings::SettingsStore;
use staff_mode::StaffMode; use staff_mode::StaffMode;
use std::{borrow::Cow, mem, sync::Arc}; use std::{borrow::Cow, mem, sync::Arc};
use theme::IconButton; use theme::{components::ComponentExt, IconButton};
use util::{iife, ResultExt, TryFutureExt}; use util::{iife, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel}, dock::{DockPosition, Panel},
@ -53,6 +54,11 @@ struct RemoveChannel {
channel_id: u64, channel_id: u64,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct ToggleCollapse {
channel_id: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct NewChannel { struct NewChannel {
channel_id: u64, channel_id: u64,
@ -73,7 +79,16 @@ struct RenameChannel {
channel_id: u64, channel_id: u64,
} }
actions!(collab_panel, [ToggleFocus, Remove, Secondary]); actions!(
collab_panel,
[
ToggleFocus,
Remove,
Secondary,
CollapseSelectedChannel,
ExpandSelectedChannel
]
);
impl_actions!( impl_actions!(
collab_panel, collab_panel,
@ -82,7 +97,8 @@ impl_actions!(
NewChannel, NewChannel,
InviteMembers, InviteMembers,
ManageMembers, 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::manage_members);
cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_selected_channel);
cx.add_action(CollabPanel::rename_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)] #[derive(Debug)]
@ -147,6 +166,7 @@ pub struct CollabPanel {
list_state: ListState<Self>, list_state: ListState<Self>,
subscriptions: Vec<Subscription>, subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>, collapsed_sections: Vec<Section>,
collapsed_channels: Vec<ChannelId>,
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
context_menu_on_selected: bool, context_menu_on_selected: bool,
} }
@ -398,6 +418,7 @@ impl CollabPanel {
subscriptions: Vec::default(), subscriptions: Vec::default(),
match_candidates: Vec::default(), match_candidates: Vec::default(),
collapsed_sections: vec![Section::Offline], collapsed_sections: vec![Section::Offline],
collapsed_channels: Vec::default(),
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
client: workspace.app_state().client.clone(), client: workspace.app_state().client.clone(),
context_menu_on_selected: true, context_menu_on_selected: true,
@ -657,10 +678,24 @@ impl CollabPanel {
self.entries.push(ListEntry::ChannelEditor { depth: 0 }); self.entries.push(ListEntry::ChannelEditor { depth: 0 });
} }
} }
let mut collapse_depth = None;
for mat in matches { for mat in matches {
let (depth, channel) = let (depth, channel) =
channel_store.channel_at_index(mat.candidate_id).unwrap(); 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 { match &self.channel_editing_state {
Some(ChannelEditingState::Create { parent_id, .. }) Some(ChannelEditingState::Create { parent_id, .. })
if *parent_id == Some(channel.id) => if *parent_id == Some(channel.id) =>
@ -1332,7 +1367,7 @@ impl CollabPanel {
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
if can_collapse { if can_collapse {
this.toggle_expanded(section, cx); this.toggle_section_expanded(section, cx);
} }
}) })
} }
@ -1479,6 +1514,11 @@ impl CollabPanel {
cx: &AppContext, cx: &AppContext,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
Flex::row() Flex::row()
.with_child(
Empty::new()
.constrained()
.with_width(theme.collab_panel.disclosure.button_space()),
)
.with_child( .with_child(
Svg::new("icons/hash.svg") Svg::new("icons/hash.svg")
.with_color(theme.collab_panel.channel_hash.color) .with_color(theme.collab_panel.channel_hash.color)
@ -1537,6 +1577,10 @@ impl CollabPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
let channel_id = channel.id; 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 is_active = iife!({
let call_channel = ActiveCall::global(cx) let call_channel = ActiveCall::global(cx)
.read(cx) .read(cx)
@ -1550,7 +1594,7 @@ impl CollabPanel {
const FACEPILE_LIMIT: usize = 3; const FACEPILE_LIMIT: usize = 3;
MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| { MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
Flex::row() Flex::<Self>::row()
.with_child( .with_child(
Svg::new("icons/hash.svg") Svg::new("icons/hash.svg")
.with_color(theme.channel_hash.color) .with_color(theme.channel_hash.color)
@ -1599,6 +1643,11 @@ impl CollabPanel {
} }
}) })
.align_children_center() .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() .constrained()
.with_height(theme.row_height) .with_height(theme.row_height)
.contained() .contained()
@ -1825,6 +1874,12 @@ impl CollabPanel {
OverlayPositionMode::Window OverlayPositionMode::Window
}); });
let expand_action_name = if self.is_channel_collapsed(channel_id) {
"Expand Subchannels"
} else {
"Collapse Subchannels"
};
context_menu.show( context_menu.show(
position.unwrap_or_default(), position.unwrap_or_default(),
if self.context_menu_on_selected { if self.context_menu_on_selected {
@ -1833,6 +1888,7 @@ impl CollabPanel {
gpui::elements::AnchorCorner::BottomLeft gpui::elements::AnchorCorner::BottomLeft
}, },
vec![ vec![
ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
ContextMenuItem::Separator, ContextMenuItem::Separator,
ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }),
@ -1912,7 +1968,7 @@ impl CollabPanel {
| Section::Online | Section::Online
| Section::Offline | Section::Offline
| Section::ChannelInvites => { | Section::ChannelInvites => {
self.toggle_expanded(*section, cx); self.toggle_section_expanded(*section, cx);
} }
}, },
ListEntry::Contact { contact, calling } => { 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) { if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
self.collapsed_sections.remove(ix); self.collapsed_sections.remove(ix);
} else { } else {
@ -2009,6 +2065,54 @@ impl CollabPanel {
self.update_entries(false, cx); 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>) { fn leave_call(cx: &mut ViewContext<Self>) {
ActiveCall::global(cx) ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(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>) { 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 { self.channel_editing_state = Some(ChannelEditingState::Create {
parent_id: Some(action.channel_id), parent_id: Some(action.channel_id),
pending_name: None, pending_name: None,

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

View 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())))
}
}

View file

@ -108,6 +108,8 @@ const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024; const MAX_SELECTION_HISTORY_LEN: usize = 1024;
const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
@ -3248,7 +3250,7 @@ impl Editor {
} }
fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> { fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
let project = self.project.as_ref()?; let project = self.project.clone()?;
let buffer = self.buffer.read(cx); let buffer = self.buffer.read(cx);
let newest_selection = self.selections.newest_anchor().clone(); let newest_selection = self.selections.newest_anchor().clone();
let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?;
@ -3257,11 +3259,15 @@ impl Editor {
return None; return None;
} }
let actions = project.update(cx, |project, cx| {
project.code_actions(&start_buffer, start..end, cx)
});
self.code_actions_task = Some(cx.spawn(|this, mut cx| async move { self.code_actions_task = Some(cx.spawn(|this, mut cx| async move {
let actions = actions.await; cx.background().timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT).await;
let actions = project
.update(&mut cx, |project, cx| {
project.code_actions(&start_buffer, start..end, cx)
})
.await;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.available_code_actions = actions.log_err().and_then(|actions| { this.available_code_actions = actions.log_err().and_then(|actions| {
if actions.is_empty() { if actions.is_empty() {
@ -3282,7 +3288,7 @@ impl Editor {
return None; return None;
} }
let project = self.project.as_ref()?; let project = self.project.clone()?;
let buffer = self.buffer.read(cx); let buffer = self.buffer.read(cx);
let newest_selection = self.selections.newest_anchor().clone(); let newest_selection = self.selections.newest_anchor().clone();
let cursor_position = newest_selection.head(); let cursor_position = newest_selection.head();
@ -3293,12 +3299,19 @@ impl Editor {
return None; return None;
} }
let highlights = project.update(cx, |project, cx| {
project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
});
self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move { self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move {
if let Some(highlights) = highlights.await.log_err() { cx.background()
.timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT)
.await;
let highlights = project
.update(&mut cx, |project, cx| {
project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
})
.await
.log_err();
if let Some(highlights) = highlights {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
if this.pending_rename.is_some() { if this.pending_rename.is_some() {
return; return;

View file

@ -1434,6 +1434,74 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let line_height = cx.update_editor(|editor, cx| {
editor.set_vertical_scroll_margin(2, cx);
editor.style(cx).text.line_height(cx.font_cache())
});
let window = cx.window;
window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx);
cx.set_state(
&r#"ˇone
two
three
four
five
six
seven
eight
nine
ten
"#,
);
cx.update_editor(|editor, cx| {
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0));
});
// Add a cursor below the visible area. Since both cursors cannot fit
// on screen, the editor autoscrolls to reveal the newest cursor, and
// allows the vertical scroll margin below that cursor.
cx.update_editor(|editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
selections.select_ranges([
Point::new(0, 0)..Point::new(0, 0),
Point::new(6, 0)..Point::new(6, 0),
]);
})
});
cx.update_editor(|editor, cx| {
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
});
// Move down. The editor cursor scrolls down to track the newest cursor.
cx.update_editor(|editor, cx| {
editor.move_down(&Default::default(), cx);
});
cx.update_editor(|editor, cx| {
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0));
});
// Add a cursor above the visible area. Since both cursors fit on screen,
// the editor scrolls to show both.
cx.update_editor(|editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
selections.select_ranges([
Point::new(1, 0)..Point::new(1, 0),
Point::new(6, 0)..Point::new(6, 0),
]);
})
});
cx.update_editor(|editor, cx| {
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0));
});
}
#[gpui::test] #[gpui::test]
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});

View file

@ -2079,14 +2079,11 @@ impl Element<Editor> for EditorElement {
scroll_height scroll_height
.min(constraint.max_along(Axis::Vertical)) .min(constraint.max_along(Axis::Vertical))
.max(constraint.min_along(Axis::Vertical)) .max(constraint.min_along(Axis::Vertical))
.max(line_height)
.min(line_height * max_lines as f32), .min(line_height * max_lines as f32),
) )
} else if let EditorMode::SingleLine = snapshot.mode { } else if let EditorMode::SingleLine = snapshot.mode {
size.set_y( size.set_y(line_height.max(constraint.min_along(Axis::Vertical)))
line_height
.min(constraint.max_along(Axis::Vertical))
.max(constraint.min_along(Axis::Vertical)),
)
} else if size.y().is_infinite() { } else if size.y().is_infinite() {
size.set_y(scroll_height); size.set_y(scroll_height);
} }

View file

@ -65,47 +65,52 @@ impl Editor {
self.set_scroll_position(scroll_position, cx); self.set_scroll_position(scroll_position, cx);
} }
let (autoscroll, local) = let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() { return false;
autoscroll };
} else {
return false;
};
let first_cursor_top; let mut target_top;
let last_cursor_bottom; let mut target_bottom;
if let Some(highlighted_rows) = &self.highlighted_rows { if let Some(highlighted_rows) = &self.highlighted_rows {
first_cursor_top = highlighted_rows.start as f32; target_top = highlighted_rows.start as f32;
last_cursor_bottom = first_cursor_top + 1.; target_bottom = target_top + 1.;
} else if autoscroll == Autoscroll::newest() {
let newest_selection = self.selections.newest::<Point>(cx);
first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
last_cursor_bottom = first_cursor_top + 1.;
} else { } else {
let selections = self.selections.all::<Point>(cx); let selections = self.selections.all::<Point>(cx);
first_cursor_top = selections target_top = selections
.first() .first()
.unwrap() .unwrap()
.head() .head()
.to_display_point(&display_map) .to_display_point(&display_map)
.row() as f32; .row() as f32;
last_cursor_bottom = selections target_bottom = selections
.last() .last()
.unwrap() .unwrap()
.head() .head()
.to_display_point(&display_map) .to_display_point(&display_map)
.row() as f32 .row() as f32
+ 1.0; + 1.0;
// If the selections can't all fit on screen, scroll to the newest.
if autoscroll == Autoscroll::newest()
|| autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines
{
let newest_selection_top = selections
.iter()
.max_by_key(|s| s.id)
.unwrap()
.head()
.to_display_point(&display_map)
.row() as f32;
target_top = newest_selection_top;
target_bottom = newest_selection_top + 1.;
}
} }
let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
0. 0.
} else { } else {
((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() ((visible_lines - (target_bottom - target_top)) / 2.0).floor()
}; };
if margin < 0.0 {
return false;
}
let strategy = match autoscroll { let strategy = match autoscroll {
Autoscroll::Strategy(strategy) => strategy, Autoscroll::Strategy(strategy) => strategy,
@ -113,8 +118,8 @@ impl Editor {
let last_autoscroll = &self.scroll_manager.last_autoscroll; let last_autoscroll = &self.scroll_manager.last_autoscroll;
if let Some(last_autoscroll) = last_autoscroll { if let Some(last_autoscroll) = last_autoscroll {
if self.scroll_manager.anchor.offset == last_autoscroll.0 if self.scroll_manager.anchor.offset == last_autoscroll.0
&& first_cursor_top == last_autoscroll.1 && target_top == last_autoscroll.1
&& last_cursor_bottom == last_autoscroll.2 && target_bottom == last_autoscroll.2
{ {
last_autoscroll.3.next() last_autoscroll.3.next()
} else { } else {
@ -129,37 +134,41 @@ impl Editor {
match strategy { match strategy {
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
let margin = margin.min(self.scroll_manager.vertical_scroll_margin); let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
let target_top = (first_cursor_top - margin).max(0.0); let target_top = (target_top - margin).max(0.0);
let target_bottom = last_cursor_bottom + margin; let target_bottom = target_bottom + margin;
let start_row = scroll_position.y(); let start_row = scroll_position.y();
let end_row = start_row + visible_lines; let end_row = start_row + visible_lines;
if target_top < start_row { let needs_scroll_up = target_top < start_row;
let needs_scroll_down = target_bottom >= end_row;
if needs_scroll_up && !needs_scroll_down {
scroll_position.set_y(target_top); scroll_position.set_y(target_top);
self.set_scroll_position_internal(scroll_position, local, true, cx); self.set_scroll_position_internal(scroll_position, local, true, cx);
} else if target_bottom >= end_row { }
if !needs_scroll_up && needs_scroll_down {
scroll_position.set_y(target_bottom - visible_lines); scroll_position.set_y(target_bottom - visible_lines);
self.set_scroll_position_internal(scroll_position, local, true, cx); self.set_scroll_position_internal(scroll_position, local, true, cx);
} }
} }
AutoscrollStrategy::Center => { AutoscrollStrategy::Center => {
scroll_position.set_y((first_cursor_top - margin).max(0.0)); scroll_position.set_y((target_top - margin).max(0.0));
self.set_scroll_position_internal(scroll_position, local, true, cx); self.set_scroll_position_internal(scroll_position, local, true, cx);
} }
AutoscrollStrategy::Top => { AutoscrollStrategy::Top => {
scroll_position.set_y((first_cursor_top).max(0.0)); scroll_position.set_y((target_top).max(0.0));
self.set_scroll_position_internal(scroll_position, local, true, cx); self.set_scroll_position_internal(scroll_position, local, true, cx);
} }
AutoscrollStrategy::Bottom => { AutoscrollStrategy::Bottom => {
scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0)); scroll_position.set_y((target_bottom - visible_lines).max(0.0));
self.set_scroll_position_internal(scroll_position, local, true, cx); self.set_scroll_position_internal(scroll_position, local, true, cx);
} }
} }
self.scroll_manager.last_autoscroll = Some(( self.scroll_manager.last_autoscroll = Some((
self.scroll_manager.anchor.offset, self.scroll_manager.anchor.offset,
first_cursor_top, target_top,
last_cursor_bottom, target_bottom,
strategy, strategy,
)); ));

View file

@ -2,7 +2,7 @@ use button_component::Button;
use gpui::{ use gpui::{
color::Color, color::Color,
elements::{Component, ContainerStyle, Flex, Label, ParentElement}, elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent},
fonts::{self, TextStyle}, fonts::{self, TextStyle},
platform::WindowOptions, platform::WindowOptions,
AnyElement, App, Element, Entity, View, ViewContext, AnyElement, App, Element, Entity, View, ViewContext,
@ -114,7 +114,7 @@ mod theme {
// Component creation: // Component creation:
mod toggleable_button { mod toggleable_button {
use gpui::{ use gpui::{
elements::{Component, ContainerStyle, LabelStyle}, elements::{ContainerStyle, LabelStyle, StatefulComponent},
scene::MouseClick, scene::MouseClick,
EventContext, View, 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> { fn render(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
let button = if let Some(style) = self.style { let button = if let Some(style) = self.style {
self.button.with_style(*style.style_for(self.active)) self.button.with_style(*style.style_for(self.active))
@ -171,7 +171,7 @@ mod toggleable_button {
mod button_component { mod button_component {
use gpui::{ use gpui::{
elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler}, elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent},
platform::MouseButton, platform::MouseButton,
scene::MouseClick, scene::MouseClick,
AnyElement, Element, EventContext, TypeTag, View, ViewContext, 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> { fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
let click_handler = self.click_handler; let click_handler = self.click_handler;

View file

@ -234,6 +234,27 @@ pub trait Element<V: 'static>: 'static {
{ {
MouseEventHandler::for_child::<Tag>(self.into_any(), region_id) 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> { trait AnyElementState<V> {

View file

@ -1,79 +1,81 @@
use std::marker::PhantomData; use std::{any::Any, marker::PhantomData};
use pathfinder_geometry::{rect::RectF, vector::Vector2F}; use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use crate::{ use crate::{
AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
ViewContext,
}; };
use super::Empty; use super::Empty;
pub trait GeneralComponent { /// The core stateless component trait, simply rendering an element tree
fn render<V: View>(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>; pub trait Component {
fn element<V: View>(self) -> ComponentAdapter<V, Self> fn render<V: 'static>(self, cx: &mut ViewContext<V>) -> AnyElement<V>;
fn element<V: 'static>(self) -> ComponentAdapter<V, Self>
where where
Self: Sized, Self: Sized,
{ {
ComponentAdapter::new(self) 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 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; fn with_style(self, style: Self::Style) -> Self::Output;
} }
impl GeneralComponent for () { /// All stylable components can trivially implement SafeStylable
fn render<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> { impl<C: Stylable> SafeStylable for C {
Empty::new().into_any() type Style = C::Style;
type Output = C;
fn with_style(self, style: Self::Style) -> Self::Output {
self.with_style(style)
} }
} }
impl StyleableComponent for () { /// Allows converting an unstylable component into a stylable one
type Style = (); /// by using `()` as the style type
type Output = (); pub struct StylableAdapter<C: Component> {
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> {
component: C, 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 { pub fn new(component: C) -> Self {
Self { Self { component }
component,
phantom: std::marker::PhantomData,
}
} }
} }
impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapter<C, V> { impl<C: Component> SafeStylable for StylableAdapter<C> {
type Style = (); type Style = ();
type Output = C; type Output = C;
@ -83,13 +85,150 @@ impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapt
} }
} }
// Element -> Component /// This is a secondary trait for components that can be styled
pub struct ElementAdapter<V: View> { /// 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>, element: AnyElement<V>,
_phantom: std::marker::PhantomData<V>, _phantom: std::marker::PhantomData<V>,
} }
impl<V: View> ElementAdapter<V> { impl<V: 'static> StatefulElementAdapter<V> {
pub fn new(element: AnyElement<V>) -> Self { pub fn new(element: AnyElement<V>) -> Self {
Self { Self {
element, 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> { fn render(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
self.element self.element
} }
} }
// Component -> Element /// A convenient shorthand for creating an empty component.
pub struct ComponentAdapter<V: View, E> { 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>, component: Option<E>,
element: Option<AnyElement<V>>, element: Option<AnyElement<V>>,
phantom: PhantomData<V>, phantom: PhantomData<V>,
} }
impl<E, V: View> ComponentAdapter<V, E> { impl<E, V: 'static> ComponentAdapter<V, E> {
pub fn new(e: E) -> Self { pub fn new(e: E) -> Self {
Self { Self {
component: Some(e), 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 LayoutState = ();
type PaintState = (); type PaintState = ();
@ -184,6 +338,7 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
) -> serde_json::Value { ) -> serde_json::Value {
serde_json::json!({ serde_json::json!({
"type": "ComponentAdapter", "type": "ComponentAdapter",
"component": std::any::type_name::<C>(),
"child": self.element.as_ref().map(|el| el.debug(view, cx)), "child": self.element.as_ref().map(|el| el.debug(view, cx)),
}) })
} }

View file

@ -44,6 +44,14 @@ impl ContainerStyle {
..Default::default() ..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> { pub struct Container<V> {

View file

@ -22,6 +22,7 @@ pub struct Flex<V> {
children: Vec<AnyElement<V>>, children: Vec<AnyElement<V>>,
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>, scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
child_alignment: f32, child_alignment: f32,
spacing: f32,
} }
impl<V: 'static> Flex<V> { impl<V: 'static> Flex<V> {
@ -31,6 +32,7 @@ impl<V: 'static> Flex<V> {
children: Default::default(), children: Default::default(),
scroll_state: None, scroll_state: None,
child_alignment: -1., child_alignment: -1.,
spacing: 0.,
} }
} }
@ -51,6 +53,11 @@ impl<V: 'static> Flex<V> {
self self
} }
pub fn with_spacing(mut self, spacing: f32) -> Self {
self.spacing = spacing;
self
}
pub fn scrollable<Tag>( pub fn scrollable<Tag>(
mut self, mut self,
element_id: usize, element_id: usize,
@ -81,7 +88,7 @@ impl<V: 'static> Flex<V> {
cx: &mut LayoutContext<V>, cx: &mut LayoutContext<V>,
) { ) {
let cross_axis = self.axis.invert(); let cross_axis = self.axis.invert();
for child in &mut self.children { for child in self.children.iter_mut() {
if let Some(metadata) = child.metadata::<FlexParentData>() { if let Some(metadata) = child.metadata::<FlexParentData>() {
if let Some((flex, expanded)) = metadata.flex { if let Some((flex, expanded)) = metadata.flex {
if expanded != layout_expanded { if expanded != layout_expanded {
@ -132,12 +139,12 @@ impl<V: 'static> Element<V> for Flex<V> {
cx: &mut LayoutContext<V>, cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) { ) -> (Vector2F, Self::LayoutState) {
let mut total_flex = None; let mut total_flex = None;
let mut fixed_space = 0.0; let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing;
let mut contains_float = false; let mut contains_float = false;
let cross_axis = self.axis.invert(); let cross_axis = self.axis.invert();
let mut cross_axis_max: f32 = 0.0; let mut cross_axis_max: f32 = 0.0;
for child in &mut self.children { for child in self.children.iter_mut() {
let metadata = child.metadata::<FlexParentData>(); let metadata = child.metadata::<FlexParentData>();
contains_float |= metadata.map_or(false, |metadata| metadata.float); contains_float |= metadata.map_or(false, |metadata| metadata.float);
@ -315,7 +322,7 @@ impl<V: 'static> Element<V> for Flex<V> {
} }
} }
for child in &mut self.children { for child in self.children.iter_mut() {
if remaining_space > 0. { if remaining_space > 0. {
if let Some(metadata) = child.metadata::<FlexParentData>() { if let Some(metadata) = child.metadata::<FlexParentData>() {
if metadata.float { if metadata.float {
@ -354,8 +361,8 @@ impl<V: 'static> Element<V> for Flex<V> {
child.paint(scene, aligned_child_origin, visible_bounds, view, cx); child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
match self.axis { match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing),
} }
} }

View file

@ -150,9 +150,10 @@ impl ToolbarItemView for QuickActionBar {
cx.notify(); cx.notify();
} }
})); }));
ToolbarItemLocation::PrimaryRight { flex: None }
} else {
ToolbarItemLocation::Hidden
} }
ToolbarItemLocation::PrimaryRight { flex: None }
} }
None => { None => {
self.active_item = None; self.active_item = None;

View file

@ -171,12 +171,12 @@ impl Peer {
let this = self.clone(); let this = self.clone();
let response_channels = connection_state.response_channels.clone(); let response_channels = connection_state.response_channels.clone();
let handle_io = async move { let handle_io = async move {
tracing::debug!(%connection_id, "handle io future: start"); tracing::trace!(%connection_id, "handle io future: start");
let _end_connection = util::defer(|| { let _end_connection = util::defer(|| {
response_channels.lock().take(); response_channels.lock().take();
this.connections.write().remove(&connection_id); this.connections.write().remove(&connection_id);
tracing::debug!(%connection_id, "handle io future: end"); tracing::trace!(%connection_id, "handle io future: end");
}); });
// Send messages on this frequency so the connection isn't closed. // Send messages on this frequency so the connection isn't closed.
@ -188,68 +188,68 @@ impl Peer {
futures::pin_mut!(receive_timeout); futures::pin_mut!(receive_timeout);
loop { loop {
tracing::debug!(%connection_id, "outer loop iteration start"); tracing::trace!(%connection_id, "outer loop iteration start");
let read_message = reader.read().fuse(); let read_message = reader.read().fuse();
futures::pin_mut!(read_message); futures::pin_mut!(read_message);
loop { loop {
tracing::debug!(%connection_id, "inner loop iteration start"); tracing::trace!(%connection_id, "inner loop iteration start");
futures::select_biased! { futures::select_biased! {
outgoing = outgoing_rx.next().fuse() => match outgoing { outgoing = outgoing_rx.next().fuse() => match outgoing {
Some(outgoing) => { Some(outgoing) => {
tracing::debug!(%connection_id, "outgoing rpc message: writing"); tracing::trace!(%connection_id, "outgoing rpc message: writing");
futures::select_biased! { futures::select_biased! {
result = writer.write(outgoing).fuse() => { result = writer.write(outgoing).fuse() => {
tracing::debug!(%connection_id, "outgoing rpc message: done writing"); tracing::trace!(%connection_id, "outgoing rpc message: done writing");
result.context("failed to write RPC message")?; result.context("failed to write RPC message")?;
tracing::debug!(%connection_id, "keepalive interval: resetting after sending message"); tracing::trace!(%connection_id, "keepalive interval: resetting after sending message");
keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse());
} }
_ = create_timer(WRITE_TIMEOUT).fuse() => { _ = create_timer(WRITE_TIMEOUT).fuse() => {
tracing::debug!(%connection_id, "outgoing rpc message: writing timed out"); tracing::trace!(%connection_id, "outgoing rpc message: writing timed out");
Err(anyhow!("timed out writing message"))?; Err(anyhow!("timed out writing message"))?;
} }
} }
} }
None => { None => {
tracing::debug!(%connection_id, "outgoing rpc message: channel closed"); tracing::trace!(%connection_id, "outgoing rpc message: channel closed");
return Ok(()) return Ok(())
}, },
}, },
_ = keepalive_timer => { _ = keepalive_timer => {
tracing::debug!(%connection_id, "keepalive interval: pinging"); tracing::trace!(%connection_id, "keepalive interval: pinging");
futures::select_biased! { futures::select_biased! {
result = writer.write(proto::Message::Ping).fuse() => { result = writer.write(proto::Message::Ping).fuse() => {
tracing::debug!(%connection_id, "keepalive interval: done pinging"); tracing::trace!(%connection_id, "keepalive interval: done pinging");
result.context("failed to send keepalive")?; result.context("failed to send keepalive")?;
tracing::debug!(%connection_id, "keepalive interval: resetting after pinging"); tracing::trace!(%connection_id, "keepalive interval: resetting after pinging");
keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse());
} }
_ = create_timer(WRITE_TIMEOUT).fuse() => { _ = create_timer(WRITE_TIMEOUT).fuse() => {
tracing::debug!(%connection_id, "keepalive interval: pinging timed out"); tracing::trace!(%connection_id, "keepalive interval: pinging timed out");
Err(anyhow!("timed out sending keepalive"))?; Err(anyhow!("timed out sending keepalive"))?;
} }
} }
} }
incoming = read_message => { incoming = read_message => {
let incoming = incoming.context("error reading rpc message from socket")?; let incoming = incoming.context("error reading rpc message from socket")?;
tracing::debug!(%connection_id, "incoming rpc message: received"); tracing::trace!(%connection_id, "incoming rpc message: received");
tracing::debug!(%connection_id, "receive timeout: resetting"); tracing::trace!(%connection_id, "receive timeout: resetting");
receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse()); receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse());
if let proto::Message::Envelope(incoming) = incoming { if let proto::Message::Envelope(incoming) = incoming {
tracing::debug!(%connection_id, "incoming rpc message: processing"); tracing::trace!(%connection_id, "incoming rpc message: processing");
futures::select_biased! { futures::select_biased! {
result = incoming_tx.send(incoming).fuse() => match result { result = incoming_tx.send(incoming).fuse() => match result {
Ok(_) => { Ok(_) => {
tracing::debug!(%connection_id, "incoming rpc message: processed"); tracing::trace!(%connection_id, "incoming rpc message: processed");
} }
Err(_) => { Err(_) => {
tracing::debug!(%connection_id, "incoming rpc message: channel closed"); tracing::trace!(%connection_id, "incoming rpc message: channel closed");
return Ok(()) return Ok(())
} }
}, },
_ = create_timer(WRITE_TIMEOUT).fuse() => { _ = create_timer(WRITE_TIMEOUT).fuse() => {
tracing::debug!(%connection_id, "incoming rpc message: processing timed out"); tracing::trace!(%connection_id, "incoming rpc message: processing timed out");
Err(anyhow!("timed out processing incoming message"))? Err(anyhow!("timed out processing incoming message"))?
} }
} }
@ -257,7 +257,7 @@ impl Peer {
break; break;
}, },
_ = receive_timeout => { _ = receive_timeout => {
tracing::debug!(%connection_id, "receive timeout: delay between messages too long"); tracing::trace!(%connection_id, "receive timeout: delay between messages too long");
Err(anyhow!("delay between messages too long"))? Err(anyhow!("delay between messages too long"))?
} }
} }
@ -274,13 +274,13 @@ impl Peer {
let response_channels = response_channels.clone(); let response_channels = response_channels.clone();
async move { async move {
let message_id = incoming.id; let message_id = incoming.id;
tracing::debug!(?incoming, "incoming message future: start"); tracing::trace!(?incoming, "incoming message future: start");
let _end = util::defer(move || { let _end = util::defer(move || {
tracing::debug!(%connection_id, message_id, "incoming message future: end"); tracing::trace!(%connection_id, message_id, "incoming message future: end");
}); });
if let Some(responding_to) = incoming.responding_to { if let Some(responding_to) = incoming.responding_to {
tracing::debug!( tracing::trace!(
%connection_id, %connection_id,
message_id, message_id,
responding_to, responding_to,
@ -290,7 +290,7 @@ impl Peer {
if let Some(tx) = channel { if let Some(tx) = channel {
let requester_resumed = oneshot::channel(); let requester_resumed = oneshot::channel();
if let Err(error) = tx.send((incoming, requester_resumed.0)) { if let Err(error) = tx.send((incoming, requester_resumed.0)) {
tracing::debug!( tracing::trace!(
%connection_id, %connection_id,
message_id, message_id,
responding_to = responding_to, responding_to = responding_to,
@ -299,14 +299,14 @@ impl Peer {
); );
} }
tracing::debug!( tracing::trace!(
%connection_id, %connection_id,
message_id, message_id,
responding_to, responding_to,
"incoming response: waiting to resume requester" "incoming response: waiting to resume requester"
); );
let _ = requester_resumed.1.await; let _ = requester_resumed.1.await;
tracing::debug!( tracing::trace!(
%connection_id, %connection_id,
message_id, message_id,
responding_to, responding_to,
@ -323,7 +323,7 @@ impl Peer {
None None
} else { } else {
tracing::debug!(%connection_id, message_id, "incoming message: received"); tracing::trace!(%connection_id, message_id, "incoming message: received");
proto::build_typed_envelope(connection_id, incoming).or_else(|| { proto::build_typed_envelope(connection_id, incoming).or_else(|| {
tracing::error!( tracing::error!(
%connection_id, %connection_id,

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
history::SearchHistory, history::SearchHistory,
mode::{next_mode, SearchMode}, mode::{next_mode, SearchMode, Side},
search_bar::{render_nav_button, render_search_mode_button}, search_bar::{render_nav_button, render_search_mode_button},
CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
@ -156,11 +156,12 @@ impl View for BufferSearchBar {
self.query_editor.update(cx, |editor, cx| { self.query_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(new_placeholder_text, cx); editor.set_placeholder_text(new_placeholder_text, cx);
}); });
let search_button_for_mode = |mode, cx: &mut ViewContext<BufferSearchBar>| { let search_button_for_mode = |mode, side, cx: &mut ViewContext<BufferSearchBar>| {
let is_active = self.current_mode == mode; let is_active = self.current_mode == mode;
render_search_mode_button( render_search_mode_button(
mode, mode,
side,
is_active, is_active,
move |_, this, cx| { move |_, this, cx| {
this.activate_search_mode(mode, cx); this.activate_search_mode(mode, cx);
@ -212,20 +213,11 @@ impl View for BufferSearchBar {
) )
}; };
let icon_style = theme.search.editor_icon.clone(); let query_column = Flex::row()
let nav_column = Flex::row()
.with_child(self.render_action_button("Select All", cx))
.with_child(nav_button_for_direction("<", Direction::Prev, cx))
.with_child(nav_button_for_direction(">", Direction::Next, cx))
.with_child(Flex::row().with_children(match_count))
.constrained()
.with_height(theme.search.search_bar_row_height);
let query = Flex::row()
.with_child( .with_child(
Svg::for_style(icon_style.icon) Svg::for_style(theme.search.editor_icon.clone().icon)
.contained() .contained()
.with_style(icon_style.container), .with_style(theme.search.editor_icon.clone().container),
) )
.with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) .with_child(ChildView::new(&self.query_editor, cx).flex(1., true))
.with_child( .with_child(
@ -244,49 +236,45 @@ impl View for BufferSearchBar {
.contained(), .contained(),
) )
.align_children_center() .align_children_center()
.flex(1., true);
let editor_column = Flex::row()
.with_child(
query
.contained()
.with_style(query_container_style)
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.with_height(theme.search.search_bar_row_height)
.flex(1., false),
)
.contained() .contained()
.with_style(query_container_style)
.constrained() .constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.with_height(theme.search.search_bar_row_height) .with_height(theme.search.search_bar_row_height)
.flex(1., false); .flex(1., false);
let mode_column = Flex::row() let mode_column = Flex::row()
.with_child( .with_child(search_button_for_mode(
Flex::row() SearchMode::Text,
.with_child(search_button_for_mode(SearchMode::Text, cx)) Some(Side::Left),
.with_child(search_button_for_mode(SearchMode::Regex, cx))
.contained()
.with_style(theme.search.modes_container),
)
.with_child(super::search_bar::render_close_button(
"Dismiss Buffer Search",
&theme.search,
cx, cx,
|_, this, cx| this.dismiss(&Default::default(), cx),
Some(Box::new(Dismiss)),
)) ))
.with_child(search_button_for_mode(
SearchMode::Regex,
Some(Side::Right),
cx,
))
.contained()
.with_style(theme.search.modes_container)
.constrained()
.with_height(theme.search.search_bar_row_height);
let nav_column = Flex::row()
.with_child(self.render_action_button("all", cx))
.with_child(Flex::row().with_children(match_count))
.with_child(nav_button_for_direction("<", Direction::Prev, cx))
.with_child(nav_button_for_direction(">", Direction::Next, cx))
.constrained() .constrained()
.with_height(theme.search.search_bar_row_height) .with_height(theme.search.search_bar_row_height)
.aligned()
.right()
.flex_float(); .flex_float();
Flex::row() Flex::row()
.with_child(editor_column) .with_child(query_column)
.with_child(nav_column)
.with_child(mode_column) .with_child(mode_column)
.with_child(nav_column)
.contained() .contained()
.with_style(theme.search.container) .with_style(theme.search.container)
.aligned()
.into_any_named("search bar") .into_any_named("search bar")
} }
} }
@ -340,8 +328,9 @@ impl ToolbarItemView for BufferSearchBar {
ToolbarItemLocation::Hidden ToolbarItemLocation::Hidden
} }
} }
fn row_count(&self, _: &ViewContext<Self>) -> usize { fn row_count(&self, _: &ViewContext<Self>) -> usize {
2 1
} }
} }

View file

@ -48,41 +48,18 @@ impl SearchMode {
SearchMode::Regex => Box::new(ActivateRegexMode), SearchMode::Regex => Box::new(ActivateRegexMode),
} }
} }
pub(crate) fn border_right(&self) -> bool {
match self {
SearchMode::Regex => true,
SearchMode::Text => true,
SearchMode::Semantic => true,
}
}
pub(crate) fn border_left(&self) -> bool {
match self {
SearchMode::Text => true,
_ => false,
}
}
pub(crate) fn button_side(&self) -> Option<Side> {
match self {
SearchMode::Text => Some(Side::Left),
SearchMode::Semantic => None,
SearchMode::Regex => Some(Side::Right),
}
}
} }
pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {
let next_text_state = if semantic_enabled {
SearchMode::Semantic
} else {
SearchMode::Regex
};
match mode { match mode {
SearchMode::Text => next_text_state, SearchMode::Text => SearchMode::Regex,
SearchMode::Semantic => SearchMode::Regex, SearchMode::Regex => {
SearchMode::Regex => SearchMode::Text, if semantic_enabled {
SearchMode::Semantic
} else {
SearchMode::Text
}
}
SearchMode::Semantic => SearchMode::Text,
} }
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
history::SearchHistory, history::SearchHistory,
mode::SearchMode, mode::{SearchMode, Side},
search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
@ -1420,8 +1420,13 @@ impl View for ProjectSearchBar {
}, },
cx, cx,
); );
let search = _search.read(cx); let search = _search.read(cx);
let is_semantic_available = SemanticIndex::enabled(cx);
let is_semantic_disabled = search.semantic_state.is_none(); let is_semantic_disabled = search.semantic_state.is_none();
let icon_style = theme.search.editor_icon.clone();
let is_active = search.active_match_index.is_some();
let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| { let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| {
crate::search_bar::render_option_button_icon( crate::search_bar::render_option_button_icon(
self.is_option_enabled(option, cx), self.is_option_enabled(option, cx),
@ -1447,28 +1452,23 @@ impl View for ProjectSearchBar {
render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx) render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx)
}); });
let search = _search.read(cx); let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
let icon_style = theme.search.editor_icon.clone(); let is_active = if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
// Editor Functionality search.current_mode == mode
let query = Flex::row() } else {
.with_child( false
Svg::for_style(icon_style.icon) };
.contained() render_search_mode_button(
.with_style(icon_style.container), mode,
side,
is_active,
move |_, this, cx| {
this.activate_search_mode(mode, cx);
},
cx,
) )
.with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) };
.with_child(
Flex::row()
.with_child(filter_button)
.with_children(case_sensitive)
.with_children(whole_word)
.flex(1., false)
.constrained()
.contained(),
)
.align_children_center()
.flex(1., true);
let search = _search.read(cx); let search = _search.read(cx);
@ -1486,50 +1486,6 @@ impl View for ProjectSearchBar {
theme.search.include_exclude_editor.input.container theme.search.include_exclude_editor.input.container
}; };
let included_files_view = ChildView::new(&search.included_files_editor, cx)
.contained()
.flex(1., true);
let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx)
.contained()
.flex(1., true);
let filters = search.filters_enabled.then(|| {
Flex::row()
.with_child(
included_files_view
.contained()
.with_style(include_container_style)
.constrained()
.with_height(theme.search.search_bar_row_height)
.with_min_width(theme.search.include_exclude_editor.min_width)
.with_max_width(theme.search.include_exclude_editor.max_width),
)
.with_child(
excluded_files_view
.contained()
.with_style(exclude_container_style)
.constrained()
.with_height(theme.search.search_bar_row_height)
.with_min_width(theme.search.include_exclude_editor.min_width)
.with_max_width(theme.search.include_exclude_editor.max_width),
)
.contained()
.with_padding_top(theme.workspace.toolbar.container.padding.bottom)
});
let editor_column = Flex::column()
.with_child(
query
.contained()
.with_style(query_container_style)
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.with_height(theme.search.search_bar_row_height)
.flex(1., false),
)
.with_children(filters)
.flex(1., false);
let matches = search.active_match_index.map(|match_ix| { let matches = search.active_match_index.map(|match_ix| {
Label::new( Label::new(
format!( format!(
@ -1544,25 +1500,81 @@ impl View for ProjectSearchBar {
.aligned() .aligned()
}); });
let search_button_for_mode = |mode, cx: &mut ViewContext<ProjectSearchBar>| { let query_column = Flex::column()
let is_active = if let Some(search) = self.active_project_search.as_ref() { .with_spacing(theme.search.search_row_spacing)
let search = search.read(cx); .with_child(
search.current_mode == mode Flex::row()
} else { .with_child(
false Svg::for_style(icon_style.icon)
}; .contained()
render_search_mode_button( .with_style(icon_style.container),
mode, )
is_active, .with_child(ChildView::new(&search.query_editor, cx).flex(1., true))
move |_, this, cx| { .with_child(
this.activate_search_mode(mode, cx); Flex::row()
}, .with_child(filter_button)
cx, .with_children(case_sensitive)
.with_children(whole_word)
.flex(1., false)
.constrained()
.contained(),
)
.align_children_center()
.contained()
.with_style(query_container_style)
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.with_height(theme.search.search_bar_row_height)
.flex(1., false),
) )
}; .with_children(search.filters_enabled.then(|| {
let is_active = search.active_match_index.is_some(); Flex::row()
let semantic_index = SemanticIndex::enabled(cx) .with_child(
.then(|| search_button_for_mode(SearchMode::Semantic, cx)); ChildView::new(&search.included_files_editor, cx)
.contained()
.with_style(include_container_style)
.constrained()
.with_height(theme.search.search_bar_row_height)
.flex(1., true),
)
.with_child(
ChildView::new(&search.excluded_files_editor, cx)
.contained()
.with_style(exclude_container_style)
.constrained()
.with_height(theme.search.search_bar_row_height)
.flex(1., true),
)
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false)
}))
.flex(1., false);
let mode_column =
Flex::row()
.with_child(search_button_for_mode(
SearchMode::Text,
Some(Side::Left),
cx,
))
.with_child(search_button_for_mode(
SearchMode::Regex,
if is_semantic_available {
None
} else {
Some(Side::Right)
},
cx,
))
.with_children(is_semantic_available.then(|| {
search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx)
}))
.contained()
.with_style(theme.search.modes_container);
let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| { let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
render_nav_button( render_nav_button(
label, label,
@ -1578,43 +1590,17 @@ impl View for ProjectSearchBar {
}; };
let nav_column = Flex::row() let nav_column = Flex::row()
.with_child(Flex::row().with_children(matches))
.with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction("<", Direction::Prev, cx))
.with_child(nav_button_for_direction(">", Direction::Next, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx))
.with_child(Flex::row().with_children(matches))
.constrained()
.with_height(theme.search.search_bar_row_height);
let mode_column = Flex::row()
.with_child(
Flex::row()
.with_child(search_button_for_mode(SearchMode::Text, cx))
.with_children(semantic_index)
.with_child(search_button_for_mode(SearchMode::Regex, cx))
.contained()
.with_style(theme.search.modes_container),
)
.with_child(super::search_bar::render_close_button(
"Dismiss Project Search",
&theme.search,
cx,
|_, this, cx| {
if let Some(search) = this.active_project_search.as_mut() {
search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss))
}
},
None,
))
.constrained() .constrained()
.with_height(theme.search.search_bar_row_height) .with_height(theme.search.search_bar_row_height)
.aligned()
.right()
.top()
.flex_float(); .flex_float();
Flex::row() Flex::row()
.with_child(editor_column) .with_child(query_column)
.with_child(nav_column)
.with_child(mode_column) .with_child(mode_column)
.with_child(nav_column)
.contained() .contained()
.with_style(theme.search.container) .with_style(theme.search.container)
.into_any_named("project search") .into_any_named("project search")
@ -1637,7 +1623,7 @@ impl ToolbarItemView for ProjectSearchBar {
self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
self.active_project_search = Some(search); self.active_project_search = Some(search);
ToolbarItemLocation::PrimaryLeft { ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)), flex: Some((1., true)),
} }
} else { } else {
ToolbarItemLocation::Hidden ToolbarItemLocation::Hidden
@ -1645,13 +1631,12 @@ impl ToolbarItemView for ProjectSearchBar {
} }
fn row_count(&self, cx: &ViewContext<Self>) -> usize { fn row_count(&self, cx: &ViewContext<Self>) -> usize {
self.active_project_search if let Some(search) = self.active_project_search.as_ref() {
.as_ref() if search.read(cx).filters_enabled {
.map(|search| { return 2;
let offset = search.read(cx).filters_enabled as usize; }
2 + offset }
}) 1
.unwrap_or_else(|| 2)
} }
} }

View file

@ -2,13 +2,13 @@ use bitflags::bitflags;
pub use buffer_search::BufferSearchBar; pub use buffer_search::BufferSearchBar;
use gpui::{ use gpui::{
actions, actions,
elements::{Component, StyleableComponent, TooltipStyle}, elements::{Component, SafeStylable, TooltipStyle},
Action, AnyElement, AppContext, Element, View, Action, AnyElement, AppContext, Element, View,
}; };
pub use mode::SearchMode; pub use mode::SearchMode;
use project::search::SearchQuery; use project::search::SearchQuery;
pub use project_search::{ProjectSearchBar, ProjectSearchView}; 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; pub mod buffer_search;
mod history; mod history;
@ -89,15 +89,12 @@ impl SearchOptions {
tooltip_style: TooltipStyle, tooltip_style: TooltipStyle,
button_style: ToggleIconButtonStyle, button_style: ToggleIconButtonStyle,
) -> AnyElement<V> { ) -> AnyElement<V> {
ActionButton::new_dynamic( Button::dynamic_action(self.to_toggle_action())
self.to_toggle_action(), .with_tooltip(format!("Toggle {}", self.label()), tooltip_style)
format!("Toggle {}", self.label()), .with_contents(Svg::new(self.icon()))
tooltip_style, .toggleable(active)
) .with_style(button_style)
.with_contents(theme::components::svg::Svg::new(self.icon())) .element()
.toggleable(active) .into_any()
.with_style(button_style)
.element()
.into_any()
} }
} }

View file

@ -13,34 +13,6 @@ use crate::{
SelectNextMatch, SelectPrevMatch, SelectNextMatch, SelectPrevMatch,
}; };
pub(super) fn render_close_button<V: View>(
tooltip: &'static str,
theme: &theme::Search,
cx: &mut ViewContext<V>,
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
dismiss_action: Option<Box<dyn Action>>,
) -> AnyElement<V> {
let tooltip_style = theme::current(cx).tooltip.clone();
enum CloseButton {}
MouseEventHandler::new::<CloseButton, _>(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.search_bar_row_height)
})
.on_click(MouseButton::Left, on_click)
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<CloseButton>(0, tooltip.to_string(), dismiss_action, tooltip_style, cx)
.into_any()
}
pub(super) fn render_nav_button<V: View>( pub(super) fn render_nav_button<V: View>(
icon: &'static str, icon: &'static str,
direction: Direction, direction: Direction,
@ -111,6 +83,7 @@ pub(super) fn render_nav_button<V: View>(
pub(crate) fn render_search_mode_button<V: View>( pub(crate) fn render_search_mode_button<V: View>(
mode: SearchMode, mode: SearchMode,
side: Option<Side>,
is_active: bool, is_active: bool,
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static, on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
@ -119,41 +92,41 @@ pub(crate) fn render_search_mode_button<V: View>(
enum SearchModeButton {} enum SearchModeButton {}
MouseEventHandler::new::<SearchModeButton, _>(mode.region_id(), cx, |state, cx| { MouseEventHandler::new::<SearchModeButton, _>(mode.region_id(), cx, |state, cx| {
let theme = theme::current(cx); let theme = theme::current(cx);
let mut style = theme let style = theme
.search .search
.mode_button .mode_button
.in_state(is_active) .in_state(is_active)
.style_for(state) .style_for(state)
.clone(); .clone();
style.container.border.left = mode.border_left();
style.container.border.right = mode.border_right();
let label = Label::new(mode.label(), style.text.clone()) let mut container_style = style.container;
.aligned() if let Some(button_side) = side {
.contained();
let mut container_style = style.container.clone();
if let Some(button_side) = mode.button_side() {
if button_side == Side::Left { if button_side == Side::Left {
container_style.border.left = true;
container_style.corner_radii = CornerRadii { container_style.corner_radii = CornerRadii {
bottom_right: 0., bottom_right: 0.,
top_right: 0., top_right: 0.,
..container_style.corner_radii ..container_style.corner_radii
}; };
label.with_style(container_style)
} else { } else {
container_style.border.left = false;
container_style.corner_radii = CornerRadii { container_style.corner_radii = CornerRadii {
bottom_left: 0., bottom_left: 0.,
top_left: 0., top_left: 0.,
..container_style.corner_radii ..container_style.corner_radii
}; };
label.with_style(container_style)
} }
} else { } else {
container_style.border.left = false;
container_style.corner_radii = CornerRadii::default(); container_style.corner_radii = CornerRadii::default();
label.with_style(container_style)
} }
.constrained()
.with_height(theme.search.search_bar_row_height) Label::new(mode.label(), style.text)
.aligned()
.contained()
.with_style(container_style)
.constrained()
.with_height(theme.search.search_bar_row_height)
}) })
.on_click(MouseButton::Left, on_click) .on_click(MouseButton::Left, on_click)
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)

View file

@ -1,23 +1,143 @@
use gpui::elements::StyleableComponent; use gpui::{elements::SafeStylable, Action};
use crate::{Interactive, Toggleable}; 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 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, ()> { fn toggleable(self, active: bool) -> Toggle<C, ()> {
Toggle::new(self, active) 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 { pub mod toggle {
use gpui::elements::{GeneralComponent, StyleableComponent}; use gpui::elements::{Component, SafeStylable};
use crate::Toggleable; use crate::Toggleable;
@ -27,7 +147,7 @@ pub mod toggle {
component: C, component: C,
} }
impl<C: StyleableComponent> Toggle<C, ()> { impl<C: SafeStylable> Toggle<C, ()> {
pub fn new(component: C, active: bool) -> Self { pub fn new(component: C, active: bool) -> Self {
Toggle { Toggle {
active, 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 Style = Toggleable<C::Style>;
type Output = Toggle<C, Self::Style>; type Output = Toggle<C, Self::Style>;
@ -51,15 +171,11 @@ pub mod toggle {
} }
} }
impl<C: StyleableComponent> GeneralComponent for Toggle<C, Toggleable<C::Style>> { impl<C: SafeStylable> Component for Toggle<C, Toggleable<C::Style>> {
fn render<V: gpui::View>( fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
self,
v: &mut V,
cx: &mut gpui::ViewContext<V>,
) -> gpui::AnyElement<V> {
self.component self.component
.with_style(self.style.in_state(self.active).clone()) .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 std::borrow::Cow;
use gpui::{ use gpui::{
elements::{ elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle},
ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle,
},
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
Action, Element, TypeTag, View, Action, Element, TypeTag,
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde_derive::Deserialize; use serde_derive::Deserialize;
use crate::Interactive; 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>, action: Box<dyn Action>,
tooltip: Cow<'static, str>, tooltip: Option<(Cow<'static, str>, TooltipStyle)>,
tooltip_style: TooltipStyle,
tag: TypeTag, tag: TypeTag,
id: usize,
contents: C, contents: C,
style: Interactive<S>, style: Interactive<S>,
} }
#[derive(Clone, Deserialize, Default, JsonSchema)] impl Button<(), ()> {
pub struct ButtonStyle<C> { pub fn dynamic_action(action: Box<dyn Action>) -> Button<(), ()> {
#[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 {
Self { Self {
contents: (), contents: (),
tag: action.type_tag(), tag: action.type_tag(),
style: Interactive::new_blank(),
tooltip: tooltip.into(),
tooltip_style,
action, action,
style: Interactive::new_blank(),
tooltip: None,
id: 0,
} }
} }
pub fn new<A: Action + Clone>( pub fn action<A: Action + Clone>(action: A) -> Self {
action: A, Self::dynamic_action(Box::new(action))
}
pub fn with_tooltip(
mut self,
tooltip: impl Into<Cow<'static, str>>, tooltip: impl Into<Cow<'static, str>>,
tooltip_style: TooltipStyle, tooltip_style: TooltipStyle,
) -> Self { ) -> 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, ()> { pub fn with_id(mut self, id: usize) -> Self {
ActionButton { self.id = id;
self
}
pub fn with_contents<C: SafeStylable>(self, contents: C) -> Button<C, ()> {
Button {
action: self.action, action: self.action,
tag: self.tag, tag: self.tag,
style: self.style, style: self.style,
tooltip: self.tooltip, tooltip: self.tooltip,
tooltip_style: self.tooltip_style, id: self.id,
contents, contents,
} }
} }
} }
impl<C: StyleableComponent> StyleableComponent for ActionButton<C, ()> { impl<C: SafeStylable> SafeStylable for Button<C, ()> {
type Style = Interactive<ButtonStyle<C::Style>>; 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 { fn with_style(self, style: Self::Style) -> Self::Output {
ActionButton { Button {
action: self.action, action: self.action,
tag: self.tag, tag: self.tag,
contents: self.contents, contents: self.contents,
tooltip: self.tooltip, tooltip: self.tooltip,
tooltip_style: self.tooltip_style, id: self.id,
style, style,
} }
} }
} }
impl<C: StyleableComponent> GeneralComponent for ActionButton<C, ButtonStyle<C::Style>> { impl<C: SafeStylable> Component for Button<C, ButtonStyle<C::Style>> {
fn render<V: View>(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> { fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| {
let style = self.style.style_for(state); let style = self.style.style_for(state);
let mut contents = self let mut contents = self
.contents .contents
.with_style(style.contents.to_owned()) .with_style(style.contents.to_owned())
.render(v, cx) .render(cx)
.contained() .contained()
.with_style(style.container) .with_style(style.container)
.constrained(); .constrained();
@ -185,15 +308,15 @@ pub mod action_button {
} }
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.with_dynamic_tooltip( .into_any();
self.tag,
0, if let Some((tooltip, style)) = self.tooltip {
self.tooltip, button = button
Some(self.action), .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx)
self.tooltip_style, .into_any()
cx, }
)
.into_any() button
} }
} }
} }
@ -202,7 +325,7 @@ pub mod svg {
use std::borrow::Cow; use std::borrow::Cow;
use gpui::{ use gpui::{
elements::{GeneralComponent, StyleableComponent}, elements::{Component, Empty, SafeStylable},
Element, Element,
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
@ -225,6 +348,7 @@ pub mod svg {
pub enum IconSize { pub enum IconSize {
IconSize { icon_size: f32 }, IconSize { icon_size: f32 },
Dimensions { width: f32, height: f32 }, Dimensions { width: f32, height: f32 },
IconDimensions { icon_width: f32, icon_height: f32 },
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -248,6 +372,14 @@ pub mod svg {
icon_height: height, icon_height: height,
color, color,
}, },
IconSize::IconDimensions {
icon_width,
icon_height,
} => SvgStyle {
icon_width,
icon_height,
color,
},
}; };
Ok(result) Ok(result)
@ -255,20 +387,27 @@ pub mod svg {
} }
pub struct Svg<S> { pub struct Svg<S> {
path: Cow<'static, str>, path: Option<Cow<'static, str>>,
style: S, style: S,
} }
impl Svg<()> { impl Svg<()> {
pub fn new(path: impl Into<Cow<'static, str>>) -> Self { pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
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: (), style: (),
} }
} }
} }
impl StyleableComponent for Svg<()> { impl SafeStylable for Svg<()> {
type Style = SvgStyle; type Style = SvgStyle;
type Output = Svg<SvgStyle>; type Output = Svg<SvgStyle>;
@ -281,18 +420,19 @@ pub mod svg {
} }
} }
impl GeneralComponent for Svg<SvgStyle> { impl Component for Svg<SvgStyle> {
fn render<V: gpui::View>( fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
self, if let Some(path) = self.path {
_: &mut V, gpui::elements::Svg::new(path)
_: &mut gpui::ViewContext<V>, .with_color(self.style.color)
) -> gpui::AnyElement<V> { .constrained()
gpui::elements::Svg::new(self.path) } else {
.with_color(self.style.color) Empty::new().constrained()
.constrained() }
.with_width(self.style.icon_width) .constrained()
.with_height(self.style.icon_height) .with_width(self.style.icon_width)
.into_any() .with_height(self.style.icon_height)
.into_any()
} }
} }
} }
@ -301,7 +441,8 @@ pub mod label {
use std::borrow::Cow; use std::borrow::Cow;
use gpui::{ use gpui::{
elements::{GeneralComponent, LabelStyle, StyleableComponent}, elements::{Component, LabelStyle, SafeStylable},
fonts::TextStyle,
Element, Element,
}; };
@ -319,25 +460,21 @@ pub mod label {
} }
} }
impl StyleableComponent for Label<()> { impl SafeStylable for Label<()> {
type Style = LabelStyle; type Style = TextStyle;
type Output = Label<LabelStyle>; type Output = Label<LabelStyle>;
fn with_style(self, style: Self::Style) -> Self::Output { fn with_style(self, style: Self::Style) -> Self::Output {
Label { Label {
text: self.text, text: self.text,
style, style: style.into(),
} }
} }
} }
impl GeneralComponent for Label<LabelStyle> { impl Component for Label<LabelStyle> {
fn render<V: gpui::View>( fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
self,
_: &mut V,
_: &mut gpui::ViewContext<V>,
) -> gpui::AnyElement<V> {
gpui::elements::Label::new(self.text, self.style).into_any() gpui::elements::Label::new(self.text, self.style).into_any()
} }
} }

View file

@ -3,7 +3,7 @@ mod theme_registry;
mod theme_settings; mod theme_settings;
pub mod ui; pub mod ui;
use components::ToggleIconButtonStyle; use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle};
use gpui::{ use gpui::{
color::Color, color::Color,
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@ -14,7 +14,7 @@ use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value; use serde_json::Value;
use settings::SettingsStore; use settings::SettingsStore;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, ops::Deref, sync::Arc};
use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle}; use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
pub use theme_registry::*; pub use theme_registry::*;
@ -66,6 +66,7 @@ pub struct Theme {
pub feedback: FeedbackStyle, pub feedback: FeedbackStyle,
pub welcome: WelcomeStyle, pub welcome: WelcomeStyle,
pub titlebar: Titlebar, pub titlebar: Titlebar,
pub component_test: ComponentTest,
} }
#[derive(Deserialize, Default, Clone, JsonSchema)] #[derive(Deserialize, Default, Clone, JsonSchema)]
@ -221,6 +222,7 @@ pub struct CopilotAuthAuthorized {
pub struct CollabPanel { pub struct CollabPanel {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub disclosure: DisclosureStyle<()>,
pub list_empty_state: Toggleable<Interactive<ContainedText>>, pub list_empty_state: Toggleable<Interactive<ContainedText>>,
pub list_empty_icon: Icon, pub list_empty_icon: Icon,
pub list_empty_label_container: ContainerStyle, pub list_empty_label_container: ContainerStyle,
@ -259,6 +261,13 @@ pub struct CollabPanel {
pub face_overlap: f32, 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)] #[derive(Deserialize, Default, JsonSchema)]
pub struct TabbedModal { pub struct TabbedModal {
pub tab_button: Toggleable<Interactive<ContainedText>>, pub tab_button: Toggleable<Interactive<ContainedText>>,
@ -428,11 +437,11 @@ pub struct Search {
pub match_index: ContainedText, pub match_index: ContainedText,
pub major_results_status: TextStyle, pub major_results_status: TextStyle,
pub minor_results_status: TextStyle, pub minor_results_status: TextStyle,
pub dismiss_button: Interactive<IconButton>,
pub editor_icon: IconStyle, pub editor_icon: IconStyle,
pub mode_button: Toggleable<Interactive<ContainedText>>, pub mode_button: Toggleable<Interactive<ContainedText>>,
pub nav_button: Toggleable<Interactive<ContainedLabel>>, pub nav_button: Toggleable<Interactive<ContainedLabel>>,
pub search_bar_row_height: f32, pub search_bar_row_height: f32,
pub search_row_spacing: f32,
pub option_button_height: f32, pub option_button_height: f32,
pub modes_container: ContainerStyle, pub modes_container: ContainerStyle,
} }
@ -890,6 +899,14 @@ pub struct Interactive<T> {
pub disabled: Option<T>, pub disabled: Option<T>,
} }
impl<T> Deref for Interactive<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.default
}
}
impl Interactive<()> { impl Interactive<()> {
pub fn new_blank() -> Self { pub fn new_blank() -> Self {
Self { Self {
@ -907,6 +924,14 @@ pub struct Toggleable<T> {
inactive: T, inactive: T,
} }
impl<T> Deref for Toggleable<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inactive
}
}
impl Toggleable<()> { impl Toggleable<()> {
pub fn new_blank() -> Self { pub fn new_blank() -> Self {
Self { Self {

View file

@ -81,10 +81,7 @@ impl View for Toolbar {
ToolbarItemLocation::PrimaryLeft { flex } => { ToolbarItemLocation::PrimaryLeft { flex } => {
primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
let left_item = ChildView::new(item.as_any(), cx) let left_item = ChildView::new(item.as_any(), cx).aligned();
.aligned()
.contained()
.with_margin_right(spacing);
if let Some((flex, expanded)) = flex { if let Some((flex, expanded)) = flex {
primary_left_items.push(left_item.flex(flex, expanded).into_any()); primary_left_items.push(left_item.flex(flex, expanded).into_any());
} else { } else {
@ -94,11 +91,7 @@ impl View for Toolbar {
ToolbarItemLocation::PrimaryRight { flex } => { ToolbarItemLocation::PrimaryRight { flex } => {
primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
let right_item = ChildView::new(item.as_any(), cx) let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float();
.aligned()
.contained()
.with_margin_left(spacing)
.flex_float();
if let Some((flex, expanded)) = flex { if let Some((flex, expanded)) = flex {
primary_right_items.push(right_item.flex(flex, expanded).into_any()); primary_right_items.push(right_item.flex(flex, expanded).into_any());
} else { } else {
@ -120,7 +113,7 @@ impl View for Toolbar {
let container_style = theme.container; let container_style = theme.container;
let height = theme.height * primary_items_row_count as f32; let height = theme.height * primary_items_row_count as f32;
let mut primary_items = Flex::row(); let mut primary_items = Flex::row().with_spacing(spacing);
primary_items.extend(primary_left_items); primary_items.extend(primary_left_items);
primary_items.extend(primary_right_items); primary_items.extend(primary_right_items);

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor." description = "The fast, collaborative code editor."
edition = "2021" edition = "2021"
name = "zed" name = "zed"
version = "0.101.0" version = "0.101.1"
publish = false publish = false
[lib] [lib]
@ -25,6 +25,7 @@ cli = { path = "../cli" }
collab_ui = { path = "../collab_ui" } collab_ui = { path = "../collab_ui" }
collections = { path = "../collections" } collections = { path = "../collections" }
command_palette = { path = "../command_palette" } command_palette = { path = "../command_palette" }
component_test = { path = "../component_test" }
context_menu = { path = "../context_menu" } context_menu = { path = "../context_menu" }
client = { path = "../client" } client = { path = "../client" }
clock = { path = "../clock" } clock = { path = "../clock" }

View file

@ -1 +1 @@
dev stable

View file

@ -166,6 +166,7 @@ fn main() {
terminal_view::init(cx); terminal_view::init(cx);
copilot::init(http.clone(), node_runtime, cx); copilot::init(http.clone(), node_runtime, cx);
ai::init(cx); ai::init(cx);
component_test::init(cx);
cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))

View file

@ -1706,6 +1706,8 @@ mod tests {
.remove_file(Path::new("/root/a/file2"), Default::default()) .remove_file(Path::new("/root/a/file2"), Default::default())
.await .await
.unwrap(); .unwrap();
cx.foreground().run_until_parked();
workspace workspace
.update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
.await .await

View file

@ -44,10 +44,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO
} }
const padding = { const padding = {
top: size === Button.size.Small ? 0 : 2, top: size === Button.size.Small ? 2 : 2,
bottom: size === Button.size.Small ? 0 : 2, bottom: size === Button.size.Small ? 2 : 2,
left: size === Button.size.Small ? 0 : 4, left: size === Button.size.Small ? 2 : 4,
right: size === Button.size.Small ? 0 : 4, right: size === Button.size.Small ? 2 : 4,
} }
return interactive({ return interactive({
@ -55,10 +55,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO
corner_radius: 6, corner_radius: 6,
padding: padding, padding: padding,
margin: m, margin: m,
icon_width: 14, icon_width: 12,
icon_height: 14, icon_height: 14,
button_width: 20, button_width: size === Button.size.Small ? 16 : 20,
button_height: 16, button_height: 14,
}, },
state: { state: {
default: { default: {

View file

@ -1,78 +0,0 @@
import { Interactive, interactive, toggleable, Toggleable } from "../element"
import { TextStyle, background, text } from "../style_tree/components"
import { useTheme } from "../theme"
import { Button } from "./button"
type LabelButtonStyle = {
corder_radius: number
background: string | null
padding: {
top: number
bottom: number
left: number
right: number
},
margin: Button.Options['margin']
button_height: number
} & TextStyle
/** Styles an Interactive&lt;ContainedText> */
export function label_button_style(
options: Partial<Button.Options> = {
variant: Button.variant.Default,
shape: Button.shape.Rectangle,
states: {
hovered: true,
pressed: true
}
}
): Interactive<LabelButtonStyle> {
const theme = useTheme()
const base = Button.button_base(options)
const layer = options.layer ?? theme.middle
const color = options.color ?? "base"
const default_state = {
...base,
...text(layer ?? theme.lowest, "sans", color),
font_size: Button.FONT_SIZE,
}
return interactive({
base: default_state,
state: {
hovered: {
background: background(layer, options.background ?? color, "hovered")
},
clicked: {
background: background(layer, options.background ?? color, "pressed")
}
}
})
}
/** Styles an Toggleable&lt;Interactive&lt;ContainedText>> */
export function toggle_label_button_style(
options: Partial<Button.ToggleableOptions> = {
variant: Button.variant.Default,
shape: Button.shape.Rectangle,
states: {
hovered: true,
pressed: true
}
}
): Toggleable<Interactive<LabelButtonStyle>> {
const activeOptions = {
...options,
color: options.active_color || options.color,
background: options.active_background || options.background
}
return toggleable({
state: {
inactive: label_button_style(options),
active: label_button_style(activeOptions),
},
})
}

View file

@ -0,0 +1,34 @@
type MarginOptions = {
all?: number
left?: number
right?: number
top?: number
bottom?: number
}
export type MarginStyle = {
top: number
bottom: number
left: number
right: number
}
export const margin_style = (options: MarginOptions): MarginStyle => {
const { all, top, bottom, left, right } = options
if (all !== undefined) return {
top: all,
bottom: all,
left: all,
right: all
}
if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Margin must have at least one value")
return {
top: top || 0,
bottom: bottom || 0,
left: left || 0,
right: right || 0
}
}

View file

@ -0,0 +1,34 @@
type PaddingOptions = {
all?: number
left?: number
right?: number
top?: number
bottom?: number
}
export type PaddingStyle = {
top: number
bottom: number
left: number
right: number
}
export const padding_style = (options: PaddingOptions): PaddingStyle => {
const { all, top, bottom, left, right } = options
if (all !== undefined) return {
top: all,
bottom: all,
left: all,
right: all
}
if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Padding must have at least one value")
return {
top: top || 0,
bottom: bottom || 0,
left: left || 0,
right: right || 0
}
}

View file

@ -17,6 +17,7 @@ interface TextButtonOptions {
variant?: Button.Variant variant?: Button.Variant
color?: keyof Theme["lowest"] color?: keyof Theme["lowest"]
margin?: Partial<Margin> margin?: Partial<Margin>
disabled?: boolean
text_properties?: TextProperties text_properties?: TextProperties
} }
@ -29,6 +30,7 @@ export function text_button({
color, color,
layer, layer,
margin, margin,
disabled,
text_properties, text_properties,
}: TextButtonOptions = {}) { }: TextButtonOptions = {}) {
const theme = useTheme() const theme = useTheme()
@ -65,13 +67,17 @@ export function text_button({
state: { state: {
default: { default: {
background: background_color, background: background_color,
color: foreground(layer ?? theme.lowest, color), color:
disabled
? foreground(layer ?? theme.lowest, "disabled")
: foreground(layer ?? theme.lowest, color),
}, },
hovered: { hovered:
background: background(layer ?? theme.lowest, color, "hovered"), disabled ? {} : {
color: foreground(layer ?? theme.lowest, color, "hovered"), background: background(layer ?? theme.lowest, color, "hovered"),
}, color: foreground(layer ?? theme.lowest, color, "hovered"),
clicked: { },
clicked: disabled ? {} : {
background: background(layer ?? theme.lowest, color, "pressed"), background: background(layer ?? theme.lowest, color, "pressed"),
color: foreground(layer ?? theme.lowest, color, "pressed"), color: foreground(layer ?? theme.lowest, color, "pressed"),
}, },

View file

@ -12,7 +12,6 @@ import simple_message_notification from "./simple_message_notification"
import project_shared_notification from "./project_shared_notification" import project_shared_notification from "./project_shared_notification"
import tooltip from "./tooltip" import tooltip from "./tooltip"
import terminal from "./terminal" import terminal from "./terminal"
import contact_finder from "./contact_finder"
import collab_panel from "./collab_panel" import collab_panel from "./collab_panel"
import toolbar_dropdown_menu from "./toolbar_dropdown_menu" import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
import incoming_call_notification from "./incoming_call_notification" import incoming_call_notification from "./incoming_call_notification"
@ -22,6 +21,7 @@ import assistant from "./assistant"
import { titlebar } from "./titlebar" import { titlebar } from "./titlebar"
import editor from "./editor" import editor from "./editor"
import feedback from "./feedback" import feedback from "./feedback"
import component_test from "./component_test"
import { useTheme } from "../common" import { useTheme } from "../common"
export default function app(): any { export default function app(): any {
@ -54,6 +54,7 @@ export default function app(): any {
tooltip: tooltip(), tooltip: tooltip(),
terminal: terminal(), terminal: terminal(),
assistant: assistant(), assistant: assistant(),
feedback: feedback() feedback: feedback(),
component_test: component_test(),
} }
} }

View file

@ -14,6 +14,7 @@ import { indicator } from "../component/indicator"
export default function contacts_panel(): any { export default function contacts_panel(): any {
const theme = useTheme() const theme = useTheme()
const CHANNEL_SPACING = 4 as const
const NAME_MARGIN = 6 as const const NAME_MARGIN = 6 as const
const SPACING = 12 as const const SPACING = 12 as const
const INDENT_SIZE = 8 as const const INDENT_SIZE = 8 as const
@ -152,6 +153,10 @@ export default function contacts_panel(): any {
return { return {
...collab_modals(), ...collab_modals(),
disclosure: {
button: icon_button({ variant: "ghost", size: "sm" }),
spacing: CHANNEL_SPACING,
},
log_in_button: interactive({ log_in_button: interactive({
base: { base: {
background: background(theme.middle), background: background(theme.middle),
@ -194,7 +199,7 @@ export default function contacts_panel(): any {
add_channel_button: header_icon_button, add_channel_button: header_icon_button,
leave_call_button: header_icon_button, leave_call_button: header_icon_button,
row_height: ITEM_HEIGHT, row_height: ITEM_HEIGHT,
channel_indent: INDENT_SIZE * 2, channel_indent: INDENT_SIZE * 2 + 2,
section_icon_size: 14, section_icon_size: 14,
header_row: { header_row: {
...text(layer, "sans", { size: "sm", weight: "bold" }), ...text(layer, "sans", { size: "sm", weight: "bold" }),
@ -264,7 +269,7 @@ export default function contacts_panel(): any {
channel_name: { channel_name: {
...text(layer, "sans", { size: "sm" }), ...text(layer, "sans", { size: "sm" }),
margin: { margin: {
left: NAME_MARGIN, left: CHANNEL_SPACING,
}, },
}, },
list_empty_label_container: { list_empty_label_container: {

View 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,
}
}
}

View file

@ -2,9 +2,23 @@ import { with_opacity } from "../theme/color"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme } from "../theme" import { useTheme } from "../theme"
import { text_button } from "../component/text_button"
const search_results = () => {
const theme = useTheme()
return {
// TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
match_background: with_opacity(
foreground(theme.highest, "accent"),
0.4
),
}
}
export default function search(): any { export default function search(): any {
const theme = useTheme() const theme = useTheme()
const SEARCH_ROW_SPACING = 12
// Search input // Search input
const editor = { const editor = {
@ -34,12 +48,8 @@ export default function search(): any {
} }
return { return {
padding: { top: 16, bottom: 16, left: 16, right: 16 }, padding: { top: 4, bottom: 4 },
// TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
match_background: with_opacity(
foreground(theme.highest, "accent"),
0.4
),
option_button: toggleable({ option_button: toggleable({
base: interactive({ base: interactive({
base: { base: {
@ -153,47 +163,13 @@ export default function search(): any {
}, },
}, },
}), }),
// Search tool buttons
// HACK: This is not how disabled elements should be created
// Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled
action_button: toggleable({ action_button: toggleable({
base: interactive({
base: {
...text(theme.highest, "mono", "disabled"),
background: background(theme.highest, "disabled"),
corner_radius: 6,
border: border(theme.highest, "disabled"),
padding: {
// bottom: 2,
left: 10,
right: 10,
// top: 2,
},
margin: {
right: 9,
}
},
state: {
hovered: {}
},
}),
state: { state: {
active: interactive({ inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }),
base: { active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } })
...text(theme.highest, "mono", "on"),
background: background(theme.highest, "on"),
border: border(theme.highest, "on"),
},
state: {
hovered: {
...text(theme.highest, "mono", "on", "hovered"),
background: background(theme.highest, "on", "hovered"),
border: border(theme.highest, "on", "hovered"),
},
clicked: {
...text(theme.highest, "mono", "on", "pressed"),
background: background(theme.highest, "on", "pressed"),
border: border(theme.highest, "on", "pressed"),
},
},
})
} }
}), }),
editor, editor,
@ -207,15 +183,15 @@ export default function search(): any {
border: border(theme.highest, "negative"), border: border(theme.highest, "negative"),
}, },
match_index: { match_index: {
...text(theme.highest, "mono", "variant"), ...text(theme.highest, "mono", { size: "sm" }),
padding: { padding: {
left: 9, right: SEARCH_ROW_SPACING,
}, },
}, },
option_button_group: { option_button_group: {
padding: { padding: {
left: 12, left: SEARCH_ROW_SPACING,
right: 12, right: SEARCH_ROW_SPACING,
}, },
}, },
include_exclude_inputs: { include_exclude_inputs: {
@ -232,52 +208,26 @@ export default function search(): any {
...text(theme.highest, "mono", "variant"), ...text(theme.highest, "mono", "variant"),
size: 13, size: 13,
}, },
dismiss_button: interactive({ // Input Icon
base: {
color: foreground(theme.highest, "variant"),
icon_width: 14,
button_width: 32,
corner_radius: 6,
padding: {
// // top: 10,
// bottom: 10,
left: 10,
right: 10,
},
background: background(theme.highest, "variant"),
border: border(theme.highest, "on"),
},
state: {
hovered: {
color: foreground(theme.highest, "hovered"),
background: background(theme.highest, "variant", "hovered")
},
clicked: {
color: foreground(theme.highest, "pressed"),
background: background(theme.highest, "variant", "pressed")
},
},
}),
editor_icon: { editor_icon: {
icon: { icon: {
color: foreground(theme.highest, "variant"), color: foreground(theme.highest, "disabled"),
asset: "icons/magnifying_glass_12.svg", asset: "icons/magnifying_glass.svg",
dimensions: { dimensions: {
width: 12, width: 14,
height: 12, height: 14,
} }
}, },
container: { container: {
margin: { right: 6 }, margin: { right: 4 },
padding: { left: 2, right: 2 }, padding: { left: 1, right: 1 },
} }
}, },
// Toggle group buttons - Text | Regex | Semantic
mode_button: toggleable({ mode_button: toggleable({
base: interactive({ base: interactive({
base: { base: {
...text(theme.highest, "mono", "variant"), ...text(theme.highest, "mono", "variant", { size: "sm" }),
background: background(theme.highest, "variant"), background: background(theme.highest, "variant"),
border: { border: {
@ -285,21 +235,24 @@ export default function search(): any {
left: false, left: false,
right: false right: false
}, },
margin: {
top: 1,
bottom: 1,
},
padding: { padding: {
left: 10, left: 12,
right: 10, right: 12,
}, },
corner_radius: 6, corner_radius: 6,
}, },
state: { state: {
hovered: { hovered: {
...text(theme.highest, "mono", "variant", "hovered"), ...text(theme.highest, "mono", "variant", "hovered", { size: "sm" }),
background: background(theme.highest, "variant", "hovered"), background: background(theme.highest, "variant", "hovered"),
border: border(theme.highest, "on", "hovered"), border: border(theme.highest, "on", "hovered"),
}, },
clicked: { clicked: {
...text(theme.highest, "mono", "variant", "pressed"), ...text(theme.highest, "mono", "variant", "pressed", { size: "sm" }),
background: background(theme.highest, "variant", "pressed"), background: background(theme.highest, "variant", "pressed"),
border: border(theme.highest, "on", "pressed"), border: border(theme.highest, "on", "pressed"),
}, },
@ -308,20 +261,23 @@ export default function search(): any {
state: { state: {
active: { active: {
default: { default: {
...text(theme.highest, "mono", "on"), ...text(theme.highest, "mono", "on", { size: "sm" }),
background: background(theme.highest, "on") background: background(theme.highest, "on")
}, },
hovered: { hovered: {
...text(theme.highest, "mono", "on", "hovered"), ...text(theme.highest, "mono", "on", "hovered", { size: "sm" }),
background: background(theme.highest, "on", "hovered") background: background(theme.highest, "on", "hovered")
}, },
clicked: { clicked: {
...text(theme.highest, "mono", "on", "pressed"), ...text(theme.highest, "mono", "on", "pressed", { size: "sm" }),
background: background(theme.highest, "on", "pressed") background: background(theme.highest, "on", "pressed")
}, },
}, },
}, },
}), }),
// Next/Previous Match buttons
// HACK: This is not how disabled elements should be created
// Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled
nav_button: toggleable({ nav_button: toggleable({
state: { state: {
inactive: interactive({ inactive: interactive({
@ -334,7 +290,10 @@ export default function search(): any {
left: false, left: false,
right: false, right: false,
}, },
margin: {
top: 1,
bottom: 1,
},
padding: { padding: {
left: 10, left: 10,
right: 10, right: 10,
@ -354,7 +313,10 @@ export default function search(): any {
left: false, left: false,
right: false, right: false,
}, },
margin: {
top: 1,
bottom: 1,
},
padding: { padding: {
left: 10, left: 10,
right: 10, right: 10,
@ -375,13 +337,10 @@ export default function search(): any {
}) })
} }
}), }),
search_bar_row_height: 32, search_bar_row_height: 34,
search_row_spacing: 8,
option_button_height: 22, option_button_height: 22,
modes_container: { modes_container: {},
margin: { ...search_results()
right: 9
}
}
} }
} }

View file

@ -129,7 +129,7 @@ export default function workspace(): any {
status_bar: statusBar(), status_bar: statusBar(),
titlebar: titlebar(), titlebar: titlebar(),
toolbar: { toolbar: {
height: 34, height: 42,
background: background(theme.highest), background: background(theme.highest),
border: border(theme.highest, { bottom: true }), border: border(theme.highest, { bottom: true }),
item_spacing: 8, item_spacing: 8,
@ -138,7 +138,7 @@ export default function workspace(): any {
variant: "ghost", variant: "ghost",
active_color: "accent", active_color: "accent",
}), }),
padding: { left: 8, right: 8, top: 4, bottom: 4 }, padding: { left: 8, right: 8 },
}, },
breadcrumb_height: 24, breadcrumb_height: 24,
breadcrumbs: interactive({ breadcrumbs: interactive({

View file

@ -21,8 +21,7 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"skipLibCheck": true, "skipLibCheck": true,
"useUnknownInCatchVariables": false, "useUnknownInCatchVariables": false
"baseUrl": "."
}, },
"exclude": [ "exclude": [
"node_modules" "node_modules"