feat: revamp the chat panel ui #7
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1009,7 +1009,6 @@ dependencies = [
|
|||||||
"chat",
|
"chat",
|
||||||
"common",
|
"common",
|
||||||
"dock",
|
"dock",
|
||||||
"emojis",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui_tokio",
|
"gpui_tokio",
|
||||||
"indexset",
|
"indexset",
|
||||||
@@ -1848,15 +1847,6 @@ dependencies = [
|
|||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "emojis"
|
|
||||||
version = "0.6.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4"
|
|
||||||
dependencies = [
|
|
||||||
"phf",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
|||||||
3
assets/icons/paper-plane-fill.svg
Normal file
3
assets/icons/paper-plane-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M2.66936 5.12886C2.12122 3.64104 3.6759 2.24953 5.09409 2.95862L20.0468 10.435C21.3366 11.0799 21.3366 12.9205 20.0468 13.5655L5.09409 21.0418C3.67589 21.7509 2.12122 20.3594 2.66936 18.8715L4.92467 12.75H9.25021C9.66442 12.75 10.0002 12.4142 10.0002 12C10.0002 11.5858 9.66442 11.25 9.25021 11.25H4.92452L2.66936 5.12886Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 435 B |
@@ -27,6 +27,5 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
indexset = "0.12.3"
|
indexset = "0.12.3"
|
||||||
emojis = "0.6.4"
|
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ use gpui::Action;
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
#[action(namespace = chat, no_json)]
|
||||||
|
pub enum Command {
|
||||||
|
Insert(&'static str),
|
||||||
|
ChangeSubject(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[action(namespace = chat, no_json)]
|
#[action(namespace = chat, no_json)]
|
||||||
pub struct SeenOn(pub EventId);
|
pub struct SeenOn(pub EventId);
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, px, App, AppContext, Corner, Element, InteractiveElement, IntoElement, ParentElement,
|
|
||||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::input::InputState;
|
|
||||||
use ui::popover::{Popover, PopoverContent};
|
|
||||||
use ui::{Icon, Sizable, Size};
|
|
||||||
|
|
||||||
static EMOJIS: OnceLock<Vec<SharedString>> = OnceLock::new();
|
|
||||||
|
|
||||||
fn get_emojis() -> &'static Vec<SharedString> {
|
|
||||||
EMOJIS.get_or_init(|| {
|
|
||||||
let mut emojis: Vec<SharedString> = vec![];
|
|
||||||
|
|
||||||
emojis.extend(
|
|
||||||
emojis::Group::SmileysAndEmotion
|
|
||||||
.emojis()
|
|
||||||
.map(|e| SharedString::from(e.as_str()))
|
|
||||||
.collect::<Vec<SharedString>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
emojis
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct EmojiPicker {
|
|
||||||
target: Option<WeakEntity<InputState>>,
|
|
||||||
icon: Option<Icon>,
|
|
||||||
anchor: Option<Corner>,
|
|
||||||
size: Size,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EmojiPicker {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
size: Size::default(),
|
|
||||||
target: None,
|
|
||||||
anchor: None,
|
|
||||||
icon: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn target(mut self, target: WeakEntity<InputState>) -> Self {
|
|
||||||
self.target = Some(target);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
|
||||||
self.icon = Some(icon.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn anchor(mut self, corner: Corner) -> Self {
|
|
||||||
self.anchor = Some(corner);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sizable for EmojiPicker {
|
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
||||||
self.size = size.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for EmojiPicker {
|
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
|
||||||
Popover::new("emojis")
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(corner) = self.anchor {
|
|
||||||
this.anchor(corner)
|
|
||||||
} else {
|
|
||||||
this.anchor(gpui::Corner::BottomLeft)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.trigger(
|
|
||||||
Button::new("emojis-trigger")
|
|
||||||
.when_some(self.icon, |this, icon| this.icon(icon))
|
|
||||||
.ghost()
|
|
||||||
.with_size(self.size),
|
|
||||||
)
|
|
||||||
.content(move |window, cx| {
|
|
||||||
let input = self.target.clone();
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
PopoverContent::new(window, cx, move |_window, cx| {
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_wrap()
|
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.children(get_emojis().iter().map(|e| {
|
|
||||||
div()
|
|
||||||
.id(e.clone())
|
|
||||||
.flex_auto()
|
|
||||||
.size_10()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.child(e.clone())
|
|
||||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
|
||||||
.on_click({
|
|
||||||
let item = e.clone();
|
|
||||||
let input = input.clone();
|
|
||||||
|
|
||||||
move |_, window, cx| {
|
|
||||||
if let Some(input) = input.as_ref() {
|
|
||||||
_ = input.update(cx, |this, cx| {
|
|
||||||
let value = this.value();
|
|
||||||
let new_text = if value.is_empty() {
|
|
||||||
format!("{item}")
|
|
||||||
} else if value.ends_with(" ") {
|
|
||||||
format!("{value}{item}")
|
|
||||||
} else {
|
|
||||||
format!("{value} {item}")
|
|
||||||
};
|
|
||||||
this.set_value(new_text, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
.into_any()
|
|
||||||
})
|
|
||||||
.scrollable()
|
|
||||||
.max_h(px(300.))
|
|
||||||
.max_w(px(300.))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,11 +34,9 @@ use ui::{
|
|||||||
WindowExtension,
|
WindowExtension,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::emoji::EmojiPicker;
|
|
||||||
use crate::text::RenderedText;
|
use crate::text::RenderedText;
|
||||||
|
|
||||||
mod actions;
|
mod actions;
|
||||||
mod emoji;
|
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||||
@@ -87,28 +85,40 @@ pub struct ChatPanel {
|
|||||||
|
|
||||||
impl ChatPanel {
|
impl ChatPanel {
|
||||||
pub fn new(room: WeakEntity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(room: WeakEntity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
// Get room id and name
|
||||||
|
let (id, name) = room
|
||||||
|
.read_with(cx, |this, _cx| {
|
||||||
|
let id = this.id.to_string().into();
|
||||||
|
let name = this.display_name(cx);
|
||||||
|
|
||||||
|
(id, name)
|
||||||
|
})
|
||||||
|
.unwrap_or(("Unknown".into(), "Message...".into()));
|
||||||
|
|
||||||
|
// Define input state
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder("Message...")
|
.placeholder(format!("Message {}", name))
|
||||||
.auto_grow(1, 20)
|
.auto_grow(1, 20)
|
||||||
.prevent_new_line_on_enter()
|
.prevent_new_line_on_enter()
|
||||||
.clean_on_escape()
|
.clean_on_escape()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Define attachments and replies_to entities
|
||||||
let attachments = cx.new(|_| vec![]);
|
let attachments = cx.new(|_| vec![]);
|
||||||
let replies_to = cx.new(|_| HashSet::new());
|
let replies_to = cx.new(|_| HashSet::new());
|
||||||
|
|
||||||
|
// Define list of messages
|
||||||
let messages = BTreeSet::from([Message::system()]);
|
let messages = BTreeSet::from([Message::system()]);
|
||||||
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
||||||
|
|
||||||
let id: SharedString = room
|
|
||||||
.read_with(cx, |this, _cx| this.id.to_string().into())
|
|
||||||
.unwrap_or("Unknown".into());
|
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
if let Ok(connect) = room.read_with(cx, |this, cx| this.connect(cx)) {
|
if let Some(room) = room.upgrade() {
|
||||||
|
let connect = room.read(cx).connect(cx);
|
||||||
|
let get_messages = room.read(cx).get_messages(cx);
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
// Get messaging relays and encryption keys announcement for each member
|
// Get messaging relays and encryption keys announcement for each member
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
@@ -117,9 +127,7 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(get_messages) = room.read_with(cx, |this, cx| this.get_messages(cx)) {
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
// Load all messages belonging to this room
|
// Load all messages belonging to this room
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
@@ -138,9 +146,7 @@ impl ChatPanel {
|
|||||||
.ok();
|
.ok();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(room) = room.upgrade() {
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe to room events
|
// Subscribe to room events
|
||||||
cx.subscribe_in(&room, window, move |this, _room, event, window, cx| {
|
cx.subscribe_in(&room, window, move |this, _room, event, window, cx| {
|
||||||
@@ -158,15 +164,11 @@ impl ChatPanel {
|
|||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Subscribe to input events
|
// Subscribe to input events
|
||||||
cx.subscribe_in(
|
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
|
||||||
&input,
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
window,
|
this.send_input_message(window, cx);
|
||||||
move |this: &mut Self, _input, event, window, cx| {
|
};
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
}),
|
||||||
this.send_message(window, cx);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -209,7 +211,7 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get user input content and merged all attachments
|
/// Get user input content and merged all attachments
|
||||||
fn input_content(&self, cx: &Context<Self>) -> String {
|
fn get_input_value(&self, cx: &Context<Self>) -> String {
|
||||||
// Get input's value
|
// Get input's value
|
||||||
let mut content = self.input.read(cx).value().trim().to_string();
|
let mut content = self.input.read(cx).value().trim().to_string();
|
||||||
|
|
||||||
@@ -233,10 +235,9 @@ impl ChatPanel {
|
|||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a message to all members of the chat
|
fn send_input_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// Get the message which includes all attachments
|
// Get the message which includes all attachments
|
||||||
let content = self.input_content(cx);
|
let content = self.get_input_value(cx);
|
||||||
|
|
||||||
// Return if message is empty
|
// Return if message is empty
|
||||||
if content.trim().is_empty() {
|
if content.trim().is_empty() {
|
||||||
@@ -244,13 +245,18 @@ impl ChatPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.send_message(&content, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a message to all members of the chat
|
||||||
|
fn send_message(&mut self, value: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
// Get replies_to if it's present
|
// Get replies_to if it's present
|
||||||
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
|
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
|
||||||
|
|
||||||
// Get a task to create temporary message for optimistic update
|
// Get a task to create temporary message for optimistic update
|
||||||
let Ok(get_rumor) = self
|
let Ok(get_rumor) = self
|
||||||
.room
|
.room
|
||||||
.read_with(cx, |this, cx| this.create_message(&content, replies, cx))
|
.read_with(cx, |this, cx| this.create_message(value, replies, cx))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -506,15 +512,15 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||||
|
const MSG: &str =
|
||||||
|
"This conversation is private. Only members can see each other's messages.";
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id(ix)
|
.id(ix)
|
||||||
.group("")
|
.h_40()
|
||||||
.h_32()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.relative()
|
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.px_3()
|
.p_3()
|
||||||
.py_2()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_center()
|
.text_center()
|
||||||
@@ -524,12 +530,10 @@ impl ChatPanel {
|
|||||||
.child(
|
.child(
|
||||||
svg()
|
svg()
|
||||||
.path("brand/coop.svg")
|
.path("brand/coop.svg")
|
||||||
.size_10()
|
.size_12()
|
||||||
.text_color(cx.theme().elevated_surface_background),
|
.text_color(cx.theme().ghost_element_active),
|
||||||
)
|
)
|
||||||
.child(SharedString::from(
|
.child(SharedString::from(MSG))
|
||||||
"This conversation is private. Only members can see each other's messages.",
|
|
||||||
))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,6 +1120,25 @@ impl ChatPanel {
|
|||||||
|
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
match command {
|
||||||
|
Command::Insert(content) => {
|
||||||
|
self.send_message(content, window, cx);
|
||||||
|
}
|
||||||
|
Command::ChangeSubject(subject) => {
|
||||||
|
if self
|
||||||
|
.room
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.set_subject(*subject, cx);
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
window.push_notification(Notification::error("Failed to change subject"), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Panel for ChatPanel {
|
impl Panel for ChatPanel {
|
||||||
@@ -1150,6 +1173,7 @@ impl Focusable for ChatPanel {
|
|||||||
impl Render for ChatPanel {
|
impl Render for ChatPanel {
|
||||||
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 {
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.on_action(cx.listener(Self::on_command))
|
||||||
.image_cache(self.image_cache.clone())
|
.image_cache(self.image_cache.clone())
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(
|
.child(
|
||||||
@@ -1163,48 +1187,68 @@ impl Render for ChatPanel {
|
|||||||
.flex_1(),
|
.flex_1(),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
|
.p_2()
|
||||||
.w_full()
|
.w_full()
|
||||||
.relative()
|
.gap_1p5()
|
||||||
.px_3()
|
.children(self.render_attachment_list(window, cx))
|
||||||
.py_2()
|
.children(self.render_reply_list(window, cx))
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
h_flex()
|
||||||
.gap_1p5()
|
.items_end()
|
||||||
.children(self.render_attachment_list(window, cx))
|
|
||||||
.children(self.render_reply_list(window, cx))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
Button::new("upload")
|
||||||
.w_full()
|
.icon(IconName::Plus)
|
||||||
.flex()
|
.tooltip("Upload media")
|
||||||
.items_end()
|
.loading(self.uploading)
|
||||||
.gap_2p5()
|
.disabled(self.uploading)
|
||||||
|
.ghost()
|
||||||
|
.large()
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.upload(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
TextInput::new(&self.input)
|
||||||
|
.appearance(false)
|
||||||
|
.flex_1()
|
||||||
|
.text_sm(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.pl_1()
|
||||||
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
Button::new("emoji")
|
||||||
.gap_1()
|
.icon(IconName::Emoji)
|
||||||
.text_color(cx.theme().text_muted)
|
.ghost()
|
||||||
.child(
|
.large()
|
||||||
Button::new("upload")
|
.popup_menu_with_anchor(
|
||||||
.icon(IconName::Upload)
|
gpui::Corner::BottomLeft,
|
||||||
.loading(self.uploading)
|
move |this, _window, _cx| {
|
||||||
.disabled(self.uploading)
|
this.axis(gpui::Axis::Horizontal)
|
||||||
.ghost()
|
.menu("👍", Box::new(Command::Insert("👍")))
|
||||||
.large()
|
.menu("👎", Box::new(Command::Insert("👎")))
|
||||||
.on_click(cx.listener(
|
.menu("😄", Box::new(Command::Insert("😄")))
|
||||||
move |this, _, window, cx| {
|
.menu("🎉", Box::new(Command::Insert("🎉")))
|
||||||
this.upload(window, cx);
|
.menu("😕", Box::new(Command::Insert("😕")))
|
||||||
},
|
.menu("❤️", Box::new(Command::Insert("❤️")))
|
||||||
)),
|
.menu("🚀", Box::new(Command::Insert("🚀")))
|
||||||
)
|
.menu("👀", Box::new(Command::Insert("👀")))
|
||||||
.child(
|
},
|
||||||
EmojiPicker::new()
|
|
||||||
.target(self.input.downgrade())
|
|
||||||
.icon(IconName::Emoji)
|
|
||||||
.large(),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(TextInput::new(&self.input)),
|
.child(
|
||||||
|
Button::new("send")
|
||||||
|
.icon(IconName::PaperPlaneFill)
|
||||||
|
.disabled(self.uploading)
|
||||||
|
.ghost()
|
||||||
|
.large()
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.send_input_message(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ pub enum IconName {
|
|||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
PanelBottom,
|
PanelBottom,
|
||||||
PanelBottomOpen,
|
PanelBottomOpen,
|
||||||
|
PaperPlaneFill,
|
||||||
Warning,
|
Warning,
|
||||||
WindowClose,
|
WindowClose,
|
||||||
WindowMaximize,
|
WindowMaximize,
|
||||||
@@ -106,6 +107,7 @@ impl IconName {
|
|||||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||||
Self::PanelBottom => "icons/panel-bottom.svg",
|
Self::PanelBottom => "icons/panel-bottom.svg",
|
||||||
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||||
|
Self::PaperPlaneFill => "icons/paper-plane-fill.svg",
|
||||||
Self::Warning => "icons/warning.svg",
|
Self::Warning => "icons/warning.svg",
|
||||||
Self::WindowClose => "icons/window-close.svg",
|
Self::WindowClose => "icons/window-close.svg",
|
||||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, canvas, div, px, rems, Action, AnyElement, App, AppContext, Bounds, Context, Corner,
|
anchored, canvas, div, px, rems, Action, AnyElement, App, AppContext, Axis, Bounds, Context,
|
||||||
DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement,
|
Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half,
|
||||||
IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, Pixels, Render,
|
InteractiveElement, IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement,
|
||||||
ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity,
|
Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||||
Window,
|
WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -119,6 +119,8 @@ pub struct PopupMenu {
|
|||||||
pub(crate) menu_items: Vec<PopupMenuItem>,
|
pub(crate) menu_items: Vec<PopupMenuItem>,
|
||||||
/// The focus handle of Entity to handle actions.
|
/// The focus handle of Entity to handle actions.
|
||||||
pub(crate) action_context: Option<FocusHandle>,
|
pub(crate) action_context: Option<FocusHandle>,
|
||||||
|
|
||||||
|
axis_items: Axis,
|
||||||
has_icon: bool,
|
has_icon: bool,
|
||||||
selected_index: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
min_width: Option<Pixels>,
|
min_width: Option<Pixels>,
|
||||||
@@ -126,13 +128,14 @@ pub struct PopupMenu {
|
|||||||
max_height: Option<Pixels>,
|
max_height: Option<Pixels>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
size: Size,
|
size: Size,
|
||||||
|
|
||||||
/// The parent menu of this menu, if this is a submenu
|
|
||||||
parent_menu: Option<WeakEntity<Self>>,
|
|
||||||
scrollable: bool,
|
scrollable: bool,
|
||||||
external_link_icon: bool,
|
external_link_icon: bool,
|
||||||
scroll_handle: ScrollHandle,
|
scroll_handle: ScrollHandle,
|
||||||
scroll_state: ScrollbarState,
|
scroll_state: ScrollbarState,
|
||||||
|
|
||||||
|
/// The parent menu of this menu, if this is a submenu
|
||||||
|
parent_menu: Option<WeakEntity<Self>>,
|
||||||
|
|
||||||
// This will update on render
|
// This will update on render
|
||||||
submenu_anchor: (Corner, Pixels),
|
submenu_anchor: (Corner, Pixels),
|
||||||
|
|
||||||
@@ -144,6 +147,7 @@ impl PopupMenu {
|
|||||||
Self {
|
Self {
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
action_context: None,
|
action_context: None,
|
||||||
|
axis_items: Axis::Vertical,
|
||||||
parent_menu: None,
|
parent_menu: None,
|
||||||
menu_items: Vec::new(),
|
menu_items: Vec::new(),
|
||||||
selected_index: None,
|
selected_index: None,
|
||||||
@@ -180,6 +184,12 @@ impl PopupMenu {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the axis of the popup menu, default is vertical
|
||||||
|
pub fn axis(mut self, axis: Axis) -> Self {
|
||||||
|
self.axis_items = axis;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set min width of the popup menu, default is 120px
|
/// Set min width of the popup menu, default is 120px
|
||||||
pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
|
pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
|
||||||
self.min_width = Some(width.into());
|
self.min_width = Some(width.into());
|
||||||
@@ -1131,8 +1141,15 @@ impl Render for PopupMenu {
|
|||||||
.text_color(cx.theme().text)
|
.text_color(cx.theme().text)
|
||||||
.relative()
|
.relative()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
div()
|
||||||
.id("items")
|
.id("items")
|
||||||
|
.map(|this| {
|
||||||
|
if self.axis_items == Axis::Vertical {
|
||||||
|
this.flex().flex_col()
|
||||||
|
} else {
|
||||||
|
this.flex().flex_row().items_center()
|
||||||
|
}
|
||||||
|
})
|
||||||
.p_1()
|
.p_1()
|
||||||
.gap_y_0p5()
|
.gap_y_0p5()
|
||||||
.min_w(rems(8.))
|
.min_w(rems(8.))
|
||||||
|
|||||||
Reference in New Issue
Block a user