This commit is contained in:
2026-09-24 21:13:09 +07:00
parent 2a50804583
commit ce9147c09c
6 changed files with 452 additions and 137 deletions
+44 -57
View File
@@ -31,13 +31,11 @@ use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent}; use ui::dock::{Panel, PanelEvent};
use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState}; use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState};
use ui::menu::DropdownMenu; use ui::menu::DropdownMenu;
use ui::message::{MessageRow, WelcomeMessage};
use ui::notification::Notification; use ui::notification::Notification;
use ui::scroll::Scrollbar; use ui::scroll::Scrollbar;
use ui::tooltip::Tooltip; use ui::tooltip::Tooltip;
use ui::{ use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, WindowExtension,
h_flex, v_flex,
};
use crate::file::*; use crate::file::*;
use crate::text::RenderedText; use crate::text::RenderedText;
@@ -45,6 +43,10 @@ use crate::text::RenderedText;
const REACTION_EMOJIS: &[&str] = &["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"]; const REACTION_EMOJIS: &[&str] = &["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"];
const COMPACT_REACTION_EMOJIS: &[&str] = &["👍", "❤️", "👀"]; const COMPACT_REACTION_EMOJIS: &[&str] = &["👍", "❤️", "👀"];
/// Notice shown when a conversation has no messages, and above the message list.
const PRIVATE_NOTICE: &str =
"This conversation is private. Only members can see each other's messages.";
/// Regex matching strings that consist entirely of emoji characters, /// Regex matching strings that consist entirely of emoji characters,
/// zero-width joiners, variation selectors, and keycap combiners. /// zero-width joiners, variation selectors, and keycap combiners.
static EMOJI_RE: LazyLock<Regex> = static EMOJI_RE: LazyLock<Regex> =
@@ -126,9 +128,8 @@ impl ChatPanel {
let replies_to = cx.new(|_| HashSet::new()); let replies_to = cx.new(|_| HashSet::new());
let reports_by_id = Arc::new(RwLock::new(BTreeMap::new())); let reports_by_id = Arc::new(RwLock::new(BTreeMap::new()));
// Define list of messages
let messages = Vec::new(); let messages = Vec::new();
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.)); let list_state = ListState::new(messages.len() + 1, ListAlignment::Bottom, px(1024.));
// Get room id and name // Get room id and name
let (id, name) = room let (id, name) = room
@@ -577,7 +578,6 @@ impl ChatPanel {
where where
E: Into<Message>, E: Into<Message>,
{ {
let old_len = self.messages.len();
let msg: Message = m.into(); let msg: Message = m.into();
if let Err(pos) = self.messages.binary_search(&msg) { if let Err(pos) = self.messages.binary_search(&msg) {
@@ -586,7 +586,8 @@ impl ChatPanel {
for (i, message) in self.messages.iter().enumerate().skip(pos) { for (i, message) in self.messages.iter().enumerate().skip(pos) {
self.message_index.insert(message.id, i); self.message_index.insert(message.id, i);
} }
self.list_state.splice(old_len..old_len, 1); let len = self.list_state.item_count();
self.list_state.splice(len..len, 1);
if scroll { if scroll {
self.list_state.scroll_to(ListOffset { self.list_state.scroll_to(ListOffset {
@@ -657,7 +658,8 @@ impl ChatPanel {
/// Scroll to a message by its ID /// Scroll to a message by its ID
fn scroll_to(&self, id: &EventId) { fn scroll_to(&self, id: &EventId) {
if let Some(ix) = self.messages.iter().position(|msg| &msg.id == id) { if let Some(ix) = self.messages.iter().position(|msg| &msg.id == id) {
self.list_state.scroll_to_reveal_item(ix); // The welcome message occupies the first row, so message rows sit one ahead.
self.list_state.scroll_to_reveal_item(ix + 1);
} }
} }
@@ -1051,10 +1053,19 @@ impl ChatPanel {
cx.open_url(&content); cx.open_url(&content);
} }
fn render_announcement(&self, cx: &Context<Self>) -> AnyElement { fn render_welcome(&self, cx: &Context<Self>) -> AnyElement {
const MSG: &str = WelcomeMessage::new("welcome")
"This conversation is private. Only members can see each other's messages."; .icon(
svg()
.path("brand/coop.svg")
.size_12()
.text_color(cx.theme().ghost_element_background_alt),
)
.message(PRIVATE_NOTICE)
.into_any_element()
}
fn render_announcement(&self, cx: &Context<Self>) -> AnyElement {
v_flex() v_flex()
.h_40() .h_40()
.w_full() .w_full()
@@ -1072,7 +1083,7 @@ impl ChatPanel {
.size_12() .size_12()
.text_color(cx.theme().ghost_element_active), .text_color(cx.theme().ghost_element_active),
) )
.child(MSG) .child(PRIVATE_NOTICE)
.into_any_element() .into_any_element()
} }
@@ -1141,7 +1152,14 @@ impl ChatPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> AnyElement { ) -> AnyElement {
let file = self.messages.get(ix).and_then(|message| { if ix == 0 {
return self.render_welcome(cx);
}
// The welcome message occupies the first row, so message rows sit one ahead.
let index = ix - 1;
let file = self.messages.get(index).and_then(|message| {
let file = message.file.clone()?; let file = message.file.clone()?;
(!self.decrypted_files.contains_key(&message.id) && file.is_image()) (!self.decrypted_files.contains_key(&message.id) && file.is_image())
.then_some((message.id, file)) .then_some((message.id, file))
@@ -1151,9 +1169,9 @@ impl ChatPanel {
self.load_file(id, file, cx); self.load_file(id, file, cx);
} }
if let Some(message) = self.messages.get(ix) { if let Some(message) = self.messages.get(index) {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let show_author = self.is_group_start(ix); let show_author = self.is_group_start(index);
let text = self let text = self
.rendered_texts_by_id .rendered_texts_by_id
.entry(message.id) .entry(message.id)
@@ -1188,20 +1206,10 @@ impl ChatPanel {
// Hide avatar setting // Hide avatar setting
let hide_avatar = AppSettings::get_hide_avatar(cx); let hide_avatar = AppSettings::get_hide_avatar(cx);
div() MessageRow::new(ix)
.id(ix) .show_author(show_author)
.group("") .hide_avatar(hide_avatar)
.relative() .avatar(
.w_full()
.py_1()
.px_3()
.child(
div()
.flex()
.gap_3()
.when(!hide_avatar, |this| {
if show_author {
this.child(
Avatar::new(author.avatar()) Avatar::new(author.avatar())
.seed(author.avatar_seed()) .seed(author.avatar_seed())
.flex_shrink_0() .flex_shrink_0()
@@ -1213,28 +1221,10 @@ impl ChatPanel {
.menu("View on njump.me", Box::new(Command::Njump(pk))) .menu("View on njump.me", Box::new(Command::Njump(pk)))
}), }),
) )
} else { .author(author.name())
this.child(div().flex_shrink_0().w(px(32.))) .timestamp(message.created_at.to_human_time())
}
})
.child(
v_flex()
.flex_1()
.w_full()
.flex_initial()
.overflow_hidden()
.when(show_author, |this| {
this.child(
h_flex()
.gap_2()
.text_sm()
.text_color(cx.theme().text_placeholder)
.child(div().font_semibold().child(author.name()))
.child(message.created_at.to_human_time())
.when(has_reports, |this| { .when(has_reports, |this| {
this.child(self.render_sent_reports(&id, cx)) this.header_extra(self.render_sent_reports(&id, cx))
}),
)
}) })
.when(has_replies, |this| { .when(has_replies, |this| {
this.children(self.render_message_replies(replies, cx)) this.children(self.render_message_replies(replies, cx))
@@ -1246,10 +1236,8 @@ impl ChatPanel {
}) })
.when(has_reactions, |this| { .when(has_reactions, |this| {
this.child(self.render_reactions(&id, cx)) this.child(self.render_reactions(&id, cx))
}), })
), .overlay(
)
.child(
div() div()
.group_hover("", |this| this.bg(cx.theme().element_active)) .group_hover("", |this| this.bg(cx.theme().element_active))
.absolute() .absolute()
@@ -1259,7 +1247,7 @@ impl ChatPanel {
.h_full() .h_full()
.bg(cx.theme().border_transparent), .bg(cx.theme().border_transparent),
) )
.child(self.render_actions(&id, &pk, cx)) .overlay(self.render_actions(&id, &pk, cx))
.on_mouse_down( .on_mouse_down(
MouseButton::Middle, MouseButton::Middle,
cx.listener(move |this, _, _window, cx| { cx.listener(move |this, _, _window, cx| {
@@ -1269,7 +1257,6 @@ impl ChatPanel {
.on_double_click(cx.listener(move |this, _, _window, cx| { .on_double_click(cx.listener(move |this, _, _window, cx| {
this.reply_to(&id, cx); this.reply_to(&id, cx);
})) }))
.hover(|this| this.bg(cx.theme().surface_background))
.into_any_element() .into_any_element()
} }
+36 -7
View File
@@ -19,6 +19,7 @@ use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent}; use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, Textarea, TextareaState}; use ui::input::{InputEvent, Textarea, TextareaState};
use ui::message::WelcomeMessage;
use ui::notification::Notification; use ui::notification::Notification;
use ui::scroll::Scrollbar; use ui::scroll::Scrollbar;
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex}; use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex};
@@ -205,15 +206,15 @@ impl CommunityPanel {
self.rows.clear(); self.rows.clear();
self.has_more = false; self.has_more = false;
self.loading = false; self.loading = false;
self.list_state.reset(1); self.list_state.reset(2);
} }
channel channel
} }
/// The list's item count: the load-older row, then every message row. /// The list's item count: the welcome row, the load-older row, then every message row.
fn item_count(&self) -> usize { fn item_count(&self) -> usize {
self.rows.len() + 1 self.rows.len() + 2
} }
/// A timeline read, `before_ms` exclusive, or `None` for the newest rows. /// A timeline read, `before_ms` exclusive, or `None` for the newest rows.
@@ -542,7 +543,8 @@ impl CommunityPanel {
shown.insert(message.id, at); shown.insert(message.id, at);
self.rows.insert(at, message); self.rows.insert(at, message);
self.list_state.splice(at + 1..at + 1, 1); // The welcome and load-older rows sit above the messages.
self.list_state.splice(at + 2..at + 2, 1);
} }
} }
@@ -624,7 +626,29 @@ impl CommunityPanel {
.into_any_element() .into_any_element()
} }
/// The row at index 0: the affordance that pages older history in. /// The row at index 0: the welcome message for the channel.
fn render_welcome(&self, cx: &Context<Self>) -> AnyElement {
let (name, avatar) = self
.community
.read_with(cx, |community, _cx| {
let seed = community.id().to_hex();
let avatar = match community.icon() {
Some(path) => Avatar::from_source(path).seed(seed).large(),
None => Avatar::new(None).seed(seed).large(),
};
(community.name(), avatar)
})
.unwrap_or_else(|_| (SharedString::from("this community"), Avatar::new(None)));
WelcomeMessage::new("welcome")
.icon(avatar)
.title(format!("Welcome to {}", name))
.message(format!("This is the start of the {} channel.", name))
.into_any_element()
}
/// The row at index 1: the affordance that pages older history in.
fn render_older(&self, cx: &mut Context<Self>) -> AnyElement { fn render_older(&self, cx: &mut Context<Self>) -> AnyElement {
if !self.has_more { if !self.has_more {
return div().into_any_element(); return div().into_any_element();
@@ -670,14 +694,19 @@ impl CommunityPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> AnyElement { ) -> AnyElement {
if ix == 0 { if ix == 0 {
return self.render_welcome(cx);
}
if ix == 1 {
return self.render_older(cx); return self.render_older(cx);
} }
let Some(message) = self.rows.get(ix - 1) else { // The welcome and load-older rows sit above the messages.
let Some(message) = self.rows.get(ix - 2) else {
return div().into_any_element(); return div().into_any_element();
}; };
let show_author = self.opens_run(ix - 1); let show_author = self.opens_run(ix - 2);
message::render(ix, message, show_author, cx) message::render(ix, message, show_author, cx)
} }
+11 -42
View File
@@ -3,68 +3,37 @@ use std::collections::BTreeMap;
use common::TimestampExt; use common::TimestampExt;
use community::ChatMessage; use community::ChatMessage;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, div};
AnyElement, App, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, px,
};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use settings::AppSettings; use settings::AppSettings;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::{StyledExt, h_flex, v_flex}; use ui::h_flex;
use ui::message::MessageRow;
pub(crate) fn render(ix: usize, message: &ChatMessage, show_author: bool, cx: &App) -> AnyElement { pub(crate) fn render(ix: usize, message: &ChatMessage, show_author: bool, cx: &App) -> AnyElement {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let author = persons.read(cx).get(&message.author, cx); let author = persons.read(cx).get(&message.author, cx);
let hide_avatar = AppSettings::get_hide_avatar(cx); let hide_avatar = AppSettings::get_hide_avatar(cx);
div() MessageRow::new(ix)
.id(ix) .show_author(show_author)
.w_full() .hide_avatar(hide_avatar)
.py_1() .avatar(
.px_3()
.hover(|this| this.bg(cx.theme().surface_background))
.child(
h_flex()
.items_start()
.gap_3()
.when(!hide_avatar, |this| {
if show_author {
this.child(
Avatar::new(author.avatar()) Avatar::new(author.avatar())
.seed(author.avatar_seed()) .seed(author.avatar_seed())
.flex_shrink_0(), .flex_shrink_0(),
) )
} else { .author(author.name())
this.child(div().flex_shrink_0().w(px(32.))) .timestamp(Timestamp::from_secs(message.at_ms / 1000).to_human_time())
}
})
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_0p5()
.when(show_author, |this| {
this.child(
h_flex()
.gap_2()
.text_sm()
.text_color(cx.theme().text_placeholder)
.child(div().font_semibold().child(author.name()))
.child(
Timestamp::from_secs(message.at_ms / 1000).to_human_time(),
)
.when(message.edited_at.is_some(), |this| { .when(message.edited_at.is_some(), |this| {
this.child(div().child("(edited)")) this.header_extra(div().child("(edited)"))
}),
)
}) })
.child(content(message, cx)) .child(content(message, cx))
.when(!message.reactions.is_empty(), |this| { .when(!message.reactions.is_empty(), |this| {
this.child(reactions(message, cx)) this.child(reactions(message, cx))
}), })
),
)
.into_any_element() .into_any_element()
} }
+1
View File
@@ -17,6 +17,7 @@ pub mod group_box;
pub mod indicator; pub mod indicator;
pub mod input; pub mod input;
pub mod menu; pub mod menu;
pub mod message;
pub mod modal; pub mod modal;
pub mod nav_item; pub mod nav_item;
pub mod notification; pub mod notification;
+325
View File
@@ -0,0 +1,325 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, MouseButton,
MouseDownEvent, ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled, Window,
div, px,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use crate::{InteractiveElementExt as _, StyledExt as _, h_flex, v_flex};
type MouseDownListener = Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>;
type DoubleClickListener = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
/// A single message row in a chat-like list.
#[derive(IntoElement)]
pub struct MessageRow {
id: ElementId,
style: StyleRefinement,
show_author: bool,
hide_avatar: bool,
avatar: Option<AnyElement>,
author: Option<SharedString>,
timestamp: Option<SharedString>,
header_extras: SmallVec<[AnyElement; 1]>,
body: SmallVec<[AnyElement; 4]>,
overlays: SmallVec<[AnyElement; 2]>,
on_mouse_down: Option<(MouseButton, MouseDownListener)>,
on_double_click: Option<DoubleClickListener>,
}
impl MessageRow {
/// Create a message row with the given element id.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
style: StyleRefinement::default(),
show_author: false,
hide_avatar: false,
avatar: None,
author: None,
timestamp: None,
header_extras: SmallVec::new(),
body: SmallVec::new(),
overlays: SmallVec::new(),
on_mouse_down: None,
on_double_click: None,
}
}
/// Whether this row opens a run of messages from one author.
#[must_use]
pub fn show_author(mut self, show_author: bool) -> Self {
self.show_author = show_author;
self
}
/// Hide the avatar column entirely.
#[must_use]
pub fn hide_avatar(mut self, hide_avatar: bool) -> Self {
self.hide_avatar = hide_avatar;
self
}
/// The avatar shown when this row opens a run of messages.
#[must_use]
pub fn avatar(mut self, avatar: impl IntoElement) -> Self {
self.avatar = Some(avatar.into_any_element());
self
}
/// The author's display name, shown next to the timestamp.
#[must_use]
pub fn author(mut self, author: impl Into<SharedString>) -> Self {
self.author = Some(author.into());
self
}
/// The time the message was sent.
#[must_use]
pub fn timestamp(mut self, timestamp: impl Into<SharedString>) -> Self {
self.timestamp = Some(timestamp.into());
self
}
/// Append an element to the header row.
#[must_use]
pub fn header_extra(mut self, extra: impl IntoElement) -> Self {
self.header_extras.push(extra.into_any_element());
self
}
/// Append an element to the message body, in order.
#[must_use]
pub fn child(mut self, child: impl IntoElement) -> Self {
self.body.push(child.into_any_element());
self
}
/// Append several elements to the message body, in order.
#[must_use]
pub fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self {
self.body
.extend(children.into_iter().map(|child| child.into_any_element()));
self
}
/// Append an element positioned over the row.
#[must_use]
pub fn overlay(mut self, overlay: impl IntoElement) -> Self {
self.overlays.push(overlay.into_any_element());
self
}
/// Handle a mouse button press anywhere on the row.
#[must_use]
pub fn on_mouse_down(
mut self,
button: MouseButton,
listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_mouse_down = Some((button, Box::new(listener)));
self
}
/// Handle a double click anywhere on the row.
#[must_use]
pub fn on_double_click(
mut self,
listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_double_click = Some(Box::new(listener));
self
}
}
impl Styled for MessageRow {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for MessageRow {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let Self {
id,
style,
show_author,
hide_avatar,
avatar,
author,
timestamp,
header_extras,
body,
overlays,
on_mouse_down,
on_double_click,
} = self;
div()
.id(id)
.group("")
.relative()
.w_full()
.py_1()
.when(show_author, |this| this.pt_2())
.px_3()
.refine_style(&style)
.child(
h_flex()
.items_start()
.gap_3()
.when(!hide_avatar, |this| {
if show_author {
match avatar {
Some(avatar) => this.child(avatar),
None => this.child(div().flex_shrink_0().w(px(32.))),
}
} else {
this.child(div().flex_shrink_0().w(px(32.)))
}
})
.child(
v_flex()
.flex_1()
.w_full()
.min_w_0()
.flex_initial()
.overflow_hidden()
.when(show_author, |this| {
this.child(
h_flex()
.gap_2()
.text_sm()
.text_color(cx.theme().text_placeholder)
.when_some(author, |this, author| {
this.child(div().font_semibold().child(author))
})
.when_some(timestamp, |this, timestamp| {
this.child(timestamp)
})
.children(header_extras),
)
})
.children(body),
),
)
.children(overlays)
.when_some(on_mouse_down, |this, (button, listener)| {
this.on_mouse_down(button, listener)
})
.when_some(on_double_click, |this, listener| {
this.on_double_click(listener)
})
.hover(|this| this.bg(cx.theme().surface_background))
.into_any_element()
}
}
/// A welcome message shown at the top of a message list.
#[derive(IntoElement)]
pub struct WelcomeMessage {
id: ElementId,
style: StyleRefinement,
icon: Option<AnyElement>,
title: Option<SharedString>,
message: Option<SharedString>,
children: SmallVec<[AnyElement; 2]>,
}
impl WelcomeMessage {
/// Create a welcome message with the given element id.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
style: StyleRefinement::default(),
icon: None,
title: None,
message: None,
children: SmallVec::new(),
}
}
/// The icon shown above the title.
#[must_use]
pub fn icon(mut self, icon: impl IntoElement) -> Self {
self.icon = Some(icon.into_any_element());
self
}
/// The welcome title.
#[must_use]
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
self.title = Some(title.into());
self
}
/// The welcome body text.
#[must_use]
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
self.message = Some(message.into());
self
}
/// Append an element below the body text.
#[must_use]
pub fn child(mut self, child: impl IntoElement) -> Self {
self.children.push(child.into_any_element());
self
}
}
impl Styled for WelcomeMessage {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for WelcomeMessage {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let Self {
id,
style,
icon,
title,
message,
children,
} = self;
v_flex()
.id(id)
.w_full()
.gap_2()
.p_3()
.items_center()
.justify_center()
.text_center()
.refine_style(&style)
.when_some(icon, |this, icon| this.child(icon))
.child(
v_flex()
.items_center()
.justify_center()
.text_center()
.when_some(title, |this, title| {
this.child(
div()
.text_sm()
.font_semibold()
.text_color(cx.theme().text)
.child(title),
)
})
.when_some(message, |this, message| {
this.child(
div()
.text_xs()
.text_color(cx.theme().text_placeholder)
.child(message),
)
}),
)
.children(children)
}
}
+5 -1
View File
@@ -127,6 +127,7 @@ impl RenderOnce for TabBar {
let selected_index = self.selected_index; let selected_index = self.selected_index;
let on_click = self.on_click.clone(); let on_click = self.on_click.clone();
let segmented = self.segmented; let segmented = self.segmented;
let has_prefix = self.prefix.is_some();
self.base self.base
.group("tab-bar") .group("tab-bar")
@@ -145,7 +146,10 @@ impl RenderOnce for TabBar {
h_flex() h_flex()
.id("tabs") .id("tabs")
.flex_1() .flex_1()
.when(!segmented, |this| this.overflow_x_scroll()) .when(!segmented, |this| {
this.overflow_x_scroll()
.when(!has_prefix, |this| this.pl_2())
})
.when_some(self.scroll_handle, |this, scroll_handle| { .when_some(self.scroll_handle, |this, scroll_handle| {
this.track_scroll(&scroll_handle) this.track_scroll(&scroll_handle)
}) })