Compare commits
8 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
002cd36d87 | ||
![]() |
27216c0174 | ||
![]() |
098f5499bd | ||
![]() |
65861ce580 | ||
![]() |
cee6a2c4ce | ||
![]() |
95b0dab876 | ||
![]() |
d8fb9cb0f5 | ||
![]() |
0fd971325f |
41 changed files with 1369 additions and 753 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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![
|
||||||
|
|
|
@ -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,
|
||||||
|
|
18
crates/component_test/Cargo.toml
Normal file
18
crates/component_test/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "component_test"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/component_test.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
project = { path = "../project" }
|
121
crates/component_test/src/component_test.rs
Normal file
121
crates/component_test/src/component_test.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
elements::{Component, Flex, ParentElement, SafeStylable},
|
||||||
|
AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
|
};
|
||||||
|
use project::Project;
|
||||||
|
use theme::components::{action_button::Button, label::Label, ComponentExt};
|
||||||
|
use workspace::{
|
||||||
|
item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.add_action(ComponentTest::toggle_disclosure);
|
||||||
|
cx.add_action(ComponentTest::toggle_toggle);
|
||||||
|
cx.add_action(ComponentTest::deploy);
|
||||||
|
register_deserializable_item::<ComponentTest>(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
test,
|
||||||
|
[NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest]
|
||||||
|
);
|
||||||
|
|
||||||
|
struct ComponentTest {
|
||||||
|
disclosed: bool,
|
||||||
|
toggled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTest {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
disclosed: false,
|
||||||
|
toggled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext<Workspace>) {
|
||||||
|
workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext<Self>) {
|
||||||
|
self.disclosed = !self.disclosed;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext<Self>) {
|
||||||
|
self.toggled = !self.toggled;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ComponentTest {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ComponentTest {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"Component Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||||
|
let theme = theme::current(cx);
|
||||||
|
|
||||||
|
PaneBackdrop::new(
|
||||||
|
cx.view_id(),
|
||||||
|
Flex::column()
|
||||||
|
.with_spacing(10.)
|
||||||
|
.with_child(
|
||||||
|
Button::action(NoAction)
|
||||||
|
.with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
|
||||||
|
.with_contents(Label::new("Click me!"))
|
||||||
|
.with_style(theme.component_test.button.clone())
|
||||||
|
.element(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::action(ToggleToggle)
|
||||||
|
.with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
|
||||||
|
.with_contents(Label::new("Toggle me!"))
|
||||||
|
.toggleable(self.toggled)
|
||||||
|
.with_style(theme.component_test.toggle.clone())
|
||||||
|
.element(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new("A disclosure")
|
||||||
|
.disclosable(Some(self.disclosed), Box::new(ToggleDisclosure))
|
||||||
|
.with_style(theme.component_test.disclosure.clone())
|
||||||
|
.element(),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_width(200.)
|
||||||
|
.aligned()
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for ComponentTest {
|
||||||
|
fn tab_content<V: 'static>(
|
||||||
|
&self,
|
||||||
|
_: Option<usize>,
|
||||||
|
style: &theme::Tab,
|
||||||
|
_: &AppContext,
|
||||||
|
) -> gpui::AnyElement<V> {
|
||||||
|
gpui::elements::Label::new("Component test", style.label.clone()).into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialized_item_kind() -> Option<&'static str> {
|
||||||
|
Some("ComponentTest")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize(
|
||||||
|
_project: ModelHandle<Project>,
|
||||||
|
_workspace: WeakViewHandle<Workspace>,
|
||||||
|
_workspace_id: WorkspaceId,
|
||||||
|
_item_id: ItemId,
|
||||||
|
cx: &mut ViewContext<Pane>,
|
||||||
|
) -> Task<anyhow::Result<ViewHandle<Self>>> {
|
||||||
|
Task::ready(Ok(cx.add_view(|_| Self::new())))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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, |_| {});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
dev
|
stable
|
|
@ -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()))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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<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<Interactive<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),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
34
styles/src/component/margin.ts
Normal file
34
styles/src/component/margin.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
34
styles/src/component/padding.ts
Normal file
34
styles/src/component/padding.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
27
styles/src/style_tree/component_test.ts
Normal file
27
styles/src/style_tree/component_test.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
|
||||||
|
import { useTheme } from "../common"
|
||||||
|
import { text_button } from "../component/text_button"
|
||||||
|
import { icon_button } from "../component/icon_button"
|
||||||
|
import { text } from "./components"
|
||||||
|
import { toggleable } from "../element"
|
||||||
|
|
||||||
|
export default function contacts_panel(): any {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
return {
|
||||||
|
button: text_button({}),
|
||||||
|
toggle: toggleable({
|
||||||
|
base: text_button({}),
|
||||||
|
state: {
|
||||||
|
active: {
|
||||||
|
...text_button({ color: "accent" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
disclosure: {
|
||||||
|
...text(theme.lowest, "sans", "base"),
|
||||||
|
button: icon_button({ variant: "ghost" }),
|
||||||
|
spacing: 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue