diff --git a/Cargo.lock b/Cargo.lock index 80ce418aa2..2d544eff63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5178,6 +5178,7 @@ dependencies = [ "gpui", "log", "optional_struct", + "smallvec", ] [[package]] diff --git a/crates/gpui/playground/Cargo.toml b/crates/gpui/playground/Cargo.toml index 7bf5e50c67..abd639e01b 100644 --- a/crates/gpui/playground/Cargo.toml +++ b/crates/gpui/playground/Cargo.toml @@ -13,3 +13,6 @@ playground_ui = { path = "ui" } gpui = { path = ".." } log.workspace = true simplelog = "0.9" + +[dev-dependencies] +gpui = { path = "..", features = ["test-support"] } diff --git a/crates/gpui/playground/ui/Cargo.toml b/crates/gpui/playground/ui/Cargo.toml index 1ff8b7f50a..c0d0bb28c6 100644 --- a/crates/gpui/playground/ui/Cargo.toml +++ b/crates/gpui/playground/ui/Cargo.toml @@ -13,3 +13,7 @@ derive_more = "0.99.17" gpui = { path = "../.." } log.workspace = true optional_struct = "0.3.1" +smallvec.workspace = true + +[dev-dependencies] +gpui = { path = "../..", features = ["test-support"] } diff --git a/crates/gpui/playground/ui/src/color.rs b/crates/gpui/playground/ui/src/color.rs new file mode 100644 index 0000000000..f205ee77e1 --- /dev/null +++ b/crates/gpui/playground/ui/src/color.rs @@ -0,0 +1,189 @@ +use smallvec::SmallVec; + +pub fn rgb(hex: u32) -> Rgba { + let r = ((hex >> 16) & 0xFF) as f32 / 255.0; + let g = ((hex >> 8) & 0xFF) as f32 / 255.0; + let b = (hex & 0xFF) as f32 / 255.0; + Rgba { r, g, b, a: 1.0 } +} + +#[derive(Clone, Copy, Default, Debug)] +pub struct Rgba { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +impl From for Rgba { + fn from(color: Hsla) -> Self { + let h = color.h; + let s = color.s; + let l = color.l; + + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs()); + let m = l - c / 2.0; + let cm = c + m; + let xm = x + m; + + let (r, g, b) = match (h * 6.0).floor() as i32 { + 0 | 6 => (cm, xm, m), + 1 => (xm, cm, m), + 2 => (m, cm, xm), + 3 => (m, xm, cm), + 4 => (xm, m, cm), + _ => (cm, m, xm), + }; + + Rgba { + r, + g, + b, + a: color.a, + } + } +} + +impl Into for Rgba { + fn into(self) -> gpui::color::Color { + gpui::color::rgba(self.r, self.g, self.b, self.a) + } +} + +#[derive(Copy, Clone)] +pub struct Hsla { + h: f32, + s: f32, + l: f32, + a: f32, +} + +impl From for Hsla { + fn from(color: Rgba) -> Self { + let r = color.r; + let g = color.g; + let b = color.b; + + let max = r.max(g.max(b)); + let min = r.min(g.min(b)); + let delta = max - min; + + let l = (max + min) / 2.0; + let s = match l { + 0.0 | 1.0 => 0.0, + l if l < 0.5 => delta / (2.0 * l), + l => delta / (2.0 - 2.0 * l), + }; + + let h = if delta == 0.0 { + 0.0 + } else if max == r { + ((g - b) / delta).rem_euclid(6.0) / 6.0 + } else if max == g { + ((b - r) / delta + 2.0) / 6.0 + } else { + ((r - g) / delta + 4.0) / 6.0 + }; + + Hsla { + h, + s, + l, + a: color.a, + } + } +} + +impl Hsla { + /// Increases the saturation of the color by a certain amount, with a max + /// value of 1.0. + pub fn saturate(mut self, amount: f32) -> Self { + self.s += amount; + self.s = self.s.clamp(0.0, 1.0); + self + } + + /// Decreases the saturation of the color by a certain amount, with a min + /// value of 0.0. + pub fn desaturate(mut self, amount: f32) -> Self { + self.s -= amount; + self.s = self.s.max(0.0); + if self.s < 0.0 { + self.s = 0.0; + } + self + } + + /// Brightens the color by increasing the lightness by a certain amount, + /// with a max value of 1.0. + pub fn brighten(mut self, amount: f32) -> Self { + self.l += amount; + self.l = self.l.clamp(0.0, 1.0); + self + } + + /// Darkens the color by decreasing the lightness by a certain amount, + /// with a max value of 0.0. + pub fn darken(mut self, amount: f32) -> Self { + self.l -= amount; + self.l = self.l.clamp(0.0, 1.0); + self + } +} + +pub struct ColorScale { + colors: SmallVec<[Hsla; 2]>, + positions: SmallVec<[f32; 2]>, +} + +pub fn scale(colors: I) -> ColorScale +where + I: IntoIterator, + C: Into, +{ + let mut scale = ColorScale { + colors: colors.into_iter().map(Into::into).collect(), + positions: SmallVec::new(), + }; + let num_colors: f32 = scale.colors.len() as f32 - 1.0; + scale.positions = (0..scale.colors.len()) + .map(|i| i as f32 / num_colors) + .collect(); + scale +} + +impl ColorScale { + fn at(&self, t: f32) -> Hsla { + // Ensure that the input is within [0.0, 1.0] + debug_assert!( + 0.0 <= t && t <= 1.0, + "t value {} is out of range. Expected value in range 0.0 to 1.0", + t + ); + + let position = match self + .positions + .binary_search_by(|a| a.partial_cmp(&t).unwrap()) + { + Ok(index) | Err(index) => index, + }; + let lower_bound = position.saturating_sub(1); + let upper_bound = position.min(self.colors.len() - 1); + let lower_color = &self.colors[lower_bound]; + let upper_color = &self.colors[upper_bound]; + + match upper_bound.checked_sub(lower_bound) { + Some(0) | None => *lower_color, + Some(_) => { + let interval_t = (t - self.positions[lower_bound]) + / (self.positions[upper_bound] - self.positions[lower_bound]); + let h = lower_color.h + interval_t * (upper_color.h - lower_color.h); + let s = lower_color.s + interval_t * (upper_color.s - lower_color.s); + let l = lower_color.l + interval_t * (upper_color.l - lower_color.l); + let a = lower_color.a + interval_t * (upper_color.a - lower_color.a); + Hsla { h, s, l, a } + } + } + } +} diff --git a/crates/gpui/playground/ui/src/editor_layout_demo.rs b/crates/gpui/playground/ui/src/editor_layout_demo.rs new file mode 100644 index 0000000000..eea17cad93 --- /dev/null +++ b/crates/gpui/playground/ui/src/editor_layout_demo.rs @@ -0,0 +1,165 @@ +use gpui::{AnyElement, Element, LayoutContext, View, ViewContext}; + +#[derive(Element, Clone, Default)] +pub struct Playground(PhantomData); + +// example layout design here: https://www.figma.com/file/5QLTmxjO0xQpDD3CD4hR6T/Untitled?type=design&node-id=0%3A1&mode=design&t=SoJieVVIvDDDKagv-1 + +impl Playground { + pub fn render(&mut self, _: &mut V, _: &mut gpui::ViewContext) -> impl Element { + col() // fullscreen container with header and main in it + .width(flex(1.)) + .height(flex(1.)) + .fill(colors(gray.900)) + .children([ + row() // header container + .fill(colors(gray.900)) + .width(flex(1.)) + .children([ + row() // tab bar + .width(flex(1.)) + .gap(spacing(2)) + .padding(spacing(3)) + .overflow_x(scroll()) + .chidren([ + row() // tab + .padding_x(spacing(3)) + .padding_y(spacing(2)) + .corner_radius(6.) + .gap(spacing(3)) + .align(center()) + .fill(colors(gray.800)) + .children([text("Tab title 1"), svg("icon_name")]), + row() // tab + .padding_x(spacing(3)) + .padding_y(spacing(2)) + .corner_radius(6.) + .gap(spacing(3)) + .align(center()) + .fill(colors(gray.800)) + .children([text("Tab title 2"), svg("icon_name")]), + row() // tab + .padding_x(spacing(3)) + .padding_y(spacing(2)) + .corner_radius(6.) + .gap(spacing(3)) + .align(center()) + .fill(colors(gray.800)) + .children([text("Tab title 3"), svg("icon_name")]), + ]), + row() // tab bar actions + .border_left(colors(gray.700)) + .gap(spacing(2)) + .padding(spacing(3)) + .chidren([ + row() + .width(spacing(8)) + .height(spacing(8)) + .corner_radius(6.) + .justify(center()) + .align(center()) + .fill(colors(gray.800)) + .child(svg(icon_name)), + row() + .width(spacing(8)) + .height(spacing(8)) + .corner_radius(6.) + .justify(center()) + .align(center()) + .fill(colors(gray.800)) + .child(svg(icon_name)), + row() + .width(spacing(8)) + .height(spacing(8)) + .corner_radius(6.) + .justify(center()) + .align(center()) + .fill(colors(gray.800)) + .child(svg(icon_name)), + ]), + ]), + row() // main container + .width(flex(1.)) + .height(flex(1.)) + .children([ + col() // left sidebar + .fill(colors(gray.800)) + .border_right(colors(gray.700)) + .height(flex(1.)) + .width(260.) + .children([ + col() // containter to hold list items and notification alert box + .justify(between()) + .padding_x(spacing(6)) + .padding_bottom(3) + .padding_top(spacing(6)) + .children([ + col().gap(spacing(3)).children([ // sidebar list + text("Item"), + text("Item"), + text("Item"), + text("Item"), + text("Item"), + text("Item"), + text("Item"), + text("Item"), + ]), + col().align(center()).gap(spacing(1)).children([ // notification alert box + text("Title text").size("lg"), + text("Description text goes here") + .text_color(colors(rose.200)), + ]), + ]), + row() + .padding_x(spacing(3)) + .padding_y(spacing(2)) + .border_top(1., colors(gray.700)) + .align(center()) + .gap(spacing(2)) + .fill(colors(gray.900)) + .children([ + row() // avatar container + .width(spacing(8)) + .height(spacing(8)) + .corner_radius(spacing(8)) + .justify(center()) + .align(center()) + .child(image(image_url)), + text("FirstName Lastname"), // user name + ]), + ]), + col() // primary content container + .align(center()) + .justify(center()) + .child( + col().justify(center()).gap(spacing(8)).children([ // detail container wrapper for center positioning + col() // blue rectangle + .width(rem(30.)) + .height(rem(20.)) + .corner_radius(16.) + .fill(colors(blue.200)), + col().gap(spacing(1)).children([ // center content text items + text("This is a title").size("lg"), + text("This is a description").text_color(colors(gray.500)), + ]), + ]), + ), + col(), // right sidebar + ]), + ]) + } +} + +// row( +// padding(), +// width(), +// fill(), +// ) + +// .width(flex(1.)) +// .height(flex(1.)) +// .justify(end()) +// .align(start()) // default +// .fill(green) +// .child(other_tab_bar()) +// .child(profile_menu()) diff --git a/crates/gpui/playground/ui/src/node.rs b/crates/gpui/playground/ui/src/node.rs index cd86032c78..16f0d7b304 100644 --- a/crates/gpui/playground/ui/src/node.rs +++ b/crates/gpui/playground/ui/src/node.rs @@ -1,5 +1,6 @@ -use derive_more::Add; +use derive_more::{Add, Deref, DerefMut}; use gpui::elements::layout_highlighted_chunks; +use gpui::Entity; use gpui::{ color::Color, fonts::HighlightStyle, @@ -19,16 +20,14 @@ use log::warn; use optional_struct::*; use std::{any::Any, borrow::Cow, f32, ops::Range, sync::Arc}; +use crate::color::Rgba; + pub struct Node { style: NodeStyle, children: Vec>, id: Option>, } -pub fn node(child: impl Element) -> Node { - Node::default().child(child) -} - pub fn column() -> Node { Node::default() } @@ -91,10 +90,22 @@ impl Element for Node { view: &mut V, cx: &mut PaintContext, ) -> Self::PaintState { + dbg!(self.id_string()); + dbg!(bounds.origin(), bounds.size()); + + let bounds_center = dbg!(bounds.size()) / 2.; + let bounds_target = bounds_center + (bounds_center * self.style.align.0); + let layout_center = dbg!(layout.size) / 2.; + let layout_target = layout_center + layout_center * self.style.align.0; + let delta = bounds_target - layout_target; + + let aligned_bounds = RectF::new(bounds.origin() + delta, layout.size); + dbg!(aligned_bounds.origin(), aligned_bounds.size()); let margined_bounds = RectF::from_points( - bounds.origin() + vec2f(layout.margins.left, layout.margins.top), - bounds.lower_right() - vec2f(layout.margins.right, layout.margins.bottom), + aligned_bounds.origin() + vec2f(layout.margins.left, layout.margins.top), + aligned_bounds.lower_right() - vec2f(layout.margins.right, layout.margins.bottom), ); + dbg!(margined_bounds.origin(), margined_bounds.size()); // Paint drop shadow for shadow in &self.style.shadows { @@ -118,17 +129,11 @@ impl Element for Node { // Render the background and/or the border. let Fill::Color(fill_color) = self.style.fill; - let is_fill_visible = !fill_color.is_fully_transparent(); + let is_fill_visible = fill_color.a > 0.; if is_fill_visible || self.style.borders.is_visible() { - eprintln!( - "{}: paint background: {:?}", - self.id.as_deref().unwrap_or(""), - margined_bounds - ); - scene.push_quad(Quad { bounds: margined_bounds, - background: is_fill_visible.then_some(fill_color), + background: is_fill_visible.then_some(fill_color.into()), border: scene::Border { width: self.style.borders.width, color: self.style.borders.color, @@ -162,35 +167,7 @@ impl Element for Node { // let parent_size = padded_bounds.size(); let mut child_origin = padded_bounds.origin(); - // Align all children together along the primary axis - // let mut align_horizontally = false; - // let mut align_vertically = false; - // match axis { - // Axis2d::X => align_horizontally = true, - // Axis2d::Y => align_vertically = true, - // } - // align_child( - // &mut child_origin, - // parent_size, - // layout.content_size, - // self.style.align.0, - // align_horizontally, - // align_vertically, - // ); - for child in &mut self.children { - // Align each child along the cross axis - // align_horizontally = !align_horizontally; - // align_vertically = !align_vertically; - // align_child( - // &mut child_origin, - // parent_size, - // child.size(), - // self.style.align.0, - // align_horizontally, - // align_vertically, - // ); - // child.paint(scene, child_origin, visible_bounds, view, cx); // Advance along the primary axis by the size of this child @@ -284,6 +261,16 @@ impl Node { self } + pub fn margin_x(mut self, margin: impl Into) -> Self { + self.style.margins.set_x(margin.into()); + self + } + + pub fn margin_y(mut self, margin: impl Into) -> Self { + self.style.margins.set_y(margin.into()); + self + } + pub fn margin_top(mut self, top: Length) -> Self { self.style.margins.top = top; self @@ -304,6 +291,23 @@ impl Node { self } + pub fn align(mut self, alignment: f32) -> Self { + let cross_axis = self + .style + .axis + .to_2d() + .map(Axis2d::rotate) + .unwrap_or(Axis2d::Y); + self.style.align.set(cross_axis, alignment); + self + } + + pub fn justify(mut self, alignment: f32) -> Self { + let axis = self.style.axis.to_2d().unwrap_or(Axis2d::X); + self.style.align.set(axis, alignment); + self + } + fn id_string(&self) -> String { self.id.as_deref().unwrap_or("").to_string() } @@ -458,7 +462,7 @@ impl Node { let mut margin_flex = self.style.margins.flex().get(axis); let mut max_margin_length = constraint.max.get(axis) - fixed_length; layout.margins.compute_flex_edges( - &self.style.padding, + &self.style.margins, axis, &mut margin_flex, &mut max_margin_length, @@ -502,7 +506,8 @@ impl Node { } } - layout + dbg!(self.id_string()); + dbg!(layout) } } @@ -549,27 +554,6 @@ impl From for LeftRight { } } -fn align_child( - child_origin: &mut Vector2F, - parent_size: Vector2F, - child_size: Vector2F, - alignment: Vector2F, - horizontal: bool, - vertical: bool, -) { - let parent_center = parent_size / 2.; - let parent_target = parent_center + parent_center * alignment; - let child_center = child_size / 2.; - let child_target = child_center + child_center * alignment; - - if horizontal { - child_origin.set_x(child_origin.x() + parent_target.x() - child_target.x()) - } - if vertical { - child_origin.set_y(child_origin.y() + parent_target.y() - child_target.y()); - } -} - struct Interactive