chore: minor ui components improvements (#140)

* improve ui

* .

* .
This commit is contained in:
reya
2025-09-04 07:30:03 +07:00
committed by GitHub
parent b11b0e0115
commit 70e235dcc2
14 changed files with 86 additions and 75 deletions

View File

@@ -1231,7 +1231,7 @@ impl ChatSpace {
this.child( this.child(
h_flex() h_flex()
.id("downloading") .id("downloading")
.px_4() .px_2()
.h_6() .h_6()
.gap_1() .gap_1()
.text_xs() .text_xs()
@@ -1434,7 +1434,7 @@ impl Render for ChatSpace {
// Only render titlebar child elements if user is logged in // Only render titlebar child elements if user is logged in
if registry.identity.is_some() { if registry.identity.is_some() {
let profile = Registry::read_global(cx).identity(cx); let profile = registry.identity(cx);
let left_side = self let left_side = self
.render_titlebar_left_side(window, cx) .render_titlebar_left_side(window, cx)
@@ -1457,9 +1457,7 @@ impl Render for ChatSpace {
.relative() .relative()
.size_full() .size_full()
.child( .child(
div() v_flex()
.flex()
.flex_col()
.size_full() .size_full()
// Title Bar // Title Bar
.child(self.title_bar.clone()) .child(self.title_bar.clone())

View File

@@ -79,6 +79,9 @@ fn main() {
// Open a window with default options // Open a window with default options
cx.open_window(opts, |window, cx| { cx.open_window(opts, |window, cx| {
// Bring the app to the foreground
cx.activate(true);
// Automatically sync theme with system appearance // Automatically sync theme with system appearance
window window
.observe_window_appearance(|window, cx| { .observe_window_appearance(|window, cx| {
@@ -88,7 +91,6 @@ fn main() {
// Root Entity // Root Entity
cx.new(|cx| { cx.new(|cx| {
cx.activate(true);
// Initialize the tokio runtime // Initialize the tokio runtime
gpui_tokio::init(cx); gpui_tokio::init(cx);

View File

@@ -6,7 +6,7 @@ use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext, div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext,
ClipboardItem, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, ClipboardItem, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable,
InteractiveElement, IntoElement, ListAlignment, ListState, MouseButton, ObjectFit, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit,
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window, StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window,
}; };
@@ -139,7 +139,7 @@ impl Chat {
match signal { match signal {
RoomSignal::NewMessage((gift_wrap_id, event)) => { RoomSignal::NewMessage((gift_wrap_id, event)) => {
if !this.is_sent_by_coop(gift_wrap_id) { if !this.is_sent_by_coop(gift_wrap_id) {
this.insert_message(event, cx); this.insert_message(event, false, cx);
} }
} }
RoomSignal::Refresh => { RoomSignal::Refresh => {
@@ -258,11 +258,7 @@ impl Chat {
cx.defer_in(window, |this, window, cx| { cx.defer_in(window, |this, window, cx| {
// Optimistically update message list // Optimistically update message list
this.insert_message(temp_message, cx); this.insert_message(temp_message, true, cx);
// Scroll to reveal the new message
this.list_state
.scroll_to_reveal_item(this.messages.len() + 1);
// Remove all replies // Remove all replies
this.remove_all_replies(cx); this.remove_all_replies(cx);
@@ -341,7 +337,7 @@ impl Chat {
} }
/// Convert and insert a nostr event into the chat panel /// Convert and insert a nostr event into the chat panel
fn insert_message<E>(&mut self, event: E, _cx: &mut Context<Self>) fn insert_message<E>(&mut self, event: E, scroll: bool, cx: &mut Context<Self>)
where where
E: Into<RenderedMessage>, E: Into<RenderedMessage>,
{ {
@@ -350,13 +346,21 @@ impl Chat {
// Extend the messages list with the new events // Extend the messages list with the new events
if self.messages.insert(Message::user(event)) { if self.messages.insert(Message::user(event)) {
self.list_state.splice(old_len..old_len, 1); self.list_state.splice(old_len..old_len, 1);
if scroll {
self.list_state.scroll_to(ListOffset {
item_ix: self.list_state.item_count(),
offset_in_item: px(0.0),
});
cx.notify();
}
} }
} }
/// Convert and insert a vector of nostr events into the chat panel /// Convert and insert a vector of nostr events into the chat panel
fn insert_messages(&mut self, events: Vec<Event>, cx: &mut Context<Self>) { fn insert_messages(&mut self, events: Vec<Event>, cx: &mut Context<Self>) {
for event in events.into_iter() { for event in events.into_iter() {
self.insert_message(event, cx); self.insert_message(event, false, cx);
} }
cx.notify(); cx.notify();
} }

View File

@@ -656,6 +656,7 @@ impl Render for Sidebar {
}) })
}) })
.small() .small()
.cta()
.bold() .bold()
.secondary() .secondary()
.rounded(ButtonRounded::Full) .rounded(ButtonRounded::Full)
@@ -676,6 +677,7 @@ impl Render for Sidebar {
}) })
}) })
.small() .small()
.cta()
.bold() .bold()
.secondary() .secondary()
.rounded(ButtonRounded::Full) .rounded(ButtonRounded::Full)

View File

@@ -1,8 +1,8 @@
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement, div, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement,
IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, Stateful,
StatefulInteractiveElement as _, Styled, Window, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -121,8 +121,8 @@ type OnClick = Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>
/// A Button element. /// A Button element.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Button { pub struct Button {
pub base: Div, base: Stateful<Div>,
id: ElementId, style: StyleRefinement,
icon: Option<Icon>, icon: Option<Icon>,
label: Option<SharedString>, label: Option<SharedString>,
@@ -156,8 +156,8 @@ impl From<Button> for AnyElement {
impl Button { impl Button {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(id: impl Into<ElementId>) -> Self {
Self { Self {
base: div().flex_shrink_0(), base: div().id(id.into()).flex_shrink_0(),
id: id.into(), style: StyleRefinement::default(),
icon: None, icon: None,
label: None, label: None,
disabled: false, disabled: false,
@@ -255,14 +255,14 @@ impl Disableable for Button {
} }
impl Selectable for Button { impl Selectable for Button {
fn element_id(&self) -> &ElementId {
&self.id
}
fn selected(mut self, selected: bool) -> Self { fn selected(mut self, selected: bool) -> Self {
self.selected = selected; self.selected = selected;
self self
} }
fn is_selected(&self) -> bool {
self.selected
}
} }
impl Sizable for Button { impl Sizable for Button {
@@ -280,8 +280,8 @@ impl ButtonVariants for Button {
} }
impl Styled for Button { impl Styled for Button {
fn style(&mut self) -> &mut gpui::StyleRefinement { fn style(&mut self) -> &mut StyleRefinement {
self.base.style() &mut self.style
} }
} }
@@ -308,11 +308,11 @@ impl RenderOnce for Button {
}; };
self.base self.base
.id(self.id) .flex_shrink_0()
.flex() .flex()
.items_center() .items_center()
.justify_center() .justify_center()
.cursor_pointer() .cursor_default()
.overflow_hidden() .overflow_hidden()
.map(|this| match self.rounded { .map(|this| match self.rounded {
ButtonRounded::Normal => this.rounded(cx.theme().radius), ButtonRounded::Normal => this.rounded(cx.theme().radius),
@@ -359,6 +359,8 @@ impl RenderOnce for Button {
Size::XSmall => { Size::XSmall => {
if self.icon.is_some() { if self.icon.is_some() {
this.h_6().pl_2().pr_2p5() this.h_6().pl_2().pr_2p5()
} else if self.cta {
this.h_6().px_4()
} else { } else {
this.h_6().px_2() this.h_6().px_2()
} }
@@ -366,6 +368,8 @@ impl RenderOnce for Button {
Size::Small => { Size::Small => {
if self.icon.is_some() { if self.icon.is_some() {
this.h_7().pl_2().pr_2p5() this.h_7().pl_2().pr_2p5()
} else if self.cta {
this.h_7().px_4()
} else { } else {
this.h_7().px_2() this.h_7().px_2()
} }
@@ -388,10 +392,6 @@ impl RenderOnce for Button {
} }
}) })
.text_color(normal_style.fg) .text_color(normal_style.fg)
.when(self.selected, |this| {
let selected_style = style.selected(window, cx);
this.bg(selected_style.bg).text_color(selected_style.fg)
})
.when(!self.disabled && !self.selected, |this| { .when(!self.disabled && !self.selected, |this| {
this.bg(normal_style.bg) this.bg(normal_style.bg)
.hover(|this| { .hover(|this| {
@@ -403,6 +403,10 @@ impl RenderOnce for Button {
this.bg(active_style.bg).text_color(active_style.fg) this.bg(active_style.bg).text_color(active_style.fg)
}) })
}) })
.when(self.selected, |this| {
let selected_style = style.selected(window, cx);
this.bg(selected_style.bg).text_color(selected_style.fg)
})
.when(self.disabled, |this| { .when(self.disabled, |this| {
let disabled_style = style.disabled(window, cx); let disabled_style = style.disabled(window, cx);
this.cursor_not_allowed() this.cursor_not_allowed()
@@ -410,6 +414,7 @@ impl RenderOnce for Button {
.text_color(disabled_style.fg) .text_color(disabled_style.fg)
.shadow_none() .shadow_none()
}) })
.refine_style(&self.style)
.child({ .child({
h_flex() h_flex()
.id("label") .id("label")

View File

@@ -54,13 +54,13 @@ impl Disableable for Checkbox {
} }
impl Selectable for Checkbox { impl Selectable for Checkbox {
fn element_id(&self) -> &ElementId {
&self.id
}
fn selected(self, selected: bool) -> Self { fn selected(self, selected: bool) -> Self {
self.checked(selected) self.checked(selected)
} }
fn is_selected(&self) -> bool {
self.checked
}
} }
impl RenderOnce for Checkbox { impl RenderOnce for Checkbox {

View File

@@ -20,7 +20,7 @@ impl Indicator {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
size: Size::Small, size: Size::Small,
speed: Duration::from_secs_f64(0.8), speed: Duration::from_secs(1),
icon: Icon::new(IconName::Loader), icon: Icon::new(IconName::Loader),
color: None, color: None,
} }
@@ -52,17 +52,15 @@ impl Sizable for Indicator {
impl RenderOnce for Indicator { impl RenderOnce for Indicator {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
div() div().child(
.child( self.icon
self.icon .with_size(self.size)
.with_size(self.size) .when_some(self.color, |this, color| this.text_color(color))
.when_some(self.color, |this, color| this.text_color(color)) .with_animation(
.with_animation( "circle",
"circle", Animation::new(self.speed).repeat().with_easing(ease_in_out),
Animation::new(self.speed).repeat().with_easing(ease_in_out), |this, delta| this.transform(Transformation::rotate(percentage(delta))),
|this, delta| this.transform(Transformation::rotate(percentage(delta))), ),
), )
)
.into_element()
} }
} }

View File

@@ -15,7 +15,6 @@ type Suffix = Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct ListItem { pub struct ListItem {
id: ElementId,
base: Stateful<Div>, base: Stateful<Div>,
disabled: bool, disabled: bool,
selected: bool, selected: bool,
@@ -30,8 +29,8 @@ pub struct ListItem {
impl ListItem { impl ListItem {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(id: impl Into<ElementId>) -> Self {
let id: ElementId = id.into(); let id: ElementId = id.into();
Self { Self {
id: id.clone(),
base: h_flex().id(id).gap_x_1().py_1().px_2().text_base(), base: h_flex().id(id).gap_x_1().py_1().px_2().text_base(),
disabled: false, disabled: false,
selected: false, selected: false,
@@ -104,14 +103,14 @@ impl Disableable for ListItem {
} }
impl Selectable for ListItem { impl Selectable for ListItem {
fn element_id(&self) -> &ElementId {
&self.id
}
fn selected(mut self, selected: bool) -> Self { fn selected(mut self, selected: bool) -> Self {
self.selected = selected; self.selected = selected;
self self
} }
fn is_selected(&self) -> bool {
self.selected
}
} }
impl Styled for ListItem { impl Styled for ListItem {

View File

@@ -299,7 +299,7 @@ impl Render for Notification {
.border_1() .border_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.bg(cx.theme().surface_background) .bg(cx.theme().surface_background)
.rounded(cx.theme().radius) .rounded(cx.theme().radius * 1.6)
.shadow_md() .shadow_md()
.p_2() .p_2()
.gap_3() .gap_3()

View File

@@ -44,7 +44,7 @@ pub fn init(cx: &mut App) {
]); ]);
} }
pub trait PopupMenuExt: Styled + Selectable + IntoElement + 'static { pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement + 'static {
/// Create a popup menu with the given items, anchored to the TopLeft corner /// Create a popup menu with the given items, anchored to the TopLeft corner
fn popup_menu( fn popup_menu(
self, self,
@@ -60,9 +60,9 @@ pub trait PopupMenuExt: Styled + Selectable + IntoElement + 'static {
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static, f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> Popover<PopupMenu> { ) -> Popover<PopupMenu> {
let style = self.style().clone(); let style = self.style().clone();
let element_id = self.element_id(); let id = self.interactivity().element_id.clone();
Popover::new(SharedString::from(format!("popup-menu:{element_id:?}"))) Popover::new(SharedString::from(format!("popup-menu:{id:?}")))
.no_style() .no_style()
.trigger(self) .trigger(self)
.trigger_style(style) .trigger_style(style)

View File

@@ -1,8 +1,6 @@
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use gpui::{ use gpui::{div, px, App, Axis, Div, Element, Pixels, Refineable, StyleRefinement, Styled};
div, px, App, Axis, Div, Element, ElementId, Pixels, Refineable, StyleRefinement, Styled,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -105,9 +103,16 @@ impl From<Pixels> for Size {
/// A trait for defining element that can be selected. /// A trait for defining element that can be selected.
pub trait Selectable: Sized { pub trait Selectable: Sized {
fn element_id(&self) -> &ElementId;
/// Set the selected state of the element. /// Set the selected state of the element.
fn selected(self, selected: bool) -> Self; fn selected(self, selected: bool) -> Self;
/// Returns true if the element is selected.
fn is_selected(&self) -> bool;
/// Set is the element mouse right clicked, default do nothing.
fn secondary_selected(self, _: bool) -> Self {
self
}
} }
/// A trait for defining element that can be disabled. /// A trait for defining element that can be disabled.

View File

@@ -11,7 +11,6 @@ pub mod tab_bar;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Tab { pub struct Tab {
id: ElementId,
base: Stateful<Div>, base: Stateful<Div>,
label: AnyElement, label: AnyElement,
prefix: Option<AnyElement>, prefix: Option<AnyElement>,
@@ -25,7 +24,6 @@ impl Tab {
let id: ElementId = id.into(); let id: ElementId = id.into();
Self { Self {
id: id.clone(),
base: div().id(id), base: div().id(id),
label: label.into_any_element(), label: label.into_any_element(),
disabled: false, disabled: false,
@@ -55,14 +53,14 @@ impl Tab {
} }
impl Selectable for Tab { impl Selectable for Tab {
fn element_id(&self) -> &ElementId {
&self.id
}
fn selected(mut self, selected: bool) -> Self { fn selected(mut self, selected: bool) -> Self {
self.selected = selected; self.selected = selected;
self self
} }
fn is_selected(&self) -> bool {
self.selected
}
} }
impl InteractiveElement for Tab { impl InteractiveElement for Tab {

View File

@@ -1,5 +1,5 @@
use gpui::{ use gpui::{
div, relative, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, deferred, div, relative, App, AppContext, Context, Entity, IntoElement, ParentElement, Render,
SharedString, Styled, Window, SharedString, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -16,7 +16,7 @@ impl Tooltip {
impl Render for Tooltip { impl Render for Tooltip {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().child( div().child(deferred(
div() div()
.font_family(".SystemUIFont") .font_family(".SystemUIFont")
.m_3() .m_3()
@@ -30,6 +30,6 @@ impl Render for Tooltip {
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
.line_height(relative(1.25)) .line_height(relative(1.25))
.child(self.text.clone()), .child(self.text.clone()),
) ))
} }
} }

View File

@@ -380,6 +380,6 @@ sidebar:
loading: loading:
label: label:
en: "Downloading messages" en: "Getting messages. This may take a while..."
tooltip: tooltip:
en: "This may take a while. Please be patient." en: "The progress runs in the background. It doesn't affect your experience."