Mainline GPUI2 UI work (#3062)
This PR mainlines the current state of new GPUI2-based UI from the `gpui2-ui` branch. Release Notes: - N/A --------- Co-authored-by: Nate Butler <iamnbutler@gmail.com> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Nate <nate@zed.dev> Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
e7ee8a95f6
commit
f26ca0866c
85 changed files with 4658 additions and 1623 deletions
|
@ -1,6 +1,5 @@
|
|||
use gpui2::elements::img;
|
||||
use gpui2::style::StyleHelpers;
|
||||
use gpui2::{ArcCow, Element, IntoElement, ViewContext};
|
||||
use gpui2::ArcCow;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme;
|
||||
|
@ -11,14 +10,14 @@ pub struct Avatar {
|
|||
shape: Shape,
|
||||
}
|
||||
|
||||
pub fn avatar(src: impl Into<ArcCow<'static, str>>) -> Avatar {
|
||||
Avatar {
|
||||
src: src.into(),
|
||||
shape: Shape::Circle,
|
||||
}
|
||||
}
|
||||
|
||||
impl Avatar {
|
||||
pub fn new(src: impl Into<ArcCow<'static, str>>) -> Self {
|
||||
Self {
|
||||
src: src.into(),
|
||||
shape: Shape::Circle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shape(mut self, shape: Shape) -> Self {
|
||||
self.shape = shape;
|
||||
self
|
||||
|
|
203
crates/ui/src/elements/button.rs
Normal file
203
crates/ui/src/elements/button.rs
Normal file
|
@ -0,0 +1,203 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use gpui2::geometry::DefiniteLength;
|
||||
use gpui2::platform::MouseButton;
|
||||
use gpui2::{EventContext, Hsla, Interactive, WindowContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, theme, Icon, IconColor, IconElement, Label, LabelColor, LabelSize};
|
||||
|
||||
#[derive(Default, PartialEq, Clone, Copy)]
|
||||
pub enum IconPosition {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, PartialEq)]
|
||||
pub enum ButtonVariant {
|
||||
#[default]
|
||||
Ghost,
|
||||
Filled,
|
||||
}
|
||||
|
||||
struct ButtonHandlers<V> {
|
||||
click: Option<Rc<dyn Fn(&mut V, &mut EventContext<V>)>>,
|
||||
}
|
||||
|
||||
impl<V> Default for ButtonHandlers<V> {
|
||||
fn default() -> Self {
|
||||
Self { click: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct Button<V: 'static> {
|
||||
label: String,
|
||||
variant: ButtonVariant,
|
||||
state: InteractionState,
|
||||
icon: Option<Icon>,
|
||||
icon_position: Option<IconPosition>,
|
||||
width: Option<DefiniteLength>,
|
||||
handlers: ButtonHandlers<V>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Button<V> {
|
||||
pub fn new<L>(label: L) -> Self
|
||||
where
|
||||
L: Into<String>,
|
||||
{
|
||||
Self {
|
||||
label: label.into(),
|
||||
variant: Default::default(),
|
||||
state: Default::default(),
|
||||
icon: None,
|
||||
icon_position: None,
|
||||
width: Default::default(),
|
||||
handlers: ButtonHandlers::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ghost<L>(label: L) -> Self
|
||||
where
|
||||
L: Into<String>,
|
||||
{
|
||||
Self::new(label).variant(ButtonVariant::Ghost)
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: InteractionState) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: Icon) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
|
||||
if self.icon.is_none() {
|
||||
panic!("An icon must be present if an icon_position is provided.");
|
||||
}
|
||||
self.icon_position = Some(icon_position);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl Fn(&mut V, &mut EventContext<V>) + 'static) -> Self {
|
||||
self.handlers.click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
fn background_color(&self, cx: &mut ViewContext<V>) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
let system_color = SystemColor::new();
|
||||
|
||||
match (self.variant, self.state) {
|
||||
(ButtonVariant::Ghost, InteractionState::Hovered) => {
|
||||
theme.lowest.base.hovered.background
|
||||
}
|
||||
(ButtonVariant::Ghost, InteractionState::Active) => {
|
||||
theme.lowest.base.pressed.background
|
||||
}
|
||||
(ButtonVariant::Filled, InteractionState::Enabled) => {
|
||||
theme.lowest.on.default.background
|
||||
}
|
||||
(ButtonVariant::Filled, InteractionState::Hovered) => {
|
||||
theme.lowest.on.hovered.background
|
||||
}
|
||||
(ButtonVariant::Filled, InteractionState::Active) => theme.lowest.on.pressed.background,
|
||||
(ButtonVariant::Filled, InteractionState::Disabled) => {
|
||||
theme.lowest.on.disabled.background
|
||||
}
|
||||
_ => system_color.transparent,
|
||||
}
|
||||
}
|
||||
|
||||
fn label_color(&self) -> LabelColor {
|
||||
match self.state {
|
||||
InteractionState::Disabled => LabelColor::Disabled,
|
||||
_ => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_color(&self) -> IconColor {
|
||||
match self.state {
|
||||
InteractionState::Disabled => IconColor::Disabled,
|
||||
_ => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn border_color(&self, cx: &WindowContext) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
let system_color = SystemColor::new();
|
||||
|
||||
match self.state {
|
||||
InteractionState::Focused => theme.lowest.accent.default.border,
|
||||
_ => system_color.transparent,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_label(&self) -> Label {
|
||||
Label::new(self.label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(self.label_color())
|
||||
}
|
||||
|
||||
fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
|
||||
self.icon.map(|i| IconElement::new(i).color(icon_color))
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
let theme = theme(cx);
|
||||
let icon_color = self.icon_color();
|
||||
let system_color = SystemColor::new();
|
||||
let border_color = self.border_color(cx);
|
||||
|
||||
let mut el = h_stack()
|
||||
.h_6()
|
||||
.px_1()
|
||||
.items_center()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(border_color)
|
||||
.fill(self.background_color(cx));
|
||||
|
||||
match (self.icon, self.icon_position) {
|
||||
(Some(_), Some(IconPosition::Left)) => {
|
||||
el = el
|
||||
.gap_1()
|
||||
.child(self.render_label())
|
||||
.children(self.render_icon(icon_color))
|
||||
}
|
||||
(Some(_), Some(IconPosition::Right)) => {
|
||||
el = el
|
||||
.gap_1()
|
||||
.children(self.render_icon(icon_color))
|
||||
.child(self.render_label())
|
||||
}
|
||||
(_, _) => el = el.child(self.render_label()),
|
||||
}
|
||||
|
||||
if let Some(width) = self.width {
|
||||
el = el.w(width).justify_center();
|
||||
}
|
||||
|
||||
if let Some(click_handler) = self.handlers.click.clone() {
|
||||
el = el.on_mouse_down(MouseButton::Left, move |view, event, cx| {
|
||||
click_handler(view, cx);
|
||||
});
|
||||
}
|
||||
|
||||
el
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
use gpui2::elements::div;
|
||||
use gpui2::style::StyleHelpers;
|
||||
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme;
|
||||
|
||||
#[derive(Element, Clone)]
|
||||
|
@ -10,11 +7,11 @@ pub struct Details {
|
|||
meta: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub fn details(text: &'static str) -> Details {
|
||||
Details { text, meta: None }
|
||||
}
|
||||
|
||||
impl Details {
|
||||
pub fn new(text: &'static str) -> Self {
|
||||
Self { text, meta: None }
|
||||
}
|
||||
|
||||
pub fn meta_text(mut self, meta: &'static str) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use gpui2::elements::svg;
|
||||
use gpui2::Hsla;
|
||||
use strum::EnumIter;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::theme;
|
||||
use crate::Theme;
|
||||
use gpui2::elements::svg;
|
||||
use gpui2::style::StyleHelpers;
|
||||
use gpui2::{Element, ViewContext};
|
||||
use gpui2::{Hsla, IntoElement};
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum IconSize {
|
||||
Small,
|
||||
#[default]
|
||||
Large,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum IconColor {
|
||||
|
@ -37,8 +45,8 @@ impl IconColor {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum IconAsset {
|
||||
#[derive(Default, PartialEq, Copy, Clone, EnumIter)]
|
||||
pub enum Icon {
|
||||
Ai,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
|
@ -53,6 +61,7 @@ pub enum IconAsset {
|
|||
Close,
|
||||
ExclamationTriangle,
|
||||
File,
|
||||
FileGeneric,
|
||||
FileDoc,
|
||||
FileGit,
|
||||
FileLock,
|
||||
|
@ -67,89 +76,106 @@ pub enum IconAsset {
|
|||
InlayHint,
|
||||
MagicWand,
|
||||
MagnifyingGlass,
|
||||
Maximize,
|
||||
Menu,
|
||||
MessageBubbles,
|
||||
Mic,
|
||||
MicMute,
|
||||
Plus,
|
||||
Quote,
|
||||
Screen,
|
||||
Split,
|
||||
SplitMessage,
|
||||
Terminal,
|
||||
XCircle,
|
||||
Copilot,
|
||||
Envelope,
|
||||
}
|
||||
|
||||
impl IconAsset {
|
||||
impl Icon {
|
||||
pub fn path(self) -> &'static str {
|
||||
match self {
|
||||
IconAsset::Ai => "icons/ai.svg",
|
||||
IconAsset::ArrowLeft => "icons/arrow_left.svg",
|
||||
IconAsset::ArrowRight => "icons/arrow_right.svg",
|
||||
IconAsset::ArrowUpRight => "icons/arrow_up_right.svg",
|
||||
IconAsset::AudioOff => "icons/speaker-off.svg",
|
||||
IconAsset::AudioOn => "icons/speaker-loud.svg",
|
||||
IconAsset::Bolt => "icons/bolt.svg",
|
||||
IconAsset::ChevronDown => "icons/chevron_down.svg",
|
||||
IconAsset::ChevronLeft => "icons/chevron_left.svg",
|
||||
IconAsset::ChevronRight => "icons/chevron_right.svg",
|
||||
IconAsset::ChevronUp => "icons/chevron_up.svg",
|
||||
IconAsset::Close => "icons/x.svg",
|
||||
IconAsset::ExclamationTriangle => "icons/warning.svg",
|
||||
IconAsset::File => "icons/file_icons/file.svg",
|
||||
IconAsset::FileDoc => "icons/file_icons/book.svg",
|
||||
IconAsset::FileGit => "icons/file_icons/git.svg",
|
||||
IconAsset::FileLock => "icons/file_icons/lock.svg",
|
||||
IconAsset::FileRust => "icons/file_icons/rust.svg",
|
||||
IconAsset::FileToml => "icons/file_icons/toml.svg",
|
||||
IconAsset::FileTree => "icons/project.svg",
|
||||
IconAsset::Folder => "icons/file_icons/folder.svg",
|
||||
IconAsset::FolderOpen => "icons/file_icons/folder_open.svg",
|
||||
IconAsset::FolderX => "icons/stop_sharing.svg",
|
||||
IconAsset::Hash => "icons/hash.svg",
|
||||
IconAsset::InlayHint => "icons/inlay_hint.svg",
|
||||
IconAsset::MagicWand => "icons/magic-wand.svg",
|
||||
IconAsset::MagnifyingGlass => "icons/magnifying_glass.svg",
|
||||
IconAsset::MessageBubbles => "icons/conversations.svg",
|
||||
IconAsset::Mic => "icons/mic.svg",
|
||||
IconAsset::MicMute => "icons/mic-mute.svg",
|
||||
IconAsset::Plus => "icons/plus.svg",
|
||||
IconAsset::Screen => "icons/desktop.svg",
|
||||
IconAsset::Split => "icons/split.svg",
|
||||
IconAsset::Terminal => "icons/terminal.svg",
|
||||
IconAsset::XCircle => "icons/error.svg",
|
||||
IconAsset::Copilot => "icons/copilot.svg",
|
||||
IconAsset::Envelope => "icons/feedback.svg",
|
||||
Icon::Ai => "icons/ai.svg",
|
||||
Icon::ArrowLeft => "icons/arrow_left.svg",
|
||||
Icon::ArrowRight => "icons/arrow_right.svg",
|
||||
Icon::ArrowUpRight => "icons/arrow_up_right.svg",
|
||||
Icon::AudioOff => "icons/speaker-off.svg",
|
||||
Icon::AudioOn => "icons/speaker-loud.svg",
|
||||
Icon::Bolt => "icons/bolt.svg",
|
||||
Icon::ChevronDown => "icons/chevron_down.svg",
|
||||
Icon::ChevronLeft => "icons/chevron_left.svg",
|
||||
Icon::ChevronRight => "icons/chevron_right.svg",
|
||||
Icon::ChevronUp => "icons/chevron_up.svg",
|
||||
Icon::Close => "icons/x.svg",
|
||||
Icon::ExclamationTriangle => "icons/warning.svg",
|
||||
Icon::File => "icons/file.svg",
|
||||
Icon::FileGeneric => "icons/file_icons/file.svg",
|
||||
Icon::FileDoc => "icons/file_icons/book.svg",
|
||||
Icon::FileGit => "icons/file_icons/git.svg",
|
||||
Icon::FileLock => "icons/file_icons/lock.svg",
|
||||
Icon::FileRust => "icons/file_icons/rust.svg",
|
||||
Icon::FileToml => "icons/file_icons/toml.svg",
|
||||
Icon::FileTree => "icons/project.svg",
|
||||
Icon::Folder => "icons/file_icons/folder.svg",
|
||||
Icon::FolderOpen => "icons/file_icons/folder_open.svg",
|
||||
Icon::FolderX => "icons/stop_sharing.svg",
|
||||
Icon::Hash => "icons/hash.svg",
|
||||
Icon::InlayHint => "icons/inlay_hint.svg",
|
||||
Icon::MagicWand => "icons/magic-wand.svg",
|
||||
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
|
||||
Icon::Maximize => "icons/maximize.svg",
|
||||
Icon::Menu => "icons/menu.svg",
|
||||
Icon::MessageBubbles => "icons/conversations.svg",
|
||||
Icon::Mic => "icons/mic.svg",
|
||||
Icon::MicMute => "icons/mic-mute.svg",
|
||||
Icon::Plus => "icons/plus.svg",
|
||||
Icon::Quote => "icons/quote.svg",
|
||||
Icon::Screen => "icons/desktop.svg",
|
||||
Icon::Split => "icons/split.svg",
|
||||
Icon::SplitMessage => "icons/split_message.svg",
|
||||
Icon::Terminal => "icons/terminal.svg",
|
||||
Icon::XCircle => "icons/error.svg",
|
||||
Icon::Copilot => "icons/copilot.svg",
|
||||
Icon::Envelope => "icons/feedback.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Element, Clone)]
|
||||
pub struct Icon {
|
||||
asset: IconAsset,
|
||||
pub struct IconElement {
|
||||
icon: Icon,
|
||||
color: IconColor,
|
||||
size: IconSize,
|
||||
}
|
||||
|
||||
pub fn icon(asset: IconAsset) -> Icon {
|
||||
Icon {
|
||||
asset,
|
||||
color: IconColor::default(),
|
||||
impl IconElement {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
Self {
|
||||
icon,
|
||||
color: IconColor::default(),
|
||||
size: IconSize::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn color(mut self, color: IconColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: IconSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
let theme = theme(cx);
|
||||
let fill = self.color.color(theme);
|
||||
|
||||
svg()
|
||||
.flex_none()
|
||||
.path(self.asset.path())
|
||||
.size_4()
|
||||
.fill(fill)
|
||||
let sized_svg = match self.size {
|
||||
IconSize::Small => svg().size_3p5(),
|
||||
IconSize::Large => svg().size_4(),
|
||||
};
|
||||
|
||||
sized_svg.flex_none().path(self.icon.path()).fill(fill)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
use gpui2::elements::div;
|
||||
use gpui2::style::StyleHelpers;
|
||||
use gpui2::{Element, IntoElement, ViewContext};
|
||||
|
||||
use crate::theme;
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct Indicator {
|
||||
player: usize,
|
||||
}
|
||||
|
||||
pub fn indicator() -> Indicator {
|
||||
Indicator { player: 0 }
|
||||
}
|
||||
|
||||
impl Indicator {
|
||||
pub fn player(mut self, player: usize) -> Self {
|
||||
self.player = player;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
let theme = theme(cx);
|
||||
let player_color = theme.players[self.player].cursor;
|
||||
|
||||
div()
|
||||
.w_4()
|
||||
.h_1()
|
||||
.rounded_bl_sm()
|
||||
.rounded_br_sm()
|
||||
.fill(player_color)
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
use gpui2::elements::div;
|
||||
use gpui2::style::{StyleHelpers, Styleable};
|
||||
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme;
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub enum InputVariant {
|
||||
#[default]
|
||||
Ghost,
|
||||
Filled,
|
||||
}
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct Input {
|
||||
placeholder: &'static str,
|
||||
|
@ -13,24 +16,26 @@ pub struct Input {
|
|||
variant: InputVariant,
|
||||
}
|
||||
|
||||
pub fn input(placeholder: &'static str) -> Input {
|
||||
Input {
|
||||
placeholder,
|
||||
value: "".to_string(),
|
||||
state: InteractionState::default(),
|
||||
variant: InputVariant::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn new(placeholder: &'static str) -> Self {
|
||||
Self {
|
||||
placeholder,
|
||||
value: "".to_string(),
|
||||
state: InteractionState::default(),
|
||||
variant: InputVariant::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(mut self, value: String) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: InteractionState) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: InputVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use gpui2::{Hsla, WindowContext};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::theme;
|
||||
use gpui2::elements::div;
|
||||
use gpui2::style::StyleHelpers;
|
||||
use gpui2::{Element, ViewContext};
|
||||
use gpui2::{IntoElement, ParentElement};
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LabelColor {
|
||||
|
@ -12,8 +12,28 @@ pub enum LabelColor {
|
|||
Created,
|
||||
Modified,
|
||||
Deleted,
|
||||
Disabled,
|
||||
Hidden,
|
||||
Placeholder,
|
||||
Accent,
|
||||
}
|
||||
|
||||
impl LabelColor {
|
||||
pub fn hsla(&self, cx: &WindowContext) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
|
||||
match self {
|
||||
Self::Default => theme.middle.base.default.foreground,
|
||||
Self::Muted => theme.middle.variant.default.foreground,
|
||||
Self::Created => theme.middle.positive.default.foreground,
|
||||
Self::Modified => theme.middle.warning.default.foreground,
|
||||
Self::Deleted => theme.middle.negative.default.foreground,
|
||||
Self::Disabled => theme.middle.base.disabled.foreground,
|
||||
Self::Hidden => theme.middle.variant.default.foreground,
|
||||
Self::Placeholder => theme.middle.base.disabled.foreground,
|
||||
Self::Accent => theme.middle.accent.default.foreground,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
|
@ -25,20 +45,27 @@ pub enum LabelSize {
|
|||
|
||||
#[derive(Element, Clone)]
|
||||
pub struct Label {
|
||||
label: &'static str,
|
||||
label: String,
|
||||
color: LabelColor,
|
||||
size: LabelSize,
|
||||
}
|
||||
|
||||
pub fn label(label: &'static str) -> Label {
|
||||
Label {
|
||||
label,
|
||||
color: LabelColor::Default,
|
||||
size: LabelSize::Default,
|
||||
}
|
||||
highlight_indices: Vec<usize>,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new<L>(label: L) -> Self
|
||||
where
|
||||
L: Into<String>,
|
||||
{
|
||||
Self {
|
||||
label: label.into(),
|
||||
color: LabelColor::Default,
|
||||
size: LabelSize::Default,
|
||||
highlight_indices: Vec::new(),
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: LabelColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
|
@ -49,27 +76,86 @@ impl Label {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
|
||||
self.highlight_indices = indices;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
let theme = theme(cx);
|
||||
|
||||
let color = match self.color {
|
||||
LabelColor::Default => theme.lowest.base.default.foreground,
|
||||
LabelColor::Muted => theme.lowest.variant.default.foreground,
|
||||
LabelColor::Created => theme.lowest.positive.default.foreground,
|
||||
LabelColor::Modified => theme.lowest.warning.default.foreground,
|
||||
LabelColor::Deleted => theme.lowest.negative.default.foreground,
|
||||
LabelColor::Hidden => theme.lowest.variant.default.foreground,
|
||||
LabelColor::Placeholder => theme.lowest.base.disabled.foreground,
|
||||
};
|
||||
let highlight_color = theme.lowest.accent.default.foreground;
|
||||
|
||||
let mut div = div();
|
||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||
|
||||
if self.size == LabelSize::Small {
|
||||
div = div.text_xs();
|
||||
} else {
|
||||
div = div.text_sm();
|
||||
let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
|
||||
|
||||
for (char_ix, char) in self.label.char_indices() {
|
||||
let mut color = self.color.hsla(cx);
|
||||
|
||||
if let Some(highlight_ix) = highlight_indices.peek() {
|
||||
if char_ix == *highlight_ix {
|
||||
color = highlight_color;
|
||||
|
||||
highlight_indices.next();
|
||||
}
|
||||
}
|
||||
|
||||
let last_run = runs.last_mut();
|
||||
|
||||
let start_new_run = if let Some(last_run) = last_run {
|
||||
if color == last_run.color {
|
||||
last_run.text.push(char);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if start_new_run {
|
||||
runs.push(Run {
|
||||
text: char.to_string(),
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
div.text_color(color).child(self.label.clone())
|
||||
div()
|
||||
.flex()
|
||||
.when(self.strikethrough, |this| {
|
||||
this.relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_px()
|
||||
.my_auto()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.fill(LabelColor::Hidden.hsla(cx)),
|
||||
)
|
||||
})
|
||||
.children(runs.into_iter().map(|run| {
|
||||
let mut div = div();
|
||||
|
||||
if self.size == LabelSize::Small {
|
||||
div = div.text_xs();
|
||||
} else {
|
||||
div = div.text_sm();
|
||||
}
|
||||
|
||||
div.text_color(run.color).child(run.text)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// A run of text that receives the same style.
|
||||
struct Run {
|
||||
pub text: String,
|
||||
pub color: Hsla,
|
||||
}
|
||||
|
|
132
crates/ui/src/elements/player.rs
Normal file
132
crates/ui/src/elements/player.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use gpui2::{Hsla, ViewContext};
|
||||
|
||||
use crate::theme;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum PlayerStatus {
|
||||
#[default]
|
||||
Offline,
|
||||
Online,
|
||||
InCall,
|
||||
Away,
|
||||
DoNotDisturb,
|
||||
Invisible,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum MicStatus {
|
||||
Muted,
|
||||
#[default]
|
||||
Unmuted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum VideoStatus {
|
||||
On,
|
||||
#[default]
|
||||
Off,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum ScreenShareStatus {
|
||||
Shared,
|
||||
#[default]
|
||||
NotShared,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PlayerCallStatus {
|
||||
pub mic_status: MicStatus,
|
||||
/// Indicates if the player is currently speaking
|
||||
/// And the intensity of the volume coming through
|
||||
///
|
||||
/// 0.0 - 1.0
|
||||
pub voice_activity: f32,
|
||||
pub video_status: VideoStatus,
|
||||
pub screen_share_status: ScreenShareStatus,
|
||||
pub in_current_project: bool,
|
||||
pub disconnected: bool,
|
||||
pub following: Option<Vec<Player>>,
|
||||
pub followers: Option<Vec<Player>>,
|
||||
}
|
||||
|
||||
impl PlayerCallStatus {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
mic_status: MicStatus::default(),
|
||||
voice_activity: 0.,
|
||||
video_status: VideoStatus::default(),
|
||||
screen_share_status: ScreenShareStatus::default(),
|
||||
in_current_project: true,
|
||||
disconnected: false,
|
||||
following: None,
|
||||
followers: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Player {
|
||||
index: usize,
|
||||
avatar_src: String,
|
||||
username: String,
|
||||
status: PlayerStatus,
|
||||
}
|
||||
|
||||
pub struct PlayerWithCallStatus {
|
||||
player: Player,
|
||||
call_status: PlayerCallStatus,
|
||||
}
|
||||
|
||||
impl PlayerWithCallStatus {
|
||||
pub fn new(player: Player, call_status: PlayerCallStatus) -> Self {
|
||||
Self {
|
||||
player,
|
||||
call_status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_player(&self) -> &Player {
|
||||
&self.player
|
||||
}
|
||||
|
||||
pub fn get_call_status(&self) -> &PlayerCallStatus {
|
||||
&self.call_status
|
||||
}
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new(index: usize, avatar_src: String, username: String) -> Self {
|
||||
Self {
|
||||
index,
|
||||
avatar_src,
|
||||
username,
|
||||
status: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_status(mut self, status: PlayerStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cursor_color<V>(&self, cx: &mut ViewContext<V>) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
let index = self.index % 8;
|
||||
theme.players[self.index].cursor
|
||||
}
|
||||
|
||||
pub fn selection_color<V>(&self, cx: &mut ViewContext<V>) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
let index = self.index % 8;
|
||||
theme.players[self.index].selection
|
||||
}
|
||||
|
||||
pub fn avatar_src(&self) -> &str {
|
||||
&self.avatar_src
|
||||
}
|
||||
|
||||
pub fn index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
}
|
31
crates/ui/src/elements/stack.rs
Normal file
31
crates/ui/src/elements/stack.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use gpui2::elements::div::Div;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub trait Stack: StyleHelpers {
|
||||
/// Horizontally stacks elements.
|
||||
fn h_stack(self) -> Self {
|
||||
self.flex().flex_row().items_center()
|
||||
}
|
||||
|
||||
/// Vertically stacks elements.
|
||||
fn v_stack(self) -> Self {
|
||||
self.flex().flex_col()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Stack for Div<V> {}
|
||||
|
||||
/// Horizontally stacks elements.
|
||||
///
|
||||
/// Sets `flex()`, `flex_row()`, `items_center()`
|
||||
pub fn h_stack<V: 'static>() -> Div<V> {
|
||||
div().h_stack()
|
||||
}
|
||||
|
||||
/// Vertically stacks elements.
|
||||
///
|
||||
/// Sets `flex()`, `flex_col()`
|
||||
pub fn v_stack<V: 'static>() -> Div<V> {
|
||||
div().v_stack()
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
use gpui2::elements::div;
|
||||
use gpui2::style::{StyleHelpers, Styleable};
|
||||
use gpui2::{Element, IntoElement, ParentElement, ViewContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme;
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct TextButton {
|
||||
label: &'static str,
|
||||
variant: ButtonVariant,
|
||||
state: InteractionState,
|
||||
}
|
||||
|
||||
pub fn text_button(label: &'static str) -> TextButton {
|
||||
TextButton {
|
||||
label,
|
||||
variant: ButtonVariant::default(),
|
||||
state: InteractionState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl TextButton {
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: InteractionState) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
let theme = theme(cx);
|
||||
|
||||
let text_color_default;
|
||||
let text_color_hover;
|
||||
let text_color_active;
|
||||
|
||||
let background_color_default;
|
||||
let background_color_hover;
|
||||
let background_color_active;
|
||||
|
||||
let div = div();
|
||||
|
||||
match self.variant {
|
||||
ButtonVariant::Ghost => {
|
||||
text_color_default = theme.lowest.base.default.foreground;
|
||||
text_color_hover = theme.lowest.base.hovered.foreground;
|
||||
text_color_active = theme.lowest.base.pressed.foreground;
|
||||
background_color_default = theme.lowest.base.default.background;
|
||||
background_color_hover = theme.lowest.base.hovered.background;
|
||||
background_color_active = theme.lowest.base.pressed.background;
|
||||
}
|
||||
ButtonVariant::Filled => {
|
||||
text_color_default = theme.lowest.base.default.foreground;
|
||||
text_color_hover = theme.lowest.base.hovered.foreground;
|
||||
text_color_active = theme.lowest.base.pressed.foreground;
|
||||
background_color_default = theme.lowest.on.default.background;
|
||||
background_color_hover = theme.lowest.on.hovered.background;
|
||||
background_color_active = theme.lowest.on.pressed.background;
|
||||
}
|
||||
};
|
||||
div.h_6()
|
||||
.px_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_md()
|
||||
.text_xs()
|
||||
.text_color(text_color_default)
|
||||
.fill(background_color_default)
|
||||
.hover()
|
||||
.text_color(text_color_hover)
|
||||
.fill(background_color_hover)
|
||||
.active()
|
||||
.text_color(text_color_active)
|
||||
.fill(background_color_active)
|
||||
.child(self.label.clone())
|
||||
}
|
||||
}
|
|
@ -1,17 +1,14 @@
|
|||
use gpui2::elements::div;
|
||||
use gpui2::style::StyleHelpers;
|
||||
use gpui2::{Element, IntoElement, ViewContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme;
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct ToolDivider {}
|
||||
|
||||
pub fn tool_divider<V: 'static>() -> impl Element<V> {
|
||||
ToolDivider {}
|
||||
}
|
||||
|
||||
impl ToolDivider {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
let theme = theme(cx);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue