chore: improve chat panel

This commit is contained in:
2025-02-19 09:08:29 +07:00
parent 50242981a5
commit 61fb90bd34
8 changed files with 211 additions and 134 deletions

61
Cargo.lock generated
View File

@@ -1093,7 +1093,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1375,18 +1375,18 @@ dependencies = [
[[package]] [[package]]
name = "ctor" name = "ctor"
version = "0.3.4" version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "035a5c3c87da8be7a0e5ebd6fbabcccd8651a59c27197096d1dfd74f312aaee5" checksum = "cdd538fd2dbf2e5932fad5a4f983ff0458891f5ea40973fa2b7d3460ca378914"
dependencies = [ dependencies = [
"ctor-proc-macro", "ctor-proc-macro",
] ]
[[package]] [[package]]
name = "ctor-proc-macro" name = "ctor-proc-macro"
version = "0.0.2" version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "107adb396b2d8e7766e79151083ce1cc624b51dfd1c23e0429c50226bba693eb" checksum = "c426d2ba3e525b39c1f0a9ba41b9fe61878dee11fa4e4a76b6ab440f46c5db5d"
[[package]] [[package]]
name = "data-encoding" name = "data-encoding"
@@ -1416,7 +1416,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2135,7 +2135,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2222,7 +2222,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2237,9 +2237,9 @@ checksum = "d196ffc1627db18a531359249b2bf8416178d84b729f3cebeb278f285fb9b58c"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.7" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -2445,7 +2445,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -3132,7 +3132,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.70.1", "bindgen 0.70.1",
@@ -3311,7 +3311,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#a269f7c937cb6c220023795b07686a5d7e337fb0" source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -3335,7 +3335,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#a269f7c937cb6c220023795b07686a5d7e337fb0" source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3347,7 +3347,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#a269f7c937cb6c220023795b07686a5d7e337fb0" source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"lru", "lru",
@@ -3358,7 +3358,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#a269f7c937cb6c220023795b07686a5d7e337fb0" source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"heed", "heed",
@@ -3371,7 +3371,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#a269f7c937cb6c220023795b07686a5d7e337fb0" source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3388,7 +3388,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#a269f7c937cb6c220023795b07686a5d7e337fb0" source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -4452,7 +4452,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
] ]
@@ -4581,7 +4581,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -4961,7 +4961,7 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -5285,7 +5285,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -5733,9 +5733,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.26.1" version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a" checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@@ -5892,17 +5892,16 @@ checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.26.1" version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24" checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [ dependencies = [
"byteorder",
"bytes", "bytes",
"data-encoding", "data-encoding",
"http", "http",
"httparse", "httparse",
"log", "log",
"rand 0.8.5", "rand 0.9.0",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"sha1", "sha1",
@@ -5918,9 +5917,9 @@ checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]] [[package]]
name = "uds_windows" name = "uds_windows"
@@ -6111,7 +6110,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#b4fc127e492924427b6f0afcad03b144b632152b" source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",

View File

@@ -49,22 +49,39 @@ pub fn init(
} }
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
struct ChatItem { struct ParsedMessage {
profile: NostrProfile, avatar: SharedString,
display_name: SharedString,
created_at: SharedString,
content: SharedString, content: SharedString,
ago: SharedString, }
impl ParsedMessage {
pub fn new(profile: &NostrProfile, content: &str, created_at: Timestamp) -> Self {
let avatar = profile.avatar().into();
let display_name = profile.name().into();
let content = SharedString::new(content);
let created_at = LastSeen(created_at).human_readable();
Self {
avatar,
display_name,
created_at,
content,
}
}
} }
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
enum Message { enum Message {
Item(Box<ChatItem>), User(Box<ParsedMessage>),
System(SharedString), System(SharedString),
Placeholder, Placeholder,
} }
impl Message { impl Message {
pub fn new(chat_message: ChatItem) -> Self { pub fn new(message: ParsedMessage) -> Self {
Self::Item(Box::new(chat_message)) Self::User(Box::new(message))
} }
pub fn system(content: SharedString) -> Self { pub fn system(content: SharedString) -> Self {
@@ -286,12 +303,7 @@ impl Chat {
let old_len = self.messages.read(cx).len(); let old_len = self.messages.read(cx).len();
let room = model.read(cx); let room = model.read(cx);
let ago = LastSeen(Timestamp::now()).human_readable(); let message = Message::new(ParsedMessage::new(&room.owner, &content, Timestamp::now()));
let message = Message::new(ChatItem {
profile: room.owner.clone(),
content: content.into(),
ago,
});
// Update message list // Update message list
cx.update_entity(&self.messages, |this, cx| { cx.update_entity(&self.messages, |this, cx| {
@@ -336,11 +348,10 @@ impl Chat {
room.owner.to_owned() room.owner.to_owned()
}; };
Some(Message::new(ChatItem { let message =
profile: member, Message::new(ParsedMessage::new(&member, &ev.content, ev.created_at));
content: ev.content.into(),
ago: LastSeen(ev.created_at).human_readable(), Some(message)
}))
} else { } else {
None None
} }
@@ -378,11 +389,11 @@ impl Chat {
.iter() .iter()
.filter_map(|event| { .filter_map(|event| {
if let Some(profile) = room.member(&event.pubkey) { if let Some(profile) = room.member(&event.pubkey) {
let message = Message::new(ChatItem { let message = Message::new(ParsedMessage::new(
profile, &profile,
content: event.content.clone().into(), &event.content,
ago: LastSeen(event.created_at).human_readable(), event.created_at,
}); ));
if !old_messages.iter().any(|old| old == &message) { if !old_messages.iter().any(|old| old == &message) {
Some(message) Some(message)
@@ -598,7 +609,7 @@ impl Chat {
.w_full() .w_full()
.p_2() .p_2()
.map(|this| match message { .map(|this| match message {
Message::Item(item) => this Message::User(item) => this
.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE))) .hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
.child( .child(
div() div()
@@ -613,7 +624,7 @@ impl Chat {
}), }),
) )
.child( .child(
img(item.profile.avatar()) img(item.avatar.clone())
.size_8() .size_8()
.rounded_full() .rounded_full()
.flex_shrink_0(), .flex_shrink_0(),
@@ -630,8 +641,10 @@ impl Chat {
.items_baseline() .items_baseline()
.gap_2() .gap_2()
.text_xs() .text_xs()
.child(div().font_semibold().child(item.profile.name())) .child(
.child(div().child(item.ago.clone()).text_color( div().font_semibold().child(item.display_name.clone()),
)
.child(div().child(item.created_at.clone()).text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN), cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)), )),
) )
@@ -668,7 +681,7 @@ impl Chat {
.text_center() .text_center()
.text_xs() .text_xs()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.line_height(relative(1.)) .line_height(relative(1.2))
.child( .child(
svg() svg()
.path("brand/coop.svg") .path("brand/coop.svg")

View File

@@ -1,5 +1,5 @@
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, AnyView, App, AppContext, Axis, Context, Element, Entity, div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Element, Entity,
InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window, Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window,
}; };
@@ -374,9 +374,7 @@ impl Render for Dock {
}) })
.map(|this| match &self.panel { .map(|this| match &self.panel {
DockItem::Split { view, .. } => this.child(view.clone()), DockItem::Split { view, .. } => this.child(view.clone()),
DockItem::Tabs { view, .. } => { DockItem::Tabs { view, .. } => this.child(view.clone()),
this.child(AnyView::from(view.clone()).cached(cache_style))
}
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)), DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
}) })
.child(self.render_resize_handle(window, cx)) .child(self.render_resize_handle(window, cx))
@@ -432,14 +430,20 @@ impl Element for DockElement {
_: &mut Self::RequestLayoutState, _: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState, _: &mut Self::PrepaintState,
window: &mut gpui::Window, window: &mut gpui::Window,
_: &mut App, cx: &mut App,
) { ) {
window.on_mouse_event({ window.on_mouse_event({
let view = self.view.clone(); let view = self.view.clone();
let is_resizing = view.read(cx).is_resizing;
move |e: &MouseMoveEvent, phase, window, cx| { move |e: &MouseMoveEvent, phase, window, cx| {
if phase.bubble() { if !is_resizing {
view.update(cx, |view, cx| view.resize(e.position, window, cx)) return;
} }
if !phase.bubble() {
return;
}
view.update(cx, |view, cx| view.resize(e.position, window, cx))
} }
}); });

View File

@@ -244,7 +244,7 @@ impl RenderOnce for Modal {
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.)), .with_easing(cubic_bezier(0.32, 0.72, 0., 1.)),
move |this, delta| { move |this, delta| {
let y_offset = px(0.) + delta * px(30.); let y_offset = px(0.) + delta * px(30.);
this.top(y + y_offset).opacity(delta) this.top(y + y_offset)
}, },
), ),
), ),

View File

@@ -133,7 +133,6 @@ impl PopupMenu {
this.dismiss(&Dismiss, window, cx) this.dismiss(&Dismiss, window, cx)
}), }),
]; ];
let menu = Self { let menu = Self {
focus_handle, focus_handle,
action_focus_handle: None, action_focus_handle: None,
@@ -150,7 +149,7 @@ impl PopupMenu {
scroll_state: Rc::new(Cell::new(ScrollbarState::default())), scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
subscriptions, subscriptions,
}; };
window.refresh();
f(menu, window, cx) f(menu, window, cx)
}) })
} }

View File

@@ -510,8 +510,11 @@ impl Element for ResizePanelGroupElement {
let axis = self.axis; let axis = self.axis;
let current_ix = view.read(cx).resizing_panel_ix; let current_ix = view.read(cx).resizing_panel_ix;
move |e: &MouseMoveEvent, phase, window, cx| { move |e: &MouseMoveEvent, phase, window, cx| {
if phase.bubble() { if !phase.bubble() {
if let Some(ix) = current_ix { return;
}
let Some(ix) = current_ix else { return };
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
let panel = view let panel = view
.panels .panels
@@ -520,31 +523,25 @@ impl Element for ResizePanelGroupElement {
.read(cx); .read(cx);
match axis { match axis {
Axis::Horizontal => view.resize_panels( Axis::Horizontal => {
ix, view.resize_panels(ix, e.position.x - panel.bounds.left(), window, cx)
e.position.x - panel.bounds.left(), }
window,
cx,
),
Axis::Vertical => { Axis::Vertical => {
view.resize_panels( view.resize_panels(ix, e.position.y - panel.bounds.top(), window, cx);
ix,
e.position.y - panel.bounds.top(),
window,
cx,
);
} }
} }
}) })
} }
}
}
}); });
// When any mouse up, stop dragging // When any mouse up, stop dragging
window.on_mouse_event({ window.on_mouse_event({
let view = self.view.clone(); let view = self.view.clone();
let current_ix = view.read(cx).resizing_panel_ix;
move |_: &MouseUpEvent, phase, window, cx| { move |_: &MouseUpEvent, phase, window, cx| {
if current_ix.is_none() {
return;
}
if phase.bubble() { if phase.bubble() {
view.update(cx, |view, cx| view.done_resizing(window, cx)); view.update(cx, |view, cx| view.done_resizing(window, cx));
} }

View File

@@ -1,6 +1,14 @@
use gpui::*; use gpui::{
fill, point, px, relative, App, Bounds, ContentMask, CursorStyle, Edges, Element, EntityId,
Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels,
Point, Position, ScrollHandle, ScrollWheelEvent, UniformListScrollHandle, Window,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{cell::Cell, rc::Rc, time::Instant}; use std::{
cell::Cell,
rc::Rc,
time::{Duration, Instant},
};
use crate::theme::{scale::ColorScaleStep, ActiveTheme}; use crate::theme::{scale::ColorScaleStep, ActiveTheme};
@@ -10,18 +18,24 @@ pub enum ScrollbarShow {
#[default] #[default]
Scrolling, Scrolling,
Hover, Hover,
Always,
} }
impl ScrollbarShow { impl ScrollbarShow {
fn is_hover(&self) -> bool { fn is_hover(&self) -> bool {
matches!(self, Self::Hover) matches!(self, Self::Hover)
} }
fn is_always(&self) -> bool {
matches!(self, Self::Always)
}
} }
const BORDER_WIDTH: Pixels = px(0.); const BORDER_WIDTH: Pixels = px(0.);
pub(crate) const WIDTH: Pixels = px(12.);
const MIN_THUMB_SIZE: f32 = 80.; const MIN_THUMB_SIZE: f32 = 80.;
const THUMB_RADIUS: Pixels = Pixels(3.0); const THUMB_RADIUS: Pixels = Pixels(4.0);
const THUMB_INSET: Pixels = Pixels(4.); const THUMB_INSET: Pixels = Pixels(3.);
const FADE_OUT_DURATION: f32 = 3.0; const FADE_OUT_DURATION: f32 = 3.0;
const FADE_OUT_DELAY: f32 = 2.0; const FADE_OUT_DELAY: f32 = 2.0;
@@ -65,6 +79,8 @@ pub struct ScrollbarState {
drag_pos: Point<Pixels>, drag_pos: Point<Pixels>,
last_scroll_offset: Point<Pixels>, last_scroll_offset: Point<Pixels>,
last_scroll_time: Option<Instant>, last_scroll_time: Option<Instant>,
// Last update offset
last_update: Instant,
} }
impl Default for ScrollbarState { impl Default for ScrollbarState {
@@ -76,6 +92,7 @@ impl Default for ScrollbarState {
drag_pos: point(px(0.), px(0.)), drag_pos: point(px(0.), px(0.)),
last_scroll_offset: point(px(0.), px(0.)), last_scroll_offset: point(px(0.), px(0.)),
last_scroll_time: None, last_scroll_time: None,
last_update: Instant::now(),
} }
} }
} }
@@ -106,8 +123,8 @@ impl ScrollbarState {
fn with_hovered(&self, axis: Option<ScrollbarAxis>) -> Self { fn with_hovered(&self, axis: Option<ScrollbarAxis>) -> Self {
let mut state = *self; let mut state = *self;
state.hovered_axis = axis; state.hovered_axis = axis;
if self.is_scrollbar_visible() { if axis.is_some() {
state.last_scroll_time = Some(Instant::now()); state.last_scroll_time = Some(std::time::Instant::now());
} }
state state
} }
@@ -115,6 +132,9 @@ impl ScrollbarState {
fn with_hovered_on_thumb(&self, axis: Option<ScrollbarAxis>) -> Self { fn with_hovered_on_thumb(&self, axis: Option<ScrollbarAxis>) -> Self {
let mut state = *self; let mut state = *self;
state.hovered_on_thumb = axis; state.hovered_on_thumb = axis;
if axis.is_some() {
state.last_scroll_time = Some(std::time::Instant::now());
}
state state
} }
@@ -135,7 +155,18 @@ impl ScrollbarState {
state state
} }
fn with_last_update(&self, t: Instant) -> Self {
let mut state = *self;
state.last_update = t;
state
}
fn is_scrollbar_visible(&self) -> bool { fn is_scrollbar_visible(&self) -> bool {
// On drag
if self.dragged_axis.is_some() {
return true;
}
if let Some(last_time) = self.last_scroll_time { if let Some(last_time) = self.last_scroll_time {
let elapsed = Instant::now().duration_since(last_time).as_secs_f32(); let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
elapsed < FADE_OUT_DURATION elapsed < FADE_OUT_DURATION
@@ -178,9 +209,9 @@ impl ScrollbarAxis {
match self { match self {
Self::Vertical => vec![Self::Vertical], Self::Vertical => vec![Self::Vertical],
Self::Horizontal => vec![Self::Horizontal], Self::Horizontal => vec![Self::Horizontal],
// This should keep vertical first, vertical is the primary axis // This should keep Horizontal first, Vertical is the primary axis
// if vertical not need display, then horizontal will not keep right margin. // if Vertical not need display, then Horizontal will not keep right margin.
Self::Both => vec![Self::Vertical, Self::Horizontal], Self::Both => vec![Self::Horizontal, Self::Vertical],
} }
} }
} }
@@ -189,11 +220,14 @@ impl ScrollbarAxis {
pub struct Scrollbar { pub struct Scrollbar {
view_id: EntityId, view_id: EntityId,
axis: ScrollbarAxis, axis: ScrollbarAxis,
/// When is vertical, this is the height of the scrollbar.
width: Pixels,
scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>, scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
scroll_size: gpui::Size<Pixels>, scroll_size: gpui::Size<Pixels>,
state: Rc<Cell<ScrollbarState>>, state: Rc<Cell<ScrollbarState>>,
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
///
/// This is used to limit the update rate of the scrollbar when it is
/// being dragged for some complex interactions for reducing CPU usage.
max_fps: usize,
} }
impl Scrollbar { impl Scrollbar {
@@ -209,8 +243,8 @@ impl Scrollbar {
state, state,
axis, axis,
scroll_size, scroll_size,
width: px(12.),
scroll_handle: Rc::new(Box::new(scroll_handle)), scroll_handle: Rc::new(Box::new(scroll_handle)),
max_fps: 120,
} }
} }
@@ -290,11 +324,21 @@ impl Scrollbar {
self self
} }
/// Set maximum frames per second for scrolling by drag. Default is 120 FPS.
///
/// If you have very high CPU usage, consider reducing this value to improve performance.
///
/// Available values: 30..120
pub fn max_fps(mut self, max_fps: usize) -> Self {
self.max_fps = max_fps.clamp(30, 120);
self
}
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) { fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels) {
( (
cx.theme().scrollbar_thumb_hover, cx.theme().scrollbar_thumb_hover,
cx.theme().scrollbar, cx.theme().scrollbar,
cx.theme().base.step(cx, ColorScaleStep::THREE), cx.theme().base.step(cx, ColorScaleStep::SEVEN),
THUMB_INSET - px(1.), THUMB_INSET - px(1.),
THUMB_RADIUS, THUMB_RADIUS,
) )
@@ -304,7 +348,7 @@ impl Scrollbar {
( (
cx.theme().scrollbar_thumb_hover, cx.theme().scrollbar_thumb_hover,
cx.theme().scrollbar, cx.theme().scrollbar,
cx.theme().base.step(cx, ColorScaleStep::THREE), cx.theme().base.step(cx, ColorScaleStep::SIX),
THUMB_INSET - px(1.), THUMB_INSET - px(1.),
THUMB_RADIUS, THUMB_RADIUS,
) )
@@ -382,11 +426,11 @@ impl Element for Scrollbar {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) { ) -> (gpui::LayoutId, Self::RequestLayoutState) {
let style = Style { let style = gpui::Style {
position: Position::Absolute, position: Position::Absolute,
flex_grow: 1.0, flex_grow: 1.0,
flex_shrink: 1.0, flex_shrink: 1.0,
size: Size { size: gpui::Size {
width: relative(1.).into(), width: relative(1.).into(),
height: relative(1.).into(), height: relative(1.).into(),
}, },
@@ -409,7 +453,6 @@ impl Element for Scrollbar {
}); });
let mut states = vec![]; let mut states = vec![];
let mut has_both = self.axis.is_both(); let mut has_both = self.axis.is_both();
for axis in self.axis.all().into_iter() { for axis in self.axis.all().into_iter() {
@@ -430,7 +473,7 @@ impl Element for Scrollbar {
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible. // The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
let margin_end = if has_both && !is_vertical { let margin_end = if has_both && !is_vertical {
self.width WIDTH
} else { } else {
px(0.) px(0.)
}; };
@@ -449,31 +492,29 @@ impl Element for Scrollbar {
let bounds = Bounds { let bounds = Bounds {
origin: if is_vertical { origin: if is_vertical {
point( point(hitbox.origin.x + hitbox.size.width - WIDTH, hitbox.origin.y)
hitbox.origin.x + hitbox.size.width - self.width,
hitbox.origin.y,
)
} else { } else {
point( point(
hitbox.origin.x, hitbox.origin.x,
hitbox.origin.y + hitbox.size.height - self.width, hitbox.origin.y + hitbox.size.height - WIDTH,
) )
}, },
size: gpui::Size { size: gpui::Size {
width: if is_vertical { width: if is_vertical {
self.width WIDTH
} else { } else {
hitbox.size.width hitbox.size.width
}, },
height: if is_vertical { height: if is_vertical {
hitbox.size.height hitbox.size.height
} else { } else {
self.width WIDTH
}, },
}, },
}; };
let state = self.state.clone(); let state = self.state.clone();
let is_always_to_show = cx.theme().scrollbar_show.is_always();
let is_hover_to_show = cx.theme().scrollbar_show.is_hover(); let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
let is_hovered_on_bar = state.get().hovered_axis == Some(axis); let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis); let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
@@ -481,7 +522,9 @@ impl Element for Scrollbar {
let (thumb_bg, bar_bg, bar_border, inset, radius) = let (thumb_bg, bar_bg, bar_border, inset, radius) =
if state.get().dragged_axis == Some(axis) { if state.get().dragged_axis == Some(axis) {
Self::style_for_active(cx) Self::style_for_active(cx)
} else if is_hover_to_show && is_hovered_on_bar { } else if (is_hover_to_show || is_always_to_show)
&& (is_hovered_on_bar || is_hovered_on_thumb)
{
if is_hovered_on_thumb { if is_hovered_on_thumb {
Self::style_for_hovered_thumb(cx) Self::style_for_hovered_thumb(cx)
} else { } else {
@@ -520,12 +563,12 @@ impl Element for Scrollbar {
let thumb_bounds = if is_vertical { let thumb_bounds = if is_vertical {
Bounds::from_corners( Bounds::from_corners(
point(bounds.origin.x, bounds.origin.y + thumb_start), point(bounds.origin.x, bounds.origin.y + thumb_start),
point(bounds.origin.x + self.width, bounds.origin.y + thumb_end), point(bounds.origin.x + WIDTH, bounds.origin.y + thumb_end),
) )
} else { } else {
Bounds::from_corners( Bounds::from_corners(
point(bounds.origin.x + thumb_start, bounds.origin.y), point(bounds.origin.x + thumb_start, bounds.origin.y),
point(bounds.origin.x + thumb_end, bounds.origin.y + self.width), point(bounds.origin.x + thumb_end, bounds.origin.y + WIDTH),
) )
}; };
let thumb_fill_bounds = if is_vertical { let thumb_fill_bounds = if is_vertical {
@@ -535,7 +578,7 @@ impl Element for Scrollbar {
bounds.origin.y + thumb_start + inset, bounds.origin.y + thumb_start + inset,
), ),
point( point(
bounds.origin.x + self.width - inset, bounds.origin.x + WIDTH - inset,
bounds.origin.y + thumb_end - inset, bounds.origin.y + thumb_end - inset,
), ),
) )
@@ -547,7 +590,7 @@ impl Element for Scrollbar {
), ),
point( point(
bounds.origin.x + thumb_end - inset, bounds.origin.x + thumb_end - inset,
bounds.origin.y + self.width - inset, bounds.origin.y + WIDTH - inset,
), ),
) )
}; };
@@ -589,6 +632,15 @@ impl Element for Scrollbar {
let is_visible = self.state.get().is_scrollbar_visible(); let is_visible = self.state.get().is_scrollbar_visible();
let is_hover_to_show = cx.theme().scrollbar_show.is_hover(); let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
// Update last_scroll_time when offset is changed.
if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
self.state.set(
self.state
.get()
.with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
);
}
window.with_content_mask( window.with_content_mask(
Some(ContentMask { Some(ContentMask {
bounds: hitbox_bounds, bounds: hitbox_bounds,
@@ -711,30 +763,36 @@ impl Element for Scrollbar {
let scroll_handle = self.scroll_handle.clone(); let scroll_handle = self.scroll_handle.clone();
let state = self.state.clone(); let state = self.state.clone();
let view_id = self.view_id; let view_id = self.view_id;
let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
move |event: &MouseMoveEvent, _, _, cx| { move |event: &MouseMoveEvent, _, _, cx| {
let mut notify = false;
// When is hover to show mode or it was visible,
// we need to update the hovered state and increase the last_scroll_time.
let need_hover_to_update = is_hover_to_show || is_visible;
// Update hovered state for scrollbar // Update hovered state for scrollbar
if bounds.contains(&event.position) { if bounds.contains(&event.position) && need_hover_to_update {
if state.get().hovered_axis != Some(axis) {
state.set(state.get().with_hovered(Some(axis))); state.set(state.get().with_hovered(Some(axis)));
cx.notify(view_id);
if state.get().hovered_axis != Some(axis) {
notify = true;
} }
} else if state.get().hovered_axis == Some(axis) } else if state.get().hovered_axis == Some(axis)
&& state.get().hovered_axis.is_some() && state.get().hovered_axis.is_some()
{ {
state.set(state.get().with_hovered(None)); state.set(state.get().with_hovered(None));
cx.notify(view_id); notify = true;
} }
// Update hovered state for scrollbar thumb // Update hovered state for scrollbar thumb
if thumb_bounds.contains(&event.position) { if thumb_bounds.contains(&event.position) {
if state.get().hovered_on_thumb != Some(axis) { if state.get().hovered_on_thumb != Some(axis) {
state.set(state.get().with_hovered_on_thumb(Some(axis))); state.set(state.get().with_hovered_on_thumb(Some(axis)));
cx.notify(view_id); notify = true;
} }
} else if state.get().hovered_on_thumb == Some(axis) { } else if state.get().hovered_on_thumb == Some(axis) {
state.set(state.get().with_hovered_on_thumb(None)); state.set(state.get().with_hovered_on_thumb(None));
cx.notify(view_id); notify = true;
} }
// Move thumb position on dragging // Move thumb position on dragging
@@ -769,11 +827,19 @@ impl Element for Scrollbar {
if (scroll_handle.offset().y - offset.y).abs() > px(1.) if (scroll_handle.offset().y - offset.y).abs() > px(1.)
|| (scroll_handle.offset().x - offset.x).abs() > px(1.) || (scroll_handle.offset().x - offset.x).abs() > px(1.)
{ {
// Limit update rate
if state.get().last_update.elapsed() > max_fps_duration {
scroll_handle.set_offset(offset); scroll_handle.set_offset(offset);
cx.notify(view_id); state.set(state.get().with_last_update(Instant::now()));
notify = true;
} }
} }
} }
if notify {
cx.notify(view_id);
}
}
}); });
window.on_mouse_event({ window.on_mouse_event({

View File

@@ -121,7 +121,6 @@ impl RenderOnce for WindowBorder {
.when(!tiling.bottom, |div| div.pb(SHADOW_SIZE)) .when(!tiling.bottom, |div| div.pb(SHADOW_SIZE))
.when(!tiling.left, |div| div.pl(SHADOW_SIZE)) .when(!tiling.left, |div| div.pl(SHADOW_SIZE))
.when(!tiling.right, |div| div.pr(SHADOW_SIZE)) .when(!tiling.right, |div| div.pr(SHADOW_SIZE))
.on_mouse_move(|_e, window, _cx| window.refresh())
.on_mouse_down(MouseButton::Left, move |_, window, _cx| { .on_mouse_down(MouseButton::Left, move |_, window, _cx| {
let size = window.window_bounds().get_bounds().size; let size = window.window_bounds().get_bounds().size;
let pos = window.mouse_position(); let pos = window.mouse_position();