From c74791aac86c6b1c4b843ea44bdece75c8d1b6e1 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 25 Feb 2025 15:32:59 +0800 Subject: [PATCH] description_list: Add `DescriptionList`. (#658) image image image --- Cargo.lock | 101 +++--- crates/story/examples/description_list.rs | 177 +++++++++++ crates/ui/src/description_list.rs | 368 ++++++++++++++++++++++ crates/ui/src/lib.rs | 1 + crates/ui/src/styled.rs | 16 +- crates/ui/src/text/html.rs | 2 +- crates/ui/src/text/markdown.rs | 2 +- crates/ui/src/theme.rs | 11 + 8 files changed, 628 insertions(+), 50 deletions(-) create mode 100644 crates/story/examples/description_list.rs create mode 100644 crates/ui/src/description_list.rs diff --git a/Cargo.lock b/Cargo.lock index aaedcf53..775efe39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,7 +181,7 @@ dependencies = [ "serde", "serde_repr", "url", - "zbus 5.1.1", + "zbus 5.5.0", ] [[package]] @@ -228,7 +228,7 @@ dependencies = [ "async-task", "concurrent-queue", "fastrand 2.1.1", - "futures-lite 2.4.0", + "futures-lite 2.6.0", "slab", ] @@ -252,7 +252,7 @@ checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock 3.4.0", "blocking", - "futures-lite 2.4.0", + "futures-lite 2.6.0", ] [[package]] @@ -277,15 +277,15 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.4.0", + "futures-lite 2.6.0", "parking", "polling 3.7.3", "rustix 0.38.38", @@ -331,9 +331,9 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ - "async-io 2.3.4", + "async-io 2.4.0", "blocking", - "futures-lite 2.4.0", + "futures-lite 2.6.0", ] [[package]] @@ -360,14 +360,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ "async-channel 2.3.1", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "async-signal", "async-task", "blocking", "cfg-if", "event-listener 5.3.1", - "futures-lite 2.4.0", + "futures-lite 2.6.0", "rustix 0.38.38", "tracing", ] @@ -389,7 +389,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "atomic-waker", "cfg-if", @@ -527,7 +527,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.11.0", "log", "prettyplease", "proc-macro2", @@ -684,7 +684,7 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.4.0", + "futures-lite 2.6.0", "piper", ] @@ -1381,9 +1381,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", "syn 2.0.96", @@ -2082,9 +2082,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand 2.1.1", "futures-core", @@ -3915,7 +3915,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 3.2.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.96", @@ -4170,7 +4170,7 @@ source = "git+https://github.com/zed-industries/oo7?branch=avoid-crypto-panic#9d dependencies = [ "aes", "async-fs 2.1.2", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "async-net 2.0.0", "blocking", @@ -4178,7 +4178,7 @@ dependencies = [ "cipher", "digest", "endi", - "futures-lite 2.4.0", + "futures-lite 2.6.0", "futures-util", "hkdf", "hmac", @@ -5893,12 +5893,12 @@ dependencies = [ "async-channel 2.3.1", "async-executor", "async-fs 2.1.2", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "async-net 2.0.0", "async-process 2.3.0", "blocking", - "futures-lite 2.4.0", + "futures-lite 2.6.0", ] [[package]] @@ -7646,6 +7646,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" @@ -7872,7 +7881,7 @@ dependencies = [ "async-broadcast", "async-executor", "async-fs 2.1.2", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "async-process 2.3.0", "async-recursion", @@ -7903,14 +7912,14 @@ dependencies = [ [[package]] name = "zbus" -version = "5.1.1" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" dependencies = [ "async-broadcast", "async-executor", "async-fs 2.1.2", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "async-process 2.3.0", "async-recursion", @@ -7920,7 +7929,7 @@ dependencies = [ "enumflags2", "event-listener 5.3.1", "futures-core", - "futures-util", + "futures-lite 2.6.0", "hex", "nix", "ordered-stream", @@ -7930,11 +7939,11 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow 0.6.20", + "winnow 0.7.3", "xdg-home", - "zbus_macros 5.1.1", + "zbus_macros 5.5.0", "zbus_names 4.1.0", - "zvariant 5.1.0", + "zvariant 5.4.0", ] [[package]] @@ -7952,17 +7961,17 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.1.1" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", "syn 2.0.96", "zbus_names 4.1.0", - "zvariant 5.1.0", - "zvariant_utils 3.0.2", + "zvariant 5.4.0", + "zvariant_utils 3.2.0", ] [[package]] @@ -7985,7 +7994,7 @@ dependencies = [ "serde", "static_assertions", "winnow 0.6.20", - "zvariant 5.1.0", + "zvariant 5.4.0", ] [[package]] @@ -8074,18 +8083,18 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.1.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "url", - "winnow 0.6.20", - "zvariant_derive 5.1.0", - "zvariant_utils 3.0.2", + "winnow 0.7.3", + "zvariant_derive 5.4.0", + "zvariant_utils 3.2.0", ] [[package]] @@ -8103,15 +8112,15 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.1.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", "syn 2.0.96", - "zvariant_utils 3.0.2", + "zvariant_utils 3.2.0", ] [[package]] @@ -8127,14 +8136,14 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.0.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", "serde", "static_assertions", "syn 2.0.96", - "winnow 0.6.20", + "winnow 0.7.3", ] diff --git a/crates/story/examples/description_list.rs b/crates/story/examples/description_list.rs new file mode 100644 index 00000000..b77284d5 --- /dev/null +++ b/crates/story/examples/description_list.rs @@ -0,0 +1,177 @@ +use gpui::*; +use gpui_component::{ + button::Button, + checkbox::Checkbox, + description_list::{DescriptionItem, DescriptionList}, + h_flex, + popup_menu::PopupMenuExt as _, + text::TextView, + v_flex, AxisExt, Sizable as _, Size, +}; +use serde::Deserialize; +use story::Assets; + +#[derive(Clone, PartialEq, Eq, Deserialize)] +struct ChangeSize(Size); + +impl_internal_actions!(example, [ChangeSize]); + +pub struct Example { + layout: Axis, + bordered: bool, + size: Size, + items: Vec<(&'static str, &'static str, usize)>, +} + +impl Example { + pub fn new(_: &mut Window, _: &mut Context) -> Self { + let items = vec![ + ("Name", "GPUI Component", 1), + ( + "Description", + "UI components for building fantastic desktop application by using [GPUI](https://gpui.rs).\ + \n\n \ + Contains a lot of useful UI components, such as **Button**, **TextInput**, **Table**, **List**, **Dropdown**, **DatePicker** ... \ + \n\n \ + You can easily create your native desktop application by using GPUI Component. + ", + 3, + ), + ("Version", "0.1.0", 1), + ("License", "Apache-2.0", 1), + ("Author", "Longbridge", 1), + ("--", "--", 1), + ( + "Repository", + "https://github.com/longbridge/gpui-component", + 2, + ), + ( + "Category", + "UI, Desktop, Framework", + 1, + ), + ( + "Platform", + "macOS, Windows, Linux", + 1, + ), + ]; + + Self { + items, + bordered: true, + size: Size::default(), + layout: Axis::Horizontal, + } + } + + fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } + + fn set_layout(&mut self, layout: Axis, cx: &mut Context) { + self.layout = layout; + cx.notify(); + } + + fn set_bordered(&mut self, bordered: bool, cx: &mut Context) { + self.bordered = bordered; + cx.notify(); + } + + fn on_change_size(&mut self, a: &ChangeSize, _: &mut Window, cx: &mut Context) { + self.size = a.0; + cx.notify(); + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .id("example") + .on_action(cx.listener(Self::on_change_size)) + .p_4() + .size_full() + .gap_2() + .child( + h_flex() + .gap_3() + .child( + Checkbox::new("layout") + .checked(self.layout.is_vertical()) + .label("Vertical Layout") + .on_click(cx.listener(|this, checked: &bool, _, cx| { + let new_layout = if *checked { + Axis::Vertical + } else { + Axis::Horizontal + }; + this.set_layout(new_layout, cx); + })), + ) + .child( + Checkbox::new("bordered") + .checked(self.bordered) + .label("Bordered") + .on_click(cx.listener(|this, checked: &bool, _, cx| { + this.set_bordered(*checked, cx); + })), + ) + .child( + Button::new("size") + .small() + .label(format!("size: {:?}", self.size)) + .popup_menu({ + let size = self.size; + move |menu, _, _| { + menu.menu_with_check( + "Large", + size == Size::Large, + Box::new(ChangeSize(Size::Large)), + ) + .menu_with_check( + "Medium", + size == Size::Medium, + Box::new(ChangeSize(Size::Medium)), + ) + .menu_with_check( + "Small", + size == Size::Small, + Box::new(ChangeSize(Size::Small)), + ) + } + }), + ), + ) + .child( + DescriptionList::new() + .columns(3) + .layout(self.layout) + .bordered(self.bordered) + .with_size(self.size) + .children(self.items.clone().into_iter().enumerate().map( + |(ix, (label, value, span))| { + if label == "--" { + return DescriptionItem::Divider; + } + + DescriptionItem::new(label) + .value(TextView::markdown(ix, value).into_any_element()) + .span(span) + }, + )), + ) + } +} + +fn main() { + let app = Application::new().with_assets(Assets); + + app.run(move |cx| { + story::init(cx); + cx.activate(true); + + story::create_new_window("Description List Example", Example::view, cx); + }); +} diff --git a/crates/ui/src/description_list.rs b/crates/ui/src/description_list.rs new file mode 100644 index 00000000..f9091d78 --- /dev/null +++ b/crates/ui/src/description_list.rs @@ -0,0 +1,368 @@ +use gpui::{ + div, prelude::FluentBuilder as _, px, AnyElement, App, Axis, DefiniteLength, IntoElement, + ParentElement, RenderOnce, SharedString, Styled, Window, +}; + +use crate::{h_flex, text::Text, v_flex, ActiveTheme as _, AxisExt, Sizable, Size}; + +/// A description list. +#[derive(IntoElement)] +pub struct DescriptionList { + items: Vec, + size: Size, + layout: Axis, + label_width: DefiniteLength, + bordered: bool, + columns: usize, +} + +/// Description item. +pub enum DescriptionItem { + Item { + label: DescriptionText, + value: DescriptionText, + span: usize, + }, + Divider, +} + +#[derive(IntoElement)] +pub enum DescriptionText { + String(SharedString), + Text(Text), + AnyElement(AnyElement), +} + +impl From<&str> for DescriptionText { + fn from(text: &str) -> Self { + DescriptionText::String(SharedString::from(text.to_string())) + } +} + +impl From for DescriptionText { + fn from(text: Text) -> Self { + DescriptionText::Text(text) + } +} + +impl From for DescriptionText { + fn from(element: AnyElement) -> Self { + DescriptionText::AnyElement(element) + } +} + +impl From for DescriptionText { + fn from(text: SharedString) -> Self { + DescriptionText::String(text) + } +} + +impl RenderOnce for DescriptionText { + fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + match self { + DescriptionText::String(text) => div().child(text).into_any_element(), + DescriptionText::Text(text) => text.into_any_element(), + DescriptionText::AnyElement(element) => element, + } + } +} + +impl DescriptionItem { + /// Create a new description item, with a label. + /// + /// The value is an empty element. + pub fn new(label: impl Into) -> Self { + DescriptionItem::Item { + label: label.into(), + value: "".into(), + span: 1, + } + } + + /// Set the element value of the item. + pub fn value(mut self, value: impl Into) -> Self { + let new_value = value.into(); + if let DescriptionItem::Item { value, .. } = &mut self { + *value = new_value; + } + self + } + + /// Set the span of the item. + /// + /// This method only works for [`DescriptionItem::Item`]. + pub fn span(mut self, span: usize) -> Self { + let val = span; + if let DescriptionItem::Item { span, .. } = &mut self { + *span = val; + } + self + } + + fn _label(&self) -> Option<&DescriptionText> { + match self { + DescriptionItem::Item { label, .. } => Some(label), + _ => None, + } + } + + fn _span(&self) -> Option { + match self { + DescriptionItem::Item { span, .. } => Some(*span), + _ => None, + } + } +} + +impl DescriptionList { + /// Create a new description list with the default layout (Horizontal). + pub fn new() -> Self { + Self { + items: Vec::new(), + layout: Axis::Horizontal, + label_width: px(120.).into(), + size: Size::default(), + bordered: true, + columns: 3, + } + } + + /// Create a vertical description list. + pub fn vertical() -> Self { + Self::new().layout(Axis::Vertical) + } + + /// Create a horizontal description list, the default. + pub fn horizontal() -> Self { + Self::new().layout(Axis::Horizontal) + } + + /// Set the width of the label, only works for horizontal layout. + /// + /// Default is `120px`. + pub fn label_width(mut self, label_width: DefiniteLength) -> Self { + self.label_width = label_width; + self + } + + /// Set the layout of the description list. + pub fn layout(mut self, layout: Axis) -> Self { + self.layout = layout; + self + } + + /// Set the border of the description list, default is `true`. + /// + /// `Horizontal` layout only. + pub fn bordered(mut self, bordered: bool) -> Self { + self.bordered = bordered; + self + } + + /// Set the number of columns in the description list, default is `3`. + /// + /// A value between `1` and `10` is allowed. + pub fn columns(mut self, columns: usize) -> Self { + self.columns = columns.clamp(1, 10); + self + } + + /// Add a [`DescriptionItem::Item`] to the list. + pub fn child( + mut self, + label: impl Into, + value: impl Into, + span: usize, + ) -> Self { + self.items.push(DescriptionItem::Item { + label: label.into(), + value: value.into(), + span, + }); + self + } + + /// Add children to the list. + pub fn children( + mut self, + children: impl IntoIterator>, + ) -> Self { + self.items + .extend(children.into_iter().map(Into::into).collect::>()); + self + } + + /// Add a divider to the list. + pub fn divider(mut self) -> Self { + self.items.push(DescriptionItem::Divider); + self + } + + fn group_item_rows(items: Vec, columns: usize) -> Vec> { + let mut rows = vec![]; + let mut current_span = 0; + for item in items.into_iter() { + let span = item._span().unwrap_or(columns); + if rows.is_empty() { + rows.push(vec![]); + } + if current_span + span > columns { + rows.push(vec![]); + current_span = 0; + } + let last_group = rows.last_mut().unwrap(); + last_group.push(item); + current_span += span; + } + // Remove last empty rows if it exists + while let Some(last_group) = rows.last() { + if !last_group.is_empty() { + break; + } + + rows.pop(); + } + + rows + } +} + +impl Sizable for DescriptionList { + fn with_size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } +} + +impl RenderOnce for DescriptionList { + fn render(self, _: &mut Window, cx: &mut gpui::App) -> impl gpui::IntoElement { + let base_gap = match self.size { + Size::XSmall | Size::Small => px(2.), + Size::Medium => px(4.), + Size::Large => px(8.), + _ => px(4.), + }; + + let gap = if self.layout.is_vertical() { + base_gap + } else { + px(0.) + }; + // Only for Horizontal layout + let (padding_x, padding_y) = match self.size { + Size::XSmall | Size::Small => (px(4.), px(2.)), + Size::Medium => (px(8.), px(4.)), + Size::Large => (px(12.), px(6.)), + _ => (px(8.), px(4.)), + }; + + let label_width = if self.layout.is_horizontal() { + Some(self.label_width) + } else { + None + }; + let bordered = self.bordered && self.layout.is_horizontal(); + + // Group items by columns + let rows = Self::group_item_rows(self.items, self.columns); + let rows_len = rows.len(); + + v_flex() + .gap(gap) + .overflow_hidden() + .when(bordered, |this| { + this.rounded(padding_x) + .border_1() + .border_color(cx.theme().border) + }) + .children(rows.into_iter().enumerate().map(|(ix, items)| { + let is_last = ix == rows_len - 1; + h_flex() + .when(bordered && !is_last, |this| { + this.border_b_1().border_color(cx.theme().border) + }) + .children({ + items.into_iter().enumerate().map(|(item_ix, item)| { + let is_first_col = item_ix == 0; + + match item { + DescriptionItem::Item { label, value, .. } => { + let el = if self.layout.is_vertical() { + v_flex() + } else { + h_flex().h_full() + }; + + el.flex_1() + .child( + div() + .when(self.layout.is_horizontal(), |this| { + this.h_full() + }) + .text_color( + cx.theme().description_list_label_foreground, + ) + .text_sm() + .map(|this| match label_width { + Some(label_width) => this + .w(label_width) + .px(padding_x) + .py(padding_y) + .flex_shrink_0() + .when(bordered, |this| { + this.border_r_1() + .when(!is_first_col, |this| { + this.border_l_1() + }) + .border_color(cx.theme().border) + .bg(cx + .theme() + .description_list_label) + }), + None => this, + }) + .child(label), + ) + .child( + div() + .flex_1() + .when(self.layout.is_horizontal(), |this| { + this.px(padding_x).py(padding_y) + }) + .overflow_hidden() + .child(value), + ) + } + _ => div().h_2().w_full().when(bordered, |this| { + this.bg(cx.theme().description_list_label) + }), + } + }) + }) + })) + } +} + +#[cfg(test)] +mod tests { + use super::DescriptionItem; + + #[test] + fn test_group_item_rows() { + let items = vec![ + DescriptionItem::new("test1"), + DescriptionItem::new("test2").span(2), + DescriptionItem::new("test3"), + DescriptionItem::new("test4"), + DescriptionItem::new("test5"), + DescriptionItem::new("test6").span(3), + DescriptionItem::new("test7"), + ]; + let rows = super::DescriptionList::group_item_rows(items, 3); + assert_eq!(rows.len(), 4); + assert_eq!(rows[0].len(), 2); + assert_eq!(rows[1].len(), 3); + assert_eq!(rows[2].len(), 1); + assert_eq!(rows[3].len(), 1); + } +} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index b2149a6e..4dcd1961 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -19,6 +19,7 @@ pub mod checkbox; pub mod clipboard; pub mod color_picker; pub mod context_menu; +pub mod description_list; pub mod divider; pub mod dock; pub mod drawer; diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index c9ab88c1..a78053a0 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -5,8 +5,8 @@ use crate::{ ActiveTheme, }; use gpui::{ - div, px, App, Axis, Div, Edges, Element, ElementId, EntityId, FocusHandle, Pixels, Styled, - Window, + div, px, App, Axis, DefiniteLength, Div, Edges, Element, ElementId, EntityId, FocusHandle, + Pixels, Styled, Window, }; use serde::{Deserialize, Serialize}; @@ -46,6 +46,18 @@ pub trait StyledExt: Styled + Sized { self.flex().flex_col() } + /// Apply paddings to the element. + fn paddings(self, paddings: impl Into>) -> Self + where + L: Into + Clone + Default + std::fmt::Debug, + { + let paddings = paddings.into(); + self.pt(paddings.top.into()) + .pb(paddings.bottom.into()) + .pl(paddings.left.into()) + .pr(paddings.right.into()) + } + /// Render a border with a width of 1px, color red fn debug_red(self) -> Self { if cfg!(debug_assertions) { diff --git a/crates/ui/src/text/html.rs b/crates/ui/src/text/html.rs index 461422d3..c43be79a 100644 --- a/crates/ui/src/text/html.rs +++ b/crates/ui/src/text/html.rs @@ -82,7 +82,7 @@ pub(super) fn parse_html(source: &str) -> Result { #[derive(Clone)] pub(super) struct HtmlElement { id: ElementId, - text: SharedString, + pub(super) text: SharedString, style: TextViewStyle, } diff --git a/crates/ui/src/text/markdown.rs b/crates/ui/src/text/markdown.rs index 8b5ad540..72a08947 100644 --- a/crates/ui/src/text/markdown.rs +++ b/crates/ui/src/text/markdown.rs @@ -34,7 +34,7 @@ use super::{ #[derive(Clone)] pub(super) struct MarkdownElement { id: ElementId, - text: SharedString, + pub(super) text: SharedString, style: TextViewStyle, } diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 2c5c0723..7f1c5f1e 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -85,6 +85,10 @@ pub struct ThemeColor { pub danger_foreground: Hsla, /// Danger hover background color. pub danger_hover: Hsla, + /// Description List label background color. + pub description_list_label: Hsla, + /// Description List label foreground color. + pub description_list_label_foreground: Hsla, /// Drag border color. pub drag_border: Hsla, /// Drop target background color. @@ -224,6 +228,8 @@ impl ThemeColor { danger_active: hsl(0.0, 84.2, 47.0), danger_foreground: hsl(0.0, 0.0, 98.0), danger_hover: hsl(0.0, 84.2, 65.0), + description_list_label: hsl(240.0, 5.9, 96.9), + description_list_label_foreground: hsl(240.0, 5.9, 10.0), drag_border: crate::blue_500(), drop_target: hsl(235.0, 30., 44.0).opacity(0.25), foreground: hsl(240.0, 10., 3.9), @@ -301,6 +307,8 @@ impl ThemeColor { danger_active: hsl(0.0, 62.8, 20.6), danger_foreground: hsl(0.0, 0.0, 78.0), danger_hover: hsl(0.0, 62.8, 35.6), + description_list_label: hsl(240.0, 0., 13.0), + description_list_label_foreground: hsl(0.0, 0.0, 78.0), drag_border: crate::blue_500(), drop_target: hsl(235.0, 30., 44.0).opacity(0.1), foreground: hsl(0., 0., 78.), @@ -483,6 +491,9 @@ impl Theme { self.sidebar_primary = self.sidebar_primary.apply(mask_color); self.sidebar_primary_foreground = self.sidebar_primary_foreground.apply(mask_color); self.tiles = self.tiles.apply(mask_color); + self.description_list_label = self.description_list_label.apply(mask_color); + self.description_list_label_foreground = + self.description_list_label_foreground.apply(mask_color); } /// Sync the theme with the system appearance