From 8406c0d9a3b2109c3241e6519993af5f5f369461 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 19 Sep 2023 13:19:22 -0600 Subject: [PATCH] Checkpoint --- crates/gpui3/Cargo.toml | 31 + crates/gpui3/src/app.rs | 217 +++++++ crates/gpui3/src/arc_cow.rs | 0 crates/gpui3/src/color.rs | 240 ++++++++ crates/gpui3/src/element.rs | 275 +++++++++ crates/gpui3/src/elements.rs | 8 + crates/gpui3/src/elements/div.rs | 38 ++ crates/gpui3/src/elements/editor.rs | 61 ++ crates/gpui3/src/fonts.rs | 393 +++++++++++++ crates/gpui3/src/geometry.rs | 374 ++++++++++++ crates/gpui3/src/gpui3.rs | 141 +++++ crates/gpui3/src/platform.rs | 131 +++++ crates/gpui3/src/platform/test.rs | 23 + crates/gpui3/src/renderer.rs | 164 ++++++ crates/gpui3/src/scene.rs | 141 +++++ crates/gpui3/src/shader.frag.wgsl | 1 + crates/gpui3/src/shader.vert.wgsl | 0 crates/gpui3/src/style.rs | 345 +++++++++++ crates/gpui3/src/taffy.rs | 236 ++++++++ crates/gpui3/src/text.rs | 852 ++++++++++++++++++++++++++++ crates/gpui3/src/window.rs | 251 ++++++++ 21 files changed, 3922 insertions(+) create mode 100644 crates/gpui3/Cargo.toml create mode 100644 crates/gpui3/src/app.rs create mode 100644 crates/gpui3/src/arc_cow.rs create mode 100644 crates/gpui3/src/color.rs create mode 100644 crates/gpui3/src/element.rs create mode 100644 crates/gpui3/src/elements.rs create mode 100644 crates/gpui3/src/elements/div.rs create mode 100644 crates/gpui3/src/elements/editor.rs create mode 100644 crates/gpui3/src/fonts.rs create mode 100644 crates/gpui3/src/geometry.rs create mode 100644 crates/gpui3/src/gpui3.rs create mode 100644 crates/gpui3/src/platform.rs create mode 100644 crates/gpui3/src/platform/test.rs create mode 100644 crates/gpui3/src/renderer.rs create mode 100644 crates/gpui3/src/scene.rs create mode 100644 crates/gpui3/src/shader.frag.wgsl create mode 100644 crates/gpui3/src/shader.vert.wgsl create mode 100644 crates/gpui3/src/style.rs create mode 100644 crates/gpui3/src/taffy.rs create mode 100644 crates/gpui3/src/text.rs create mode 100644 crates/gpui3/src/window.rs diff --git a/crates/gpui3/Cargo.toml b/crates/gpui3/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f0a8debee290b77ea7ca4841a1041e1a8647abfc --- /dev/null +++ b/crates/gpui3/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "gpui3" +version = "0.1.0" +edition = "2021" + +[features] +test = [] + +[lib] +path = "src/gpui3.rs" + +[dependencies] +anyhow.workspace = true +bytemuck = "1.14.0" +derive_more.workspace = true +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" } +itertools = "0.11.0" +log.workspace = true +parking_lot.workspace = true +plane-split = "0.18.0" +raw-window-handle = "0.5.2" +refineable = { path = "../refineable" } +rust-embed.workspace = true +schemars = "0.8" +serde.workspace = true +simplelog = "0.9" +slotmap = "1.0.6" +smallvec.workspace = true +taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" } +util = { path = "../util" } +wgpu = "0.17.0" diff --git a/crates/gpui3/src/app.rs b/crates/gpui3/src/app.rs new file mode 100644 index 0000000000000000000000000000000000000000..b826b165ee041dfa9c1140a4abe810bbb2029075 --- /dev/null +++ b/crates/gpui3/src/app.rs @@ -0,0 +1,217 @@ +use anyhow::{anyhow, Result}; +use slotmap::SlotMap; +use std::{any::Any, marker::PhantomData, rc::Rc, sync::Arc}; + +use crate::FontCache; + +use super::{ + platform::Platform, + window::{Window, WindowHandle, WindowId}, + Context, LayoutId, Reference, View, WindowContext, +}; + +pub struct AppContext { + platform: Rc, + font_cache: Arc, + pub(crate) entities: SlotMap>>, + pub(crate) windows: SlotMap>, + // We recycle this memory across layout requests. + pub(crate) layout_id_buffer: Vec, +} + +impl AppContext { + pub fn new(platform: Rc) -> Self { + let font_cache = Arc::new(FontCache::new(platform.font_system())); + AppContext { + platform, + font_cache, + entities: SlotMap::with_key(), + windows: SlotMap::with_key(), + layout_id_buffer: Default::default(), + } + } + + #[cfg(any(test, feature = "test"))] + pub fn test() -> Self { + Self::new(Rc::new(super::TestPlatform::new())) + } + + pub fn platform(&self) -> &Rc { + &self.platform + } + + pub fn font_cache(&self) -> &Arc { + &self.font_cache + } + + pub fn open_window( + &mut self, + options: crate::WindowOptions, + build_root_view: impl FnOnce(&mut WindowContext) -> View, + ) -> WindowHandle { + let id = self.windows.insert(None); + let handle = WindowHandle::new(id); + self.platform.open_window(handle.into(), options); + + let mut window = Window::new(id); + let root_view = build_root_view(&mut WindowContext::mutable(self, &mut window)); + window.root_view.replace(Box::new(root_view)); + + self.windows.get_mut(id).unwrap().replace(window); + handle + } + + pub(crate) fn update_window( + &mut self, + window_id: WindowId, + update: impl FnOnce(&mut WindowContext) -> R, + ) -> Result { + let mut window = self + .windows + .get_mut(window_id) + .ok_or_else(|| anyhow!("window not found"))? + .take() + .unwrap(); + + let result = update(&mut WindowContext::mutable(self, &mut window)); + + self.windows + .get_mut(window_id) + .ok_or_else(|| anyhow!("window not found"))? + .replace(window); + + Ok(result) + } +} + +impl Context for AppContext { + type EntityContext<'a, 'w, T: 'static> = ModelContext<'a, T>; + + fn entity( + &mut self, + build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T, + ) -> Handle { + let id = self.entities.insert(None); + let entity = Box::new(build_entity(&mut ModelContext::mutable(self, id))); + self.entities.get_mut(id).unwrap().replace(entity); + + Handle::new(id) + } + + fn update_entity( + &mut self, + handle: &Handle, + update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R, + ) -> R { + let mut entity = self + .entities + .get_mut(handle.id) + .unwrap() + .take() + .unwrap() + .downcast::() + .unwrap(); + + let result = update(&mut *entity, &mut ModelContext::mutable(self, handle.id)); + self.entities.get_mut(handle.id).unwrap().replace(entity); + result + } +} + +pub struct ModelContext<'a, T> { + app: Reference<'a, AppContext>, + entity_type: PhantomData, + entity_id: EntityId, +} + +impl<'a, T: 'static> ModelContext<'a, T> { + pub(crate) fn mutable(app: &'a mut AppContext, entity_id: EntityId) -> Self { + Self { + app: Reference::Mutable(app), + entity_type: PhantomData, + entity_id, + } + } + + fn immutable(app: &'a AppContext, entity_id: EntityId) -> Self { + Self { + app: Reference::Immutable(app), + entity_type: PhantomData, + entity_id, + } + } + + fn update(&mut self, update: impl FnOnce(&mut T, &mut Self) -> R) -> R { + let mut entity = self + .app + .entities + .get_mut(self.entity_id) + .unwrap() + .take() + .unwrap(); + let result = update(entity.downcast_mut::().unwrap(), self); + self.app + .entities + .get_mut(self.entity_id) + .unwrap() + .replace(entity); + result + } +} + +impl<'a, T: 'static> Context for ModelContext<'a, T> { + type EntityContext<'b, 'c, U: 'static> = ModelContext<'b, U>; + + fn entity( + &mut self, + build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, U>) -> U, + ) -> Handle { + self.app.entity(build_entity) + } + + fn update_entity( + &mut self, + handle: &Handle, + update: impl FnOnce(&mut U, &mut Self::EntityContext<'_, '_, U>) -> R, + ) -> R { + self.app.update_entity(handle, update) + } +} + +pub struct Handle { + pub(crate) id: EntityId, + pub(crate) entity_type: PhantomData, +} + +slotmap::new_key_type! { pub struct EntityId; } + +impl Handle { + fn new(id: EntityId) -> Self { + Self { + id, + entity_type: PhantomData, + } + } + + /// Update the entity referenced by this handle with the given function. + /// + /// The update function receives a context appropriate for its environment. + /// When updating in an `AppContext`, it receives a `ModelContext`. + /// When updating an a `WindowContext`, it receives a `ViewContext`. + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut C::EntityContext<'_, '_, T>) -> R, + ) -> R { + cx.update_entity(self, update) + } +} + +impl Clone for Handle { + fn clone(&self) -> Self { + Self { + id: self.id, + entity_type: PhantomData, + } + } +} diff --git a/crates/gpui3/src/arc_cow.rs b/crates/gpui3/src/arc_cow.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/gpui3/src/color.rs b/crates/gpui3/src/color.rs new file mode 100644 index 0000000000000000000000000000000000000000..cf2a070d48db8b8e451b147f851afa6a34503b1c --- /dev/null +++ b/crates/gpui3/src/color.rs @@ -0,0 +1,240 @@ +#![allow(dead_code)] + +use bytemuck::{Pod, Zeroable}; +use serde::de::{self, Deserialize, Deserializer, Visitor}; +use std::fmt; +use std::num::ParseIntError; + +pub fn rgb>(hex: u32) -> C { + 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 }.into() +} + +#[derive(Clone, Copy, Default, Debug)] +pub struct Rgba { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +impl Rgba { + pub fn blend(&self, other: Rgba) -> Self { + if other.a >= 1.0 { + return other; + } else if other.a <= 0.0 { + return *self; + } else { + return Rgba { + r: (self.r * (1.0 - other.a)) + (other.r * other.a), + g: (self.g * (1.0 - other.a)) + (other.g * other.a), + b: (self.b * (1.0 - other.a)) + (other.b * other.a), + a: self.a, + }; + } + } +} + +struct RgbaVisitor; + +impl<'de> Visitor<'de> for RgbaVisitor { + type Value = Rgba; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string in the format #rrggbb or #rrggbbaa") + } + + fn visit_str(self, value: &str) -> Result { + if value.len() == 7 || value.len() == 9 { + let r = u8::from_str_radix(&value[1..3], 16).unwrap() as f32 / 255.0; + let g = u8::from_str_radix(&value[3..5], 16).unwrap() as f32 / 255.0; + let b = u8::from_str_radix(&value[5..7], 16).unwrap() as f32 / 255.0; + let a = if value.len() == 9 { + u8::from_str_radix(&value[7..9], 16).unwrap() as f32 / 255.0 + } else { + 1.0 + }; + Ok(Rgba { r, g, b, a }) + } else { + Err(E::custom( + "Bad format for RGBA. Expected #rrggbb or #rrggbbaa.", + )) + } + } +} + +impl<'de> Deserialize<'de> for Rgba { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(RgbaVisitor) + } +} + +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 TryFrom<&'_ str> for Rgba { + type Error = ParseIntError; + + fn try_from(value: &'_ str) -> Result { + let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0; + let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0; + let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0; + let a = if value.len() > 7 { + u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0 + } else { + 1.0 + }; + + Ok(Rgba { r, g, b, a }) + } +} + +#[derive(Default, Copy, Clone, Debug, PartialEq)] +#[repr(C)] +pub struct Hsla { + pub h: f32, + pub s: f32, + pub l: f32, + pub a: f32, +} + +impl Eq for Hsla {} +unsafe impl Zeroable for Hsla {} +unsafe impl Pod for Hsla {} + +pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { + Hsla { + h: h.clamp(0., 1.), + s: s.clamp(0., 1.), + l: l.clamp(0., 1.), + a: a.clamp(0., 1.), + } +} + +pub fn black() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 0., + a: 1., + } +} + +impl Hsla { + /// Returns true if the HSLA color is fully transparent, false otherwise. + pub fn is_transparent(&self) -> bool { + self.a == 0.0 + } + + /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors. + /// + /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color. + /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color. + /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values. + /// + /// Assumptions: + /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent. + /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing it's own alpha value. + /// - RGB color components are contained in the range [0, 1]. + /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined. + pub fn blend(self, other: Hsla) -> Hsla { + let alpha = other.a; + + if alpha >= 1.0 { + return other; + } else if alpha <= 0.0 { + return self; + } else { + let converted_self = Rgba::from(self); + let converted_other = Rgba::from(other); + let blended_rgb = converted_self.blend(converted_other); + return Hsla::from(blended_rgb); + } + } + + /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0. + /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color. + pub fn fade_out(&mut self, factor: f32) { + self.a *= 1.0 - factor.clamp(0., 1.); + } +} + +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 = if l == 0.0 || l == 1.0 { + 0.0 + } else if l < 0.5 { + delta / (2.0 * l) + } else { + 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<'de> Deserialize<'de> for Hsla { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // First, deserialize it into Rgba + let rgba = Rgba::deserialize(deserializer)?; + + // Then, use the From for Hsla implementation to convert it + Ok(Hsla::from(rgba)) + } +} diff --git a/crates/gpui3/src/element.rs b/crates/gpui3/src/element.rs new file mode 100644 index 0000000000000000000000000000000000000000..afb50fabec2fe80b07dee30880c8ae604691a156 --- /dev/null +++ b/crates/gpui3/src/element.rs @@ -0,0 +1,275 @@ +use super::{Handle, Layout, LayoutId, Pixels, Point, Result, ViewContext, WindowContext}; +use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc}; + +pub trait Element: 'static { + type State; + type FrameState; + + fn layout( + &mut self, + state: &mut Self::State, + cx: &mut ViewContext, + ) -> Result<(LayoutId, Self::FrameState)>; + + fn paint( + &mut self, + layout: Layout, + state: &mut Self::State, + frame_state: &mut Self::FrameState, + cx: &mut ViewContext, + ) -> Result<()>; +} + +pub trait ParentElement { + fn child(self, child: impl IntoAnyElement) -> Self; +} + +trait ElementObject { + fn layout(&mut self, state: &mut S, cx: &mut ViewContext) -> Result; + fn paint( + &mut self, + parent_origin: super::Point, + state: &mut S, + cx: &mut ViewContext, + ) -> Result<()>; +} + +struct RenderedElement { + element: E, + phase: ElementRenderPhase, +} + +#[derive(Default)] +enum ElementRenderPhase { + #[default] + Rendered, + LayoutRequested { + layout_id: LayoutId, + frame_state: S, + }, + Painted { + layout: Layout, + frame_state: S, + }, +} + +/// Internal struct that wraps an element to store Layout and FrameState after the element is rendered. +/// It's allocated as a trait object to erase the element type and wrapped in AnyElement for +/// improved usability. +impl RenderedElement { + fn new(element: E) -> Self { + RenderedElement { + element, + phase: ElementRenderPhase::Rendered, + } + } +} + +impl ElementObject for RenderedElement { + fn layout(&mut self, state: &mut E::State, cx: &mut ViewContext) -> Result { + let (layout_id, frame_state) = self.element.layout(state, cx)?; + self.phase = ElementRenderPhase::LayoutRequested { + layout_id, + frame_state, + }; + Ok(layout_id) + } + + fn paint( + &mut self, + parent_origin: Point, + state: &mut E::State, + cx: &mut ViewContext, + ) -> Result<()> { + self.phase = match std::mem::take(&mut self.phase) { + ElementRenderPhase::Rendered => panic!("must call layout before paint"), + + ElementRenderPhase::LayoutRequested { + layout_id, + mut frame_state, + } => { + let mut layout = cx.layout(layout_id)?; + layout.bounds.origin += parent_origin; + self.element + .paint(layout.clone(), state, &mut frame_state, cx)?; + ElementRenderPhase::Painted { + layout, + frame_state, + } + } + + ElementRenderPhase::Painted { + layout, + mut frame_state, + } => { + self.element + .paint(layout.clone(), state, &mut frame_state, cx)?; + ElementRenderPhase::Painted { + layout, + frame_state, + } + } + }; + + Ok(()) + } +} + +pub struct AnyElement(Box>); + +impl AnyElement { + pub fn layout(&mut self, state: &mut S, cx: &mut ViewContext) -> Result { + self.0.layout(state, cx) + } + + pub fn paint( + &mut self, + parent_origin: Point, + state: &mut S, + cx: &mut ViewContext, + ) -> Result<()> { + self.0.paint(parent_origin, state, cx) + } +} + +pub trait IntoAnyElement { + fn into_any(self) -> AnyElement; +} + +impl IntoAnyElement for E { + fn into_any(self) -> AnyElement { + AnyElement(Box::new(RenderedElement::new(self))) + } +} + +impl IntoAnyElement for AnyElement { + fn into_any(self) -> AnyElement { + self + } +} + +#[derive(Clone)] +pub struct View { + state: Handle, + render: Rc) -> AnyElement>, +} + +pub fn view>( + state: Handle, + render: impl 'static + Fn(&mut S, &mut ViewContext) -> E, +) -> View { + View { + state, + render: Rc::new(move |state, cx| render(state, cx).into_any()), + } +} + +impl View { + pub fn into_any(self) -> AnyView { + AnyView { + view: Rc::new(RefCell::new(self)), + parent_state_type: PhantomData, + } + } +} + +impl Element for View { + type State = (); + type FrameState = AnyElement; + + fn layout( + &mut self, + _: &mut Self::State, + cx: &mut ViewContext, + ) -> Result<(LayoutId, Self::FrameState)> { + self.state.update(cx, |state, cx| { + let mut element = (self.render)(state, cx); + let layout_id = element.layout(state, cx)?; + Ok((layout_id, element)) + }) + } + + fn paint( + &mut self, + layout: Layout, + _: &mut Self::State, + element: &mut Self::FrameState, + cx: &mut ViewContext, + ) -> Result<()> { + self.state.update(cx, |state, cx| { + element.paint(layout.bounds.origin, state, cx) + }) + } +} + +trait ViewObject { + fn layout(&mut self, cx: &mut WindowContext) -> Result<(LayoutId, Box)>; + fn paint( + &mut self, + layout: Layout, + element: &mut dyn Any, + cx: &mut WindowContext, + ) -> Result<()>; +} + +impl ViewObject for View { + fn layout(&mut self, cx: &mut WindowContext) -> Result<(LayoutId, Box)> { + self.state.update(cx, |state, cx| { + let mut element = (self.render)(state, cx); + let layout_id = element.layout(state, cx)?; + let element = Box::new(element) as Box; + Ok((layout_id, element)) + }) + } + + fn paint( + &mut self, + layout: Layout, + element: &mut dyn Any, + cx: &mut WindowContext, + ) -> Result<()> { + self.state.update(cx, |state, cx| { + element + .downcast_mut::>() + .unwrap() + .paint(layout.bounds.origin, state, cx) + }) + } +} + +pub struct AnyView { + view: Rc>, + parent_state_type: PhantomData, +} + +impl Element for AnyView { + type State = S; + type FrameState = Box; + + fn layout( + &mut self, + _: &mut Self::State, + cx: &mut ViewContext, + ) -> Result<(LayoutId, Self::FrameState)> { + self.view.borrow_mut().layout(cx) + } + + fn paint( + &mut self, + layout: Layout, + _: &mut Self::State, + element: &mut Self::FrameState, + cx: &mut ViewContext, + ) -> Result<()> { + self.view.borrow_mut().paint(layout, element, cx) + } +} + +impl Clone for AnyView { + fn clone(&self) -> Self { + Self { + view: self.view.clone(), + parent_state_type: PhantomData, + } + } +} diff --git a/crates/gpui3/src/elements.rs b/crates/gpui3/src/elements.rs new file mode 100644 index 0000000000000000000000000000000000000000..3847db24cc5fcdbaf135ec517f0415dec8da138a --- /dev/null +++ b/crates/gpui3/src/elements.rs @@ -0,0 +1,8 @@ +pub mod div; +pub mod editor; + +use super::*; +use std::marker::PhantomData; + +pub use div::div; +pub use editor::field; diff --git a/crates/gpui3/src/elements/div.rs b/crates/gpui3/src/elements/div.rs new file mode 100644 index 0000000000000000000000000000000000000000..bc0c23fc5ad0e3b1485cdf04ad18bbd33c4c1e43 --- /dev/null +++ b/crates/gpui3/src/elements/div.rs @@ -0,0 +1,38 @@ +use super::{ + Element, IntoAnyElement, Layout, LayoutId, ParentElement, PhantomData, Result, ViewContext, +}; + +pub struct Div(PhantomData); + +impl Element for Div { + type State = S; + type FrameState = (); + + fn layout( + &mut self, + _state: &mut Self::State, + _cx: &mut ViewContext, + ) -> Result<(LayoutId, Self::FrameState)> { + todo!() + } + + fn paint( + &mut self, + _layout: Layout, + _state: &mut Self::State, + _frame_state: &mut Self::FrameState, + _cx: &mut ViewContext, + ) -> Result<()> { + todo!() + } +} + +impl ParentElement for Div { + fn child(self, _child: impl IntoAnyElement) -> Self { + todo!() + } +} + +pub fn div() -> Div { + todo!() +} diff --git a/crates/gpui3/src/elements/editor.rs b/crates/gpui3/src/elements/editor.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b2cbc808b7bacbe9af56451cb98c1ce595c57b9 --- /dev/null +++ b/crates/gpui3/src/elements/editor.rs @@ -0,0 +1,61 @@ +use super::{Element, Handle, Layout, LayoutId, Result, SharedString, ViewContext}; +use std::marker::PhantomData; + +pub fn field(editor: Handle) -> EditorElement { + EditorElement { + editor, + field: true, + placeholder_text: None, + parent_state: PhantomData, + } +} + +pub struct EditorElement { + editor: Handle, + field: bool, + placeholder_text: Option, + parent_state: PhantomData, +} + +impl EditorElement { + pub fn field(mut self) -> Self { + self.field = true; + self + } + + pub fn placeholder_text(mut self, text: impl Into) -> Self { + self.placeholder_text = Some(text.into()); + self + } +} + +impl Element for EditorElement { + type State = S; + type FrameState = (); + + fn layout( + &mut self, + _: &mut Self::State, + cx: &mut ViewContext, + ) -> Result<(LayoutId, Self::FrameState)> { + self.editor.update(cx, |_editor, _cx| todo!()) + } + + fn paint( + &mut self, + _layout: Layout, + _state: &mut Self::State, + _frame_state: &mut Self::FrameState, + cx: &mut ViewContext, + ) -> Result<()> { + self.editor.update(cx, |_editor, _cx| todo!()) + } +} + +pub struct Editor {} + +impl Editor { + pub fn new(_: &mut ViewContext) -> Self { + Editor {} + } +} diff --git a/crates/gpui3/src/fonts.rs b/crates/gpui3/src/fonts.rs new file mode 100644 index 0000000000000000000000000000000000000000..00b5d8d1c3ac149f7df4ff915d4cb504be2f6ba5 --- /dev/null +++ b/crates/gpui3/src/fonts.rs @@ -0,0 +1,393 @@ +use crate::{px, Bounds, LineWrapper, Pixels, PlatformFontSystem, Result, Size}; +use anyhow::anyhow; +pub use font_kit::properties::{ + Properties as FontProperties, Stretch as FontStretch, Style as FontStyle, Weight as FontWeight, +}; +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, + sync::Arc, +}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct FontFamilyId(usize); + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct FontId(pub usize); + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct FontFeatures { + pub calt: Option, + pub case: Option, + pub cpsp: Option, + pub frac: Option, + pub liga: Option, + pub onum: Option, + pub ordn: Option, + pub pnum: Option, + pub ss01: Option, + pub ss02: Option, + pub ss03: Option, + pub ss04: Option, + pub ss05: Option, + pub ss06: Option, + pub ss07: Option, + pub ss08: Option, + pub ss09: Option, + pub ss10: Option, + pub ss11: Option, + pub ss12: Option, + pub ss13: Option, + pub ss14: Option, + pub ss15: Option, + pub ss16: Option, + pub ss17: Option, + pub ss18: Option, + pub ss19: Option, + pub ss20: Option, + pub subs: Option, + pub sups: Option, + pub swsh: Option, + pub titl: Option, + pub tnum: Option, + pub zero: Option, +} + +#[allow(non_camel_case_types)] +#[derive(Deserialize)] +enum WeightJson { + thin, + extra_light, + light, + normal, + medium, + semibold, + bold, + extra_bold, + black, +} + +struct Family { + name: Arc, + font_features: FontFeatures, + font_ids: Vec, +} + +pub struct FontCache(RwLock); + +pub struct FontCacheState { + font_system: Arc, + families: Vec, + default_family: Option, + font_selections: HashMap>, + metrics: HashMap, + wrapper_pool: HashMap<(FontId, Pixels), Vec>, +} + +unsafe impl Send for FontCache {} + +impl FontCache { + pub fn new(fonts: Arc) -> Self { + Self(RwLock::new(FontCacheState { + font_system: fonts, + families: Default::default(), + default_family: None, + font_selections: Default::default(), + metrics: Default::default(), + wrapper_pool: Default::default(), + })) + } + + pub fn family_name(&self, family_id: FontFamilyId) -> Result> { + self.0 + .read() + .families + .get(family_id.0) + .ok_or_else(|| anyhow!("invalid family id")) + .map(|family| family.name.clone()) + } + + pub fn load_family(&self, names: &[&str], features: &FontFeatures) -> Result { + for name in names { + let state = self.0.upgradable_read(); + + if let Some(ix) = state + .families + .iter() + .position(|f| f.name.as_ref() == *name && f.font_features == *features) + { + return Ok(FontFamilyId(ix)); + } + + let mut state = RwLockUpgradableReadGuard::upgrade(state); + + if let Ok(font_ids) = state.font_system.load_family(name, features) { + if font_ids.is_empty() { + continue; + } + + let family_id = FontFamilyId(state.families.len()); + for font_id in &font_ids { + if state.font_system.glyph_for_char(*font_id, 'm').is_none() { + return Err(anyhow!("font must contain a glyph for the 'm' character")); + } + } + + state.families.push(Family { + name: Arc::from(*name), + font_features: features.clone(), + font_ids, + }); + return Ok(family_id); + } + } + + Err(anyhow!( + "could not find a non-empty font family matching one of the given names" + )) + } + + /// Returns an arbitrary font family that is available on the system. + pub fn known_existing_family(&self) -> FontFamilyId { + if let Some(family_id) = self.0.read().default_family { + return family_id; + } + + let default_family = self + .load_family( + &["Courier", "Helvetica", "Arial", "Verdana"], + &Default::default(), + ) + .unwrap_or_else(|_| { + let all_family_names = self.0.read().font_system.all_families(); + let all_family_names: Vec<_> = all_family_names + .iter() + .map(|string| string.as_str()) + .collect(); + self.load_family(&all_family_names, &Default::default()) + .expect("could not load any default font family") + }); + + self.0.write().default_family = Some(default_family); + default_family + } + + pub fn default_font(&self, family_id: FontFamilyId) -> FontId { + self.select_font(family_id, Default::default(), Default::default()) + .unwrap() + } + + pub fn select_font( + &self, + family_id: FontFamilyId, + weight: FontWeight, + style: FontStyle, + ) -> Result { + let inner = self.0.upgradable_read(); + if let Some(font_id) = inner + .font_selections + .get(&family_id) + .and_then(|fonts| fonts.get(&(weight, style))) + { + Ok(*font_id) + } else { + let mut inner = RwLockUpgradableReadGuard::upgrade(inner); + let family = &inner.families[family_id.0]; + let font_id = inner + .font_system + .select_font(&family.font_ids, weight, style) + .unwrap_or(family.font_ids[0]); + inner + .font_selections + .entry(family_id) + .or_default() + .insert((weight, style), font_id); + Ok(font_id) + } + } + + pub fn metric(&self, font_id: FontId, f: F) -> T + where + F: FnOnce(&FontMetrics) -> T, + T: 'static, + { + let state = self.0.upgradable_read(); + if let Some(metrics) = state.metrics.get(&font_id) { + f(metrics) + } else { + let metrics = state.font_system.font_metrics(font_id); + let metric = f(&metrics); + let mut state = RwLockUpgradableReadGuard::upgrade(state); + state.metrics.insert(font_id, metrics); + metric + } + } + + pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Size { + let bounding_box = self.metric(font_id, |m| m.bounding_box); + + let width = px(bounding_box.size.width) * self.em_size(font_id, font_size); + let height = px(bounding_box.size.height) * self.em_size(font_id, font_size); + Size { width, height } + } + + pub fn em_width(&self, font_id: FontId, font_size: Pixels) -> Pixels { + let glyph_id; + let bounds; + { + let state = self.0.read(); + glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap(); + bounds = state + .font_system + .typographic_bounds(font_id, glyph_id) + .unwrap(); + } + bounds.size.width * self.em_size(font_id, font_size) + } + + pub fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Pixels { + let glyph_id; + let advance; + { + let state = self.0.read(); + glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap(); + advance = state.font_system.advance(font_id, glyph_id).unwrap(); + } + advance.x * self.em_size(font_id, font_size) + } + + pub fn line_height(&self, font_size: Pixels) -> Pixels { + (font_size * 1.618).round() + } + + pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.em_size(font_id, font_size) * self.metric(font_id, |m| m.cap_height) + } + + pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.em_size(font_id, font_size) * self.metric(font_id, |m| m.x_height) + } + + pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.em_size(font_id, font_size) * self.metric(font_id, |m| m.ascent) + } + + pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.em_size(font_id, font_size) * self.metric(font_id, |m| -m.descent) + } + + pub fn em_size(&self, font_id: FontId, font_size: Pixels) -> Pixels { + font_size / self.metric(font_id, |m| m.units_per_em as f32) + } + + pub fn baseline_offset(&self, font_id: FontId, font_size: Pixels) -> Pixels { + let line_height = self.line_height(font_size); + let ascent = self.ascent(font_id, font_size); + let descent = self.descent(font_id, font_size); + let padding_top = (line_height - ascent - descent) / 2.; + padding_top + ascent + } + + pub fn line_wrapper(self: &Arc, font_id: FontId, font_size: Pixels) -> LineWrapperHandle { + let mut state = self.0.write(); + let wrappers = state.wrapper_pool.entry((font_id, font_size)).or_default(); + let wrapper = wrappers + .pop() + .unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.font_system.clone())); + LineWrapperHandle { + wrapper: Some(wrapper), + font_cache: self.clone(), + } + } +} + +pub struct LineWrapperHandle { + wrapper: Option, + font_cache: Arc, +} + +impl Drop for LineWrapperHandle { + fn drop(&mut self) { + let mut state = self.font_cache.0.write(); + let wrapper = self.wrapper.take().unwrap(); + state + .wrapper_pool + .get_mut(&(wrapper.font_id, wrapper.font_size)) + .unwrap() + .push(wrapper); + } +} + +impl Deref for LineWrapperHandle { + type Target = LineWrapper; + + fn deref(&self) -> &Self::Target { + self.wrapper.as_ref().unwrap() + } +} + +impl DerefMut for LineWrapperHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + self.wrapper.as_mut().unwrap() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct FontMetrics { + pub units_per_em: u32, + pub ascent: f32, + pub descent: f32, + pub line_gap: f32, + pub underline_position: f32, + pub underline_thickness: f32, + pub cap_height: f32, + pub x_height: f32, + pub bounding_box: Bounds, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{FontStyle, FontWeight, Platform, TestPlatform}; + + #[test] + fn test_select_font() { + let platform = TestPlatform::new(); + let fonts = FontCache::new(platform.font_system()); + let arial = fonts + .load_family( + &["Arial"], + &FontFeatures { + calt: Some(false), + ..Default::default() + }, + ) + .unwrap(); + let arial_regular = fonts + .select_font(arial, FontWeight::default(), FontStyle::default()) + .unwrap(); + let arial_italic = fonts + .select_font(arial, FontWeight::default(), FontStyle::Italic) + .unwrap(); + let arial_bold = fonts + .select_font(arial, FontWeight::BOLD, FontStyle::default()) + .unwrap(); + assert_ne!(arial_regular, arial_italic); + assert_ne!(arial_regular, arial_bold); + assert_ne!(arial_italic, arial_bold); + + let arial_with_calt = fonts + .load_family( + &["Arial"], + &FontFeatures { + calt: Some(true), + ..Default::default() + }, + ) + .unwrap(); + assert_ne!(arial_with_calt, arial); + } +} diff --git a/crates/gpui3/src/geometry.rs b/crates/gpui3/src/geometry.rs new file mode 100644 index 0000000000000000000000000000000000000000..f95cc1fe037d628cf11b1fd3984a3b0ebca00492 --- /dev/null +++ b/crates/gpui3/src/geometry.rs @@ -0,0 +1,374 @@ +use bytemuck::{Pod, Zeroable}; +use core::fmt::Debug; +use derive_more::{Add, AddAssign, Div, Mul, Sub, SubAssign}; +use refineable::Refineable; +use std::ops::{Add, Mul}; + +#[derive(Refineable, Default, Add, AddAssign, Sub, Mul, Div, Copy, Debug, PartialEq, Eq, Hash)] +#[refineable(debug)] +#[repr(C)] +pub struct Point { + pub x: T, + pub y: T, +} + +pub fn point(x: T, y: T) -> Point { + Point { x, y } +} + +impl Point { + pub fn new(x: T, y: T) -> Self { + Self { x, y } + } +} + +impl Clone for Point { + fn clone(&self) -> Self { + Self { + x: self.x.clone(), + y: self.y.clone(), + } + } +} + +unsafe impl Zeroable for Point {} + +unsafe impl Pod for Point {} + +#[derive(Refineable, Default, Clone, Copy, Debug, PartialEq)] +#[refineable(debug)] +pub struct Size { + pub width: T, + pub height: T, +} + +impl Size { + pub fn full() -> Self { + Self { + width: relative(1.), + height: relative(1.), + } + } +} + +impl Size { + pub fn zero() -> Self { + Self { + width: px(0.).into(), + height: px(0.).into(), + } + } +} + +impl Size { + pub fn auto() -> Self { + Self { + width: Length::Auto, + height: Length::Auto, + } + } +} + +#[derive(Refineable, Clone, Default, Debug, PartialEq)] +#[refineable(debug)] +pub struct Bounds { + pub origin: Point, + pub size: Size, +} + +impl> Bounds { + pub fn upper_right(&self) -> Point { + Point { + x: self.origin.x + self.size.width, + y: self.origin.y, + } + } +} + +impl Copy for Bounds {} + +#[derive(Refineable, Clone, Default, Debug)] +#[refineable(debug)] +pub struct Edges { + pub top: T, + pub right: T, + pub bottom: T, + pub left: T, +} + +impl Edges { + pub fn auto() -> Self { + Self { + top: Length::Auto, + right: Length::Auto, + bottom: Length::Auto, + left: Length::Auto, + } + } + + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } +} + +impl Edges { + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } +} + +impl Edges { + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } + + pub fn to_pixels(&self, rem_size: Pixels) -> Edges { + Edges { + top: self.top.to_pixels(rem_size), + right: self.right.to_pixels(rem_size), + bottom: self.bottom.to_pixels(rem_size), + left: self.left.to_pixels(rem_size), + } + } +} + +impl Edges { + pub fn is_empty(&self) -> bool { + self.top == px(0.) && self.right == px(0.) && self.bottom == px(0.) && self.left == px(0.) + } +} + +#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)] +#[repr(transparent)] +pub struct Pixels(pub(crate) f32); + +impl Mul for Pixels { + type Output = Pixels; + + fn mul(self, other: f32) -> Pixels { + Pixels(self.0 * other) + } +} + +impl Mul for Pixels { + type Output = Pixels; + + fn mul(self, rhs: Pixels) -> Self::Output { + Pixels(self.0 * rhs.0) + } +} + +impl Pixels { + pub fn round(&self) -> Self { + Self(self.0.round()) + } +} + +impl Eq for Pixels {} + +impl Ord for Pixels { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.partial_cmp(&other.0).unwrap() + } +} + +impl std::hash::Hash for Pixels { + fn hash(&self, state: &mut H) { + self.0.to_bits().hash(state); + } +} + +unsafe impl bytemuck::Pod for Pixels {} +unsafe impl bytemuck::Zeroable for Pixels {} + +impl Debug for Pixels { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} px", self.0) + } +} + +impl From for f32 { + fn from(pixels: Pixels) -> Self { + pixels.0 + } +} + +impl From<&Pixels> for f32 { + fn from(pixels: &Pixels) -> Self { + pixels.0 + } +} + +#[derive(Clone, Copy, Default, Add, Sub, Mul, Div)] +pub struct Rems(f32); + +impl Mul for Rems { + type Output = Pixels; + + fn mul(self, other: Pixels) -> Pixels { + Pixels(self.0 * other.0) + } +} + +impl Debug for Rems { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} rem", self.0) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum AbsoluteLength { + Pixels(Pixels), + Rems(Rems), +} + +impl From for AbsoluteLength { + fn from(pixels: Pixels) -> Self { + AbsoluteLength::Pixels(pixels) + } +} + +impl From for AbsoluteLength { + fn from(rems: Rems) -> Self { + AbsoluteLength::Rems(rems) + } +} + +impl AbsoluteLength { + pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { + match self { + AbsoluteLength::Pixels(pixels) => *pixels, + AbsoluteLength::Rems(rems) => *rems * rem_size, + } + } +} + +impl Default for AbsoluteLength { + fn default() -> Self { + px(0.).into() + } +} + +/// A non-auto length that can be defined in pixels, rems, or percent of parent. +#[derive(Clone, Copy)] +pub enum DefiniteLength { + Absolute(AbsoluteLength), + /// A fraction of the parent's size between 0 and 1. + Fraction(f32), +} + +impl Debug for DefiniteLength { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DefiniteLength::Absolute(length) => Debug::fmt(length, f), + DefiniteLength::Fraction(fract) => write!(f, "{}%", (fract * 100.0) as i32), + } + } +} + +impl From for DefiniteLength { + fn from(pixels: Pixels) -> Self { + Self::Absolute(pixels.into()) + } +} + +impl From for DefiniteLength { + fn from(rems: Rems) -> Self { + Self::Absolute(rems.into()) + } +} + +impl From for DefiniteLength { + fn from(length: AbsoluteLength) -> Self { + Self::Absolute(length) + } +} + +impl Default for DefiniteLength { + fn default() -> Self { + Self::Absolute(AbsoluteLength::default()) + } +} + +/// A length that can be defined in pixels, rems, percent of parent, or auto. +#[derive(Clone, Copy)] +pub enum Length { + Definite(DefiniteLength), + Auto, +} + +impl Debug for Length { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Length::Definite(definite_length) => write!(f, "{:?}", definite_length), + Length::Auto => write!(f, "auto"), + } + } +} + +pub fn relative>(fraction: f32) -> T { + DefiniteLength::Fraction(fraction).into() +} + +pub fn rems(rems: f32) -> Rems { + Rems(rems).into() +} + +pub fn px(pixels: f32) -> Pixels { + Pixels(pixels).into() +} + +pub fn auto() -> Length { + Length::Auto +} + +impl From for Length { + fn from(pixels: Pixels) -> Self { + Self::Definite(pixels.into()) + } +} + +impl From for Length { + fn from(rems: Rems) -> Self { + Self::Definite(rems.into()) + } +} + +impl From for Length { + fn from(length: DefiniteLength) -> Self { + Self::Definite(length) + } +} + +impl From for Length { + fn from(length: AbsoluteLength) -> Self { + Self::Definite(length.into()) + } +} + +impl Default for Length { + fn default() -> Self { + Self::Definite(DefiniteLength::default()) + } +} + +impl From<()> for Length { + fn from(_: ()) -> Self { + Self::Definite(DefiniteLength::default()) + } +} diff --git a/crates/gpui3/src/gpui3.rs b/crates/gpui3/src/gpui3.rs new file mode 100644 index 0000000000000000000000000000000000000000..2b03f3b3dfb4bf0b3c6f8468f6b72a43c9ce0f36 --- /dev/null +++ b/crates/gpui3/src/gpui3.rs @@ -0,0 +1,141 @@ +mod app; +mod color; +mod element; +mod elements; +mod fonts; +mod geometry; +mod platform; +mod renderer; +mod scene; +mod style; +mod taffy; +mod text; +mod window; + +use anyhow::Result; +pub use app::*; +pub use color::*; +pub use element::*; +pub use elements::*; +pub use fonts::*; +pub use geometry::*; +pub use platform::*; +pub use scene::*; +use std::ops::{Deref, DerefMut}; +pub use style::*; +pub use taffy::LayoutId; +use taffy::TaffyLayoutEngine; +use text::*; +pub use util::arc_cow::ArcCow; +pub use window::*; + +pub trait Context { + type EntityContext<'a, 'w, T: 'static>; + + fn entity( + &mut self, + build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T, + ) -> Handle; + + fn update_entity( + &mut self, + handle: &Handle, + update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R, + ) -> R; +} + +#[derive(Clone, Eq, PartialEq)] +pub struct SharedString(ArcCow<'static, str>); + +impl std::fmt::Debug for SharedString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl>> From for SharedString { + fn from(value: T) -> Self { + Self(value.into()) + } +} + +pub enum Reference<'a, T> { + Immutable(&'a T), + Mutable(&'a mut T), +} + +impl<'a, T> Deref for Reference<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + match self { + Reference::Immutable(target) => target, + Reference::Mutable(target) => target, + } + } +} + +impl<'a, T> DerefMut for Reference<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + Reference::Immutable(_) => { + panic!("cannot mutably deref an immutable reference. this is a bug in GPUI."); + } + Reference::Mutable(target) => target, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Workspace { + left_panel: AnyView, + } + + fn workspace(cx: &mut WindowContext) -> View { + let workspace = cx.entity(|cx| Workspace { + left_panel: collab_panel(cx).into_any(), + }); + view(workspace, |workspace, cx| { + div().child(workspace.left_panel.clone()) + }) + } + + struct CollabPanel { + filter_editor: Handle, + } + + fn collab_panel(cx: &mut WindowContext) -> View { + let panel = cx.entity(|cx| CollabPanel::new(cx)); + view(panel, |panel, cx| { + div().child(div()).child( + field(panel.filter_editor.clone()).placeholder_text("Search channels, contacts"), + ) + }) + } + + impl CollabPanel { + fn new(cx: &mut ViewContext) -> Self { + Self { + filter_editor: cx.entity(|cx| editor::Editor::new(cx)), + } + } + } + + struct Editor {} + + impl Editor { + pub fn new(cx: &mut ViewContext) -> Self { + Self {} + } + } + + #[test] + fn test() { + let mut cx = AppContext::test(); + + cx.open_window(WindowOptions::default(), |cx| workspace(cx)); + } +} diff --git a/crates/gpui3/src/platform.rs b/crates/gpui3/src/platform.rs new file mode 100644 index 0000000000000000000000000000000000000000..1dba1a3907eb019ccf003a2c1cb93b203bffc6d7 --- /dev/null +++ b/crates/gpui3/src/platform.rs @@ -0,0 +1,131 @@ +#[cfg(any(test, feature = "test"))] +mod test; +use crate::{ + AnyWindowHandle, Bounds, FontFeatures, FontId, FontMetrics, FontStyle, FontWeight, GlyphId, + LineLayout, Pixels, Point, RunStyle, SharedString, +}; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use std::sync::Arc; + +#[cfg(any(test, feature = "test"))] +pub use test::*; + +pub trait Platform { + fn font_system(&self) -> Arc; + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowOptions, + ) -> Box; +} + +pub trait PlatformWindow: HasRawWindowHandle + HasRawDisplayHandle {} + +pub trait PlatformFontSystem: Send + Sync { + fn add_fonts(&self, fonts: &[Arc>]) -> anyhow::Result<()>; + fn all_families(&self) -> Vec; + fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result>; + fn select_font( + &self, + font_ids: &[FontId], + weight: FontWeight, + style: FontStyle, + ) -> anyhow::Result; + fn font_metrics(&self, font_id: FontId) -> FontMetrics; + fn typographic_bounds( + &self, + font_id: FontId, + glyph_id: GlyphId, + ) -> anyhow::Result>; + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result>; + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; + fn rasterize_glyph( + &self, + font_id: FontId, + font_size: f32, + glyph_id: GlyphId, + subpixel_shift: Point, + scale_factor: f32, + options: RasterizationOptions, + ) -> Option<(Bounds, Vec)>; + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[(usize, RunStyle)]) -> LineLayout; + fn wrap_line( + &self, + text: &str, + font_id: FontId, + font_size: Pixels, + width: Pixels, + ) -> Vec; +} + +#[derive(Copy, Clone, Debug)] +pub enum RasterizationOptions { + Alpha, + Bgra, +} + +#[derive(Debug)] +pub struct WindowOptions { + pub bounds: WindowBounds, + pub titlebar: Option, + pub center: bool, + pub focus: bool, + pub show: bool, + pub kind: WindowKind, + pub is_movable: bool, +} + +impl Default for WindowOptions { + fn default() -> Self { + Self { + bounds: WindowBounds::default(), + titlebar: Some(TitlebarOptions { + title: Default::default(), + appears_transparent: Default::default(), + traffic_light_position: Default::default(), + }), + center: false, + focus: true, + show: true, + kind: WindowKind::Normal, + is_movable: true, + } + } +} + + +#[derive(Debug, Default)] +pub struct TitlebarOptions { + pub title: Option, + pub appears_transparent: bool, + pub traffic_light_position: Option>, +} + +#[derive(Copy, Clone, Debug)] +pub enum Appearance { + Light, + VibrantLight, + Dark, + VibrantDark, +} + +impl Default for Appearance { + fn default() -> Self { + Self::Light + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum WindowKind { + Normal, + PopUp, +} + +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub enum WindowBounds { + Fullscreen, + #[default] + Maximized, + Fixed(Bounds), +} diff --git a/crates/gpui3/src/platform/test.rs b/crates/gpui3/src/platform/test.rs new file mode 100644 index 0000000000000000000000000000000000000000..a62db5b2f1bfccbf8c8860eb953852ee578c40c7 --- /dev/null +++ b/crates/gpui3/src/platform/test.rs @@ -0,0 +1,23 @@ +use super::Platform; + +pub struct TestPlatform; + +impl TestPlatform { + pub fn new() -> Self { + TestPlatform + } +} + +impl Platform for TestPlatform { + fn font_system(&self) -> std::sync::Arc { + todo!() + } + + fn open_window( + &self, + handle: crate::AnyWindowHandle, + options: crate::WindowOptions, + ) -> Box { + todo!() + } +} diff --git a/crates/gpui3/src/renderer.rs b/crates/gpui3/src/renderer.rs new file mode 100644 index 0000000000000000000000000000000000000000..9bcd347f8d26b5aa784cf12392228e5c6db97665 --- /dev/null +++ b/crates/gpui3/src/renderer.rs @@ -0,0 +1,164 @@ +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + +use super::{Scene, Size}; + +pub struct Renderer { + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface, + surface_config: wgpu::SurfaceConfiguration, + pipeline: wgpu::RenderPipeline, + vertex_buffer: wgpu::Buffer, + vertex_count: u32, +} + +pub trait Window: HasRawWindowHandle + HasRawDisplayHandle { + fn inner_size(&self) -> Size; +} + +impl Renderer { + pub async fn new(window: &W) -> Self + where + W: Window, + { + let instance = wgpu::Instance::new(Default::default()); + let surface = unsafe { instance.create_surface(window).unwrap() }; + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions::default()) + .await + .unwrap(); + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor::default(), None) + .await + .unwrap(); + + let surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: window.inner_size().width, + height: window.inner_size().height, + + // "FIFO" mode renders frames in queue synced with the display's refresh rate. + // Avoids screen tearing but may not offer the lowest latency. Ideal when image + // quality takes priority over input latency. + present_mode: wgpu::PresentMode::Fifo, + + // Use the Premultiplied alpha mode. With premultiplication, the color components + // are multiplied by the alpha value before storage or blending, meaning calculations + // with colors already factor in the influence of alpha. This typically results + // in better performance and avoids a separate multiplication operation during blending. + alpha_mode: wgpu::CompositeAlphaMode::PreMultiplied, + + // Specify the color formats for the views the surface can have. + // In this case, the format is BGRA (blue, green, red, alpha) with unsigned + // normalised integers in the 8-bit range and the color space is sRGB (standard RGB). + // sRGB is the standard color space for displaying images and video on digital displays, + // as it optimises color accuracy and consistency. + view_formats: vec![wgpu::TextureFormat::Bgra8UnormSrgb], + }; + + surface.configure(&device, &surface_config); + + let vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Vertex Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shader.vert.wgsl").into()), + }); + + let fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Fragment Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shader.frag.wgsl").into()), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render Pipeline Layout"), + bind_group_layouts: &[], + push_constant_ranges: &[], + }); + + let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Vertex Buffer"), + size: 0, + usage: wgpu::BufferUsages::VERTEX, + mapped_at_creation: false, + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vs_module, + entry_point: "main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &fs_module, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: surface_config.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + Self { + device, + queue, + surface, + surface_config, + pipeline, + vertex_buffer, + vertex_count: 0, + } + } + + pub fn render(&mut self, scene: &Scene) { + let frame = self.surface.get_current_texture().unwrap(); + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + self.queue.write_buffer( + &self.vertex_buffer, + 0, + bytemuck::cast_slice(&scene.opaque_primitives().quads), + ); + self.vertex_count = scene.opaque_primitives().quads.len() as u32; + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: true, + }, + })], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + render_pass.draw(0..self.vertex_count, 0..1); + } + + self.queue.submit(std::iter::once(encoder.finish())); + } +} diff --git a/crates/gpui3/src/scene.rs b/crates/gpui3/src/scene.rs new file mode 100644 index 0000000000000000000000000000000000000000..4614e0f6703f8afff54e3fceb98a02fceade37d5 --- /dev/null +++ b/crates/gpui3/src/scene.rs @@ -0,0 +1,141 @@ +use super::{Bounds, Hsla, Pixels, Point}; +use bytemuck::{Pod, Zeroable}; +use plane_split::BspSplitter; + +pub struct Scene { + opaque_primitives: PrimitiveBatch, + transparent_primitives: slotmap::SlotMap, + splitter: BspSplitter, +} + +impl Scene { + pub fn new() -> Scene { + Scene { + opaque_primitives: PrimitiveBatch::default(), + transparent_primitives: slotmap::SlotMap::new(), + splitter: BspSplitter::new(), + } + } + + pub fn insert(&mut self, primitive: impl Into, is_transparent: bool) { + if is_transparent { + self.transparent_primitives.insert(primitive.into()); + } else { + match primitive.into() { + Primitive::Quad(quad) => self.opaque_primitives.quads.push(quad), + Primitive::Glyph(glyph) => self.opaque_primitives.glyphs.push(glyph), + Primitive::Underline(underline) => { + self.opaque_primitives.underlines.push(underline) + } + } + } + } + + pub fn opaque_primitives(&self) -> &PrimitiveBatch { + &self.opaque_primitives + } +} + +#[derive(Clone, Debug)] +pub enum Primitive { + Quad(Quad), + Glyph(Glyph), + Underline(Underline), +} + +impl Primitive { + pub fn is_transparent(&self) -> bool { + match self { + Primitive::Quad(quad) => { + quad.background.is_transparent() && quad.border_color.is_transparent() + } + Primitive::Glyph(glyph) => glyph.color.is_transparent(), + Primitive::Underline(underline) => underline.color.is_transparent(), + } + } +} + +#[derive(Default)] +pub struct PrimitiveBatch { + pub quads: Vec, + pub glyphs: Vec, + pub underlines: Vec, +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Quad { + pub order: f32, + pub bounds: Bounds, + pub background: Hsla, + pub border_color: Hsla, + pub corner_radius: Pixels, + pub border_left: Pixels, + pub border_right: Pixels, + pub border_top: Pixels, + pub border_bottom: Pixels, +} + +impl Quad { + pub fn vertices(&self) -> impl Iterator> { + let x1 = self.bounds.origin.x; + let y1 = self.bounds.origin.y; + let x2 = x1 + self.bounds.size.width; + let y2 = y1 + self.bounds.size.height; + [ + Point::new(x1, y1), + Point::new(x2, y1), + Point::new(x2, y2), + Point::new(x1, y2), + ] + .into_iter() + } +} + +unsafe impl Zeroable for Quad {} + +unsafe impl Pod for Quad {} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct GlyphId(u32); + +#[derive(Clone, Debug)] +pub struct Glyph { + pub id: GlyphId, + pub position: Point, + pub color: Hsla, + pub index: usize, + pub is_emoji: bool, +} + +impl From for Primitive { + fn from(quad: Quad) -> Self { + Primitive::Quad(quad) + } +} + +impl From for Primitive { + fn from(glyph: Glyph) -> Self { + Primitive::Glyph(glyph) + } +} + +#[derive(Copy, Clone, Default, Debug)] +#[repr(C)] +pub struct Underline { + pub origin: Point, + pub width: Pixels, + pub thickness: Pixels, + pub color: Hsla, + pub squiggly: bool, +} + +unsafe impl Zeroable for Underline {} + +unsafe impl Pod for Underline {} + +impl From for Primitive { + fn from(underline: Underline) -> Self { + Primitive::Underline(underline) + } +} diff --git a/crates/gpui3/src/shader.frag.wgsl b/crates/gpui3/src/shader.frag.wgsl new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/crates/gpui3/src/shader.frag.wgsl @@ -0,0 +1 @@ + diff --git a/crates/gpui3/src/shader.vert.wgsl b/crates/gpui3/src/shader.vert.wgsl new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/gpui3/src/style.rs b/crates/gpui3/src/style.rs new file mode 100644 index 0000000000000000000000000000000000000000..49c5ebe8ec3d41e83c4abc3b4c109d2af882f27c --- /dev/null +++ b/crates/gpui3/src/style.rs @@ -0,0 +1,345 @@ +use super::{ + rems, AbsoluteLength, Bounds, DefiniteLength, Edges, EdgesRefinement, FontStyle, FontWeight, + Hsla, Length, Pixels, Point, PointRefinement, Rems, Result, RunStyle, SharedString, Size, + SizeRefinement, ViewContext, WindowContext, +}; +use crate::{FontCache}; +use refineable::Refineable; +pub use taffy::style::{ + AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent, + Overflow, Position, +}; + +#[derive(Clone, Refineable, Debug)] +#[refineable(debug)] +pub struct Style { + /// What layout strategy should be used? + pub display: Display, + + // Overflow properties + /// How children overflowing their container should affect layout + #[refineable] + pub overflow: Point, + /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. + pub scrollbar_width: f32, + + // Position properties + /// What should the `position` value of this struct use as a base offset? + pub position: Position, + /// How should the position of this element be tweaked relative to the layout defined? + #[refineable] + pub inset: Edges, + + // Size properies + /// Sets the initial size of the item + #[refineable] + pub size: Size, + /// Controls the minimum size of the item + #[refineable] + pub min_size: Size, + /// Controls the maximum size of the item + #[refineable] + pub max_size: Size, + /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height. + pub aspect_ratio: Option, + + // Spacing Properties + /// How large should the margin be on each side? + #[refineable] + pub margin: Edges, + /// How large should the padding be on each side? + #[refineable] + pub padding: Edges, + /// How large should the border be on each side? + #[refineable] + pub border_widths: Edges, + + // Alignment properties + /// How this node's children aligned in the cross/block axis? + pub align_items: Option, + /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set + pub align_self: Option, + /// How should content contained within this item be aligned in the cross/block axis + pub align_content: Option, + /// How should contained within this item be aligned in the main/inline axis + pub justify_content: Option, + /// How large should the gaps between items in a flex container be? + #[refineable] + pub gap: Size, + + // Flexbox properies + /// Which direction does the main axis flow in? + pub flex_direction: FlexDirection, + /// Should elements wrap, or stay in a single line? + pub flex_wrap: FlexWrap, + /// Sets the initial main axis size of the item + pub flex_basis: Length, + /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive. + pub flex_grow: f32, + /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive. + pub flex_shrink: f32, + + /// The fill color of this element + pub fill: Option, + + /// The border color of this element + pub border_color: Option, + + /// The radius of the corners of this element + #[refineable] + pub corner_radii: CornerRadii, + + /// The color of text within this element. Cascades to children unless overridden. + pub text_color: Option, + + /// The font size in rems. + pub font_size: Option, + + pub font_family: Option, + + pub font_weight: Option, + + pub font_style: Option, +} + +#[derive(Refineable, Clone, Debug)] +#[refineable(debug)] +pub struct TextStyle { + pub color: Hsla, + pub font_family: SharedString, + pub font_size: Rems, + pub font_weight: FontWeight, + pub font_style: FontStyle, + pub underline: Option, +} + +impl TextStyle { + pub fn highlight(mut self, style: HighlightStyle, _font_cache: &FontCache) -> Result { + if let Some(weight) = style.font_weight { + self.font_weight = weight; + } + if let Some(style) = style.font_style { + self.font_style = style; + } + + if let Some(color) = style.color { + self.color = self.color.blend(color); + } + + if let Some(factor) = style.fade_out { + self.color.fade_out(factor); + } + + if let Some(underline) = style.underline { + self.underline = Some(underline); + } + + Ok(self) + } + + pub fn to_run(&self) -> RunStyle { + RunStyle { + font_id: todo!(), + color: self.color, + underline: self.underline.clone(), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct HighlightStyle { + pub color: Option, + pub font_weight: Option, + pub font_style: Option, + pub underline: Option, + pub fade_out: Option, +} + +impl Eq for HighlightStyle {} + +impl Style { + pub fn text_style(&self, _cx: &WindowContext) -> Option { + if self.text_color.is_none() + && self.font_size.is_none() + && self.font_family.is_none() + && self.font_weight.is_none() + && self.font_style.is_none() + { + return None; + } + + Some(TextStyleRefinement { + color: self.text_color, + font_family: self.font_family.clone(), + font_size: self.font_size, + font_weight: self.font_weight, + font_style: self.font_style, + underline: None, + }) + } + + /// Paints the background of an element styled with this style. + pub fn paint_background(&self, _bounds: Bounds, cx: &mut ViewContext) { + let _rem_size = cx.rem_size(); + if let Some(_color) = self.fill.as_ref().and_then(Fill::color) { + todo!(); + } + } + + /// Paints the foreground of an element styled with this style. + pub fn paint_foreground(&self, _bounds: Bounds, cx: &mut ViewContext) { + let rem_size = cx.rem_size(); + + if let Some(_color) = self.border_color { + let border = self.border_widths.to_pixels(rem_size); + if !border.is_empty() { + todo!(); + } + } + } +} + +impl Default for Style { + fn default() -> Self { + Style { + display: Display::Block, + overflow: Point { + x: Overflow::Visible, + y: Overflow::Visible, + }, + scrollbar_width: 0.0, + position: Position::Relative, + inset: Edges::auto(), + margin: Edges::::zero(), + padding: Edges::::zero(), + border_widths: Edges::::zero(), + size: Size::auto(), + min_size: Size::auto(), + max_size: Size::auto(), + aspect_ratio: None, + gap: Size::zero(), + // Aligment + align_items: None, + align_self: None, + align_content: None, + justify_content: None, + // Flexbox + flex_direction: FlexDirection::Row, + flex_wrap: FlexWrap::NoWrap, + flex_grow: 0.0, + flex_shrink: 1.0, + flex_basis: Length::Auto, + fill: None, + border_color: None, + corner_radii: CornerRadii::default(), + text_color: None, + font_size: Some(rems(1.)), + font_family: None, + font_weight: None, + font_style: None, + } + } +} + +#[derive(Refineable, Clone, Default, Debug, PartialEq, Eq)] +#[refineable(debug)] +pub struct UnderlineStyle { + pub thickness: Pixels, + pub color: Option, + pub squiggly: bool, +} + +#[derive(Clone, Debug)] +pub enum Fill { + Color(Hsla), +} + +impl Fill { + pub fn color(&self) -> Option { + match self { + Fill::Color(color) => Some(*color), + } + } +} + +impl Default for Fill { + fn default() -> Self { + Self::Color(Hsla::default()) + } +} + +impl From for Fill { + fn from(color: Hsla) -> Self { + Self::Color(color) + } +} + +#[derive(Clone, Refineable, Default, Debug)] +#[refineable(debug)] +pub struct CornerRadii { + top_left: AbsoluteLength, + top_right: AbsoluteLength, + bottom_left: AbsoluteLength, + bottom_right: AbsoluteLength, +} + +impl From for HighlightStyle { + fn from(other: TextStyle) -> Self { + Self::from(&other) + } +} + +impl From<&TextStyle> for HighlightStyle { + fn from(other: &TextStyle) -> Self { + Self { + color: Some(other.color), + font_weight: Some(other.font_weight), + font_style: Some(other.font_style), + underline: other.underline.clone(), + fade_out: None, + } + } +} + +impl HighlightStyle { + pub fn highlight(&mut self, other: HighlightStyle) { + match (self.color, other.color) { + (Some(self_color), Some(other_color)) => { + self.color = Some(Hsla::blend(other_color, self_color)); + } + (None, Some(other_color)) => { + self.color = Some(other_color); + } + _ => {} + } + + if other.font_weight.is_some() { + self.font_weight = other.font_weight; + } + + if other.font_style.is_some() { + self.font_style = other.font_style; + } + + if other.underline.is_some() { + self.underline = other.underline; + } + + match (other.fade_out, self.fade_out) { + (Some(source_fade), None) => self.fade_out = Some(source_fade), + (Some(source_fade), Some(dest_fade)) => { + self.fade_out = Some((dest_fade * (1. + source_fade)).clamp(0., 1.)); + } + _ => {} + } + } +} + +impl From for HighlightStyle { + fn from(color: Hsla) -> Self { + Self { + color: Some(color), + ..Default::default() + } + } +} diff --git a/crates/gpui3/src/taffy.rs b/crates/gpui3/src/taffy.rs new file mode 100644 index 0000000000000000000000000000000000000000..ab3d45f745297bbf6c64b2cd68c9f7a1f33e7b9e --- /dev/null +++ b/crates/gpui3/src/taffy.rs @@ -0,0 +1,236 @@ +use super::{ + AbsoluteLength, Bounds, DefiniteLength, Edges, Layout, Length, Pixels, Point, Result, Size, + Style, +}; +use std::fmt::Debug; +pub use taffy::tree::NodeId as LayoutId; +pub use taffy::*; +pub struct TaffyLayoutEngine(Taffy); + +impl TaffyLayoutEngine { + pub fn new() -> Self { + TaffyLayoutEngine(Taffy::new()) + } + + pub fn request_layout( + &mut self, + style: Style, + rem_size: Pixels, + children: &[LayoutId], + ) -> Result { + let style = style.to_taffy(rem_size); + if children.is_empty() { + Ok(self.0.new_leaf(style)?) + } else { + Ok(self.0.new_with_children(style, children)?) + } + } + + pub fn layout(&mut self, id: LayoutId) -> Result { + Ok(self.0.layout(id).map(Into::into)?) + } +} + +trait ToTaffy { + fn to_taffy(&self, rem_size: Pixels) -> Output; +} + +impl ToTaffy for Style { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style { + taffy::style::Style { + display: self.display, + overflow: self.overflow.clone().into(), + scrollbar_width: self.scrollbar_width, + position: self.position, + inset: self.inset.to_taffy(rem_size), + size: self.size.to_taffy(rem_size), + min_size: self.min_size.to_taffy(rem_size), + max_size: self.max_size.to_taffy(rem_size), + aspect_ratio: self.aspect_ratio, + margin: self.margin.to_taffy(rem_size), + padding: self.padding.to_taffy(rem_size), + border: self.border_widths.to_taffy(rem_size), + align_items: self.align_items, + align_self: self.align_self, + align_content: self.align_content, + justify_content: self.justify_content, + gap: self.gap.to_taffy(rem_size), + flex_direction: self.flex_direction, + flex_wrap: self.flex_wrap, + flex_basis: self.flex_basis.to_taffy(rem_size), + flex_grow: self.flex_grow, + flex_shrink: self.flex_shrink, + ..Default::default() // Ignore grid properties for now + } + } +} + +// impl ToTaffy for Bounds { +// type Output = taffy::prelude::Bounds; + +// fn to_taffy( +// &self, +// rem_size: Pixels, +// ) -> taffy::prelude::Bounds { +// taffy::prelude::Bounds { +// origin: self.origin.to_taffy(rem_size), +// size: self.size.to_taffy(rem_size), +// } +// } +// } + +impl ToTaffy for Length { + fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto { + match self { + Length::Definite(length) => length.to_taffy(rem_size), + Length::Auto => taffy::prelude::LengthPercentageAuto::Auto, + } + } +} + +impl ToTaffy for Length { + fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::Dimension { + match self { + Length::Definite(length) => length.to_taffy(rem_size), + Length::Auto => taffy::prelude::Dimension::Auto, + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage { + match self { + DefiniteLength::Absolute(length) => match length { + AbsoluteLength::Pixels(pixels) => { + taffy::style::LengthPercentage::Length(pixels.into()) + } + AbsoluteLength::Rems(rems) => { + taffy::style::LengthPercentage::Length((*rems * rem_size).into()) + } + }, + DefiniteLength::Fraction(fraction) => { + taffy::style::LengthPercentage::Percent(*fraction) + } + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentageAuto { + match self { + DefiniteLength::Absolute(length) => match length { + AbsoluteLength::Pixels(pixels) => { + taffy::style::LengthPercentageAuto::Length(pixels.into()) + } + AbsoluteLength::Rems(rems) => { + taffy::style::LengthPercentageAuto::Length((*rems * rem_size).into()) + } + }, + DefiniteLength::Fraction(fraction) => { + taffy::style::LengthPercentageAuto::Percent(*fraction) + } + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Dimension { + match self { + DefiniteLength::Absolute(length) => match length { + AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::Length(pixels.into()), + AbsoluteLength::Rems(rems) => { + taffy::style::Dimension::Length((*rems * rem_size).into()) + } + }, + DefiniteLength::Fraction(fraction) => taffy::style::Dimension::Percent(*fraction), + } + } +} + +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage { + match self { + AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(pixels.into()), + AbsoluteLength::Rems(rems) => { + taffy::style::LengthPercentage::Length((*rems * rem_size).into()) + } + } + } +} + +impl From> for Point +where + T: Into, +{ + fn from(point: taffy::geometry::Point) -> Point { + Point { + x: point.x.into(), + y: point.y.into(), + } + } +} + +impl Into> for Point +where + T: Into, +{ + fn into(self) -> taffy::geometry::Point { + taffy::geometry::Point { + x: self.x.into(), + y: self.y.into(), + } + } +} + +impl + Clone + Debug, U> ToTaffy> for Size { + fn to_taffy(&self, rem_size: Pixels) -> taffy::geometry::Size { + taffy::geometry::Size { + width: self.width.to_taffy(rem_size).into(), + height: self.height.to_taffy(rem_size).into(), + } + } +} + +impl ToTaffy> for Edges +where + T: ToTaffy + Clone + Debug, +{ + fn to_taffy(&self, rem_size: Pixels) -> taffy::geometry::Rect { + taffy::geometry::Rect { + top: self.top.to_taffy(rem_size).into(), + right: self.right.to_taffy(rem_size).into(), + bottom: self.bottom.to_taffy(rem_size).into(), + left: self.left.to_taffy(rem_size).into(), + } + } +} + +impl From> for Size +where + S: Into, +{ + fn from(value: taffy::geometry::Size) -> Self { + Self { + width: value.width.into(), + height: value.height.into(), + } + } +} + +impl From<&taffy::tree::Layout> for Layout { + fn from(layout: &taffy::tree::Layout) -> Self { + Layout { + order: layout.order, + bounds: Bounds { + origin: layout.location.into(), + size: layout.size.into(), + }, + } + } +} + +impl From for Pixels { + fn from(pixels: f32) -> Self { + Pixels(pixels) + } +} diff --git a/crates/gpui3/src/text.rs b/crates/gpui3/src/text.rs new file mode 100644 index 0000000000000000000000000000000000000000..0e6d7500ee1f8fb247f4540f9022f4eb3b92631f --- /dev/null +++ b/crates/gpui3/src/text.rs @@ -0,0 +1,852 @@ +use crate::{black, px}; + +use super::{ + point, Bounds, FontId, Glyph, Hsla, Pixels, PlatformFontSystem, Point, UnderlineStyle, + WindowContext, +}; +use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use smallvec::SmallVec; +use std::{ + borrow::Borrow, + collections::HashMap, + hash::{Hash, Hasher}, + iter, + sync::Arc, +}; + +pub struct TextLayoutCache { + prev_frame: Mutex>>, + curr_frame: RwLock>>, + fonts: Arc, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RunStyle { + pub color: Hsla, + pub font_id: FontId, + pub underline: Option, +} + +impl TextLayoutCache { + pub fn new(fonts: Arc) -> Self { + Self { + prev_frame: Mutex::new(HashMap::new()), + curr_frame: RwLock::new(HashMap::new()), + fonts, + } + } + + pub fn finish_frame(&self) { + let mut prev_frame = self.prev_frame.lock(); + let mut curr_frame = self.curr_frame.write(); + std::mem::swap(&mut *prev_frame, &mut *curr_frame); + curr_frame.clear(); + } + + pub fn layout_str<'a>( + &'a self, + text: &'a str, + font_size: Pixels, + runs: &'a [(usize, RunStyle)], + ) -> Line { + let key = &CacheKeyRef { + text, + font_size, + runs, + } as &dyn CacheKey; + let curr_frame = self.curr_frame.upgradable_read(); + if let Some(layout) = curr_frame.get(key) { + return Line::new(layout.clone(), runs); + } + + let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame); + if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) { + curr_frame.insert(key, layout.clone()); + Line::new(layout, runs) + } else { + let layout = Arc::new(self.fonts.layout_line(text, font_size, runs)); + let key = CacheKeyValue { + text: text.into(), + font_size, + runs: SmallVec::from(runs), + }; + curr_frame.insert(key, layout.clone()); + Line::new(layout, runs) + } + } +} + +trait CacheKey { + fn key(&self) -> CacheKeyRef; +} + +impl<'a> PartialEq for (dyn CacheKey + 'a) { + fn eq(&self, other: &dyn CacheKey) -> bool { + self.key() == other.key() + } +} + +impl<'a> Eq for (dyn CacheKey + 'a) {} + +impl<'a> Hash for (dyn CacheKey + 'a) { + fn hash(&self, state: &mut H) { + self.key().hash(state) + } +} + +#[derive(Eq)] +struct CacheKeyValue { + text: String, + font_size: Pixels, + runs: SmallVec<[(usize, RunStyle); 1]>, +} + +impl CacheKey for CacheKeyValue { + fn key(&self) -> CacheKeyRef { + CacheKeyRef { + text: self.text.as_str(), + font_size: self.font_size, + runs: self.runs.as_slice(), + } + } +} + +impl PartialEq for CacheKeyValue { + fn eq(&self, other: &Self) -> bool { + self.key().eq(&other.key()) + } +} + +impl Hash for CacheKeyValue { + fn hash(&self, state: &mut H) { + self.key().hash(state); + } +} + +impl<'a> Borrow for CacheKeyValue { + fn borrow(&self) -> &(dyn CacheKey + 'a) { + self as &dyn CacheKey + } +} + +#[derive(Copy, Clone)] +struct CacheKeyRef<'a> { + text: &'a str, + font_size: Pixels, + runs: &'a [(usize, RunStyle)], +} + +impl<'a> CacheKey for CacheKeyRef<'a> { + fn key(&self) -> CacheKeyRef { + *self + } +} + +impl<'a> PartialEq for CacheKeyRef<'a> { + fn eq(&self, other: &Self) -> bool { + self.text == other.text + && self.font_size == other.font_size + && self.runs.len() == other.runs.len() + && self.runs.iter().zip(other.runs.iter()).all( + |((len_a, style_a), (len_b, style_b))| { + len_a == len_b && style_a.font_id == style_b.font_id + }, + ) + } +} + +impl<'a> Hash for CacheKeyRef<'a> { + fn hash(&self, state: &mut H) { + self.text.hash(state); + self.font_size.hash(state); + for (len, style_id) in self.runs { + len.hash(state); + style_id.font_id.hash(state); + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct Line { + layout: Arc, + style_runs: SmallVec<[StyleRun; 32]>, +} + +#[derive(Debug, Clone)] +struct StyleRun { + len: u32, + color: Hsla, + underline: UnderlineStyle, +} + +#[derive(Default, Debug)] +pub struct LineLayout { + pub font_size: Pixels, + pub width: Pixels, + pub ascent: Pixels, + pub descent: Pixels, + pub runs: Vec, + pub len: usize, +} + +#[derive(Debug)] +pub struct Run { + pub font_id: FontId, + pub glyphs: Vec, +} + +impl Line { + pub fn new(layout: Arc, runs: &[(usize, RunStyle)]) -> Self { + let mut style_runs = SmallVec::new(); + for (len, style) in runs { + style_runs.push(StyleRun { + len: *len as u32, + color: style.color, + underline: style.underline.clone().unwrap_or_default(), + }); + } + Self { layout, style_runs } + } + + pub fn runs(&self) -> &[Run] { + &self.layout.runs + } + + pub fn width(&self) -> Pixels { + self.layout.width + } + + pub fn font_size(&self) -> Pixels { + self.layout.font_size + } + + pub fn x_for_index(&self, index: usize) -> Pixels { + for run in &self.layout.runs { + for glyph in &run.glyphs { + if glyph.index >= index { + return glyph.position.x; + } + } + } + self.layout.width + } + + pub fn font_for_index(&self, index: usize) -> Option { + for run in &self.layout.runs { + for glyph in &run.glyphs { + if glyph.index >= index { + return Some(run.font_id); + } + } + } + + None + } + + pub fn len(&self) -> usize { + self.layout.len + } + + pub fn is_empty(&self) -> bool { + self.layout.len == 0 + } + + pub fn index_for_x(&self, x: Pixels) -> Option { + if x >= self.layout.width { + None + } else { + for run in self.layout.runs.iter().rev() { + for glyph in run.glyphs.iter().rev() { + if glyph.position.x <= x { + return Some(glyph.index); + } + } + } + Some(0) + } + } + + pub fn paint( + &self, + origin: Point, + visible_bounds: Bounds, + line_height: Pixels, + cx: &mut WindowContext, + ) { + let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.; + let baseline_offset = point(px(0.), padding_top + self.layout.ascent); + + let mut style_runs = self.style_runs.iter(); + let mut run_end = 0; + let mut color = black(); + let mut underline = None; + + for run in &self.layout.runs { + let max_glyph_width = cx + .font_cache() + .bounding_box(run.font_id, self.layout.font_size) + .width; + + for glyph in &run.glyphs { + let glyph_origin = origin + baseline_offset + glyph.position; + if glyph_origin.x > visible_bounds.upper_right().x { + break; + } + + let mut finished_underline: Option<(Point, UnderlineStyle)> = None; + if glyph.index >= run_end { + if let Some(style_run) = style_runs.next() { + if let Some((_, underline_style)) = &mut underline { + if style_run.underline != *underline_style { + finished_underline = underline.take(); + } + } + if style_run.underline.thickness > px(0.) { + underline.get_or_insert(( + point( + glyph_origin.x, + origin.y + baseline_offset.y + (self.layout.descent * 0.618), + ), + UnderlineStyle { + color: style_run.underline.color, + thickness: style_run.underline.thickness, + squiggly: style_run.underline.squiggly, + }, + )); + } + + run_end += style_run.len as usize; + color = style_run.color; + } else { + run_end = self.layout.len; + finished_underline = underline.take(); + } + } + + if glyph_origin.x + max_glyph_width < visible_bounds.origin.x { + continue; + } + + if let Some((_underline_origin, _underline_style)) = finished_underline { + // cx.scene().insert(Underline { + // origin: underline_origin, + // width: glyph_origin.x - underline_origin.x, + // thickness: underline_style.thickness.into(), + // color: underline_style.color.unwrap(), + // squiggly: underline_style.squiggly, + // }); + } + + // todo!() + // if glyph.is_emoji { + // cx.scene().push_image_glyph(scene::ImageGlyph { + // font_id: run.font_id, + // font_size: self.layout.font_size, + // id: glyph.id, + // origin: glyph_origin, + // }); + // } else { + // cx.scene().push_glyph(scene::Glyph { + // font_id: run.font_id, + // font_size: self.layout.font_size, + // id: glyph.id, + // origin: glyph_origin, + // color, + // }); + // } + } + } + + if let Some((_underline_start, _underline_style)) = underline.take() { + let _line_end_x = origin.x + self.layout.width; + // cx.scene().push_underline(Underline { + // origin: underline_start, + // width: line_end_x - underline_start.x, + // color: underline_style.color, + // thickness: underline_style.thickness.into(), + // squiggly: underline_style.squiggly, + // }); + } + } + + pub fn paint_wrapped( + &self, + origin: Point, + _visible_bounds: Bounds, + line_height: Pixels, + boundaries: &[ShapedBoundary], + cx: &mut WindowContext, + ) { + let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.; + let baseline_offset = point(px(0.), padding_top + self.layout.ascent); + + let mut boundaries = boundaries.into_iter().peekable(); + let mut color_runs = self.style_runs.iter(); + let mut style_run_end = 0; + let mut color = black(); + let mut underline: Option<(Point, UnderlineStyle)> = None; + + let mut glyph_origin = origin; + let mut prev_position = px(0.); + for (run_ix, run) in self.layout.runs.iter().enumerate() { + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_position; + + if boundaries + .peek() + .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix) + { + boundaries.next(); + if let Some((_underline_origin, _underline_style)) = underline.take() { + // cx.scene().push_underline(Underline { + // origin: underline_origin, + // width: glyph_origin.x - underline_origin.x, + // thickness: underline_style.thickness.into(), + // color: underline_style.color.unwrap(), + // squiggly: underline_style.squiggly, + // }); + } + + glyph_origin = point(origin.x, glyph_origin.y + line_height); + } + prev_position = glyph.position.x; + + let mut finished_underline = None; + if glyph.index >= style_run_end { + if let Some(style_run) = color_runs.next() { + style_run_end += style_run.len as usize; + color = style_run.color; + if let Some((_, underline_style)) = &mut underline { + if style_run.underline != *underline_style { + finished_underline = underline.take(); + } + } + if style_run.underline.thickness > px(0.) { + underline.get_or_insert(( + glyph_origin + + point( + px(0.), + baseline_offset.y + (self.layout.descent * 0.618), + ), + UnderlineStyle { + color: Some( + style_run.underline.color.unwrap_or(style_run.color), + ), + thickness: style_run.underline.thickness, + squiggly: style_run.underline.squiggly, + }, + )); + } + } else { + style_run_end = self.layout.len; + color = black(); + finished_underline = underline.take(); + } + } + + if let Some((_underline_origin, _underline_style)) = finished_underline { + // cx.scene().push_underline(Underline { + // origin: underline_origin, + // width: glyph_origin.x - underline_origin.x, + // thickness: underline_style.thickness.into(), + // color: underline_style.color.unwrap(), + // squiggly: underline_style.squiggly, + // }); + } + + let _glyph_bounds = Bounds { + origin: glyph_origin, + size: cx + .font_cache() + .bounding_box(run.font_id, self.layout.font_size), + }; + // todo!() + // if glyph_bounds.intersects(visible_bounds) { + // if glyph.is_emoji { + // cx.scene().push_image_glyph(scene::ImageGlyph { + // font_id: run.font_id, + // font_size: self.layout.font_size, + // id: glyph.id, + // origin: glyph_bounds.origin() + baseline_offset, + // }); + // } else { + // cx.scene().push_glyph(scene::Glyph { + // font_id: run.font_id, + // font_size: self.layout.font_size, + // id: glyph.id, + // origin: glyph_bounds.origin() + baseline_offset, + // color, + // }); + // } + // } + } + } + + if let Some((_underline_origin, _underline_style)) = underline.take() { + // let line_end_x = glyph_origin.x + self.layout.width - prev_position; + // cx.scene().push_underline(Underline { + // origin: underline_origin, + // width: line_end_x - underline_origin.x, + // thickness: underline_style.thickness.into(), + // color: underline_style.color, + // squiggly: underline_style.squiggly, + // }); + } + } +} + +impl Run { + pub fn glyphs(&self) -> &[Glyph] { + &self.glyphs + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Boundary { + pub ix: usize, + pub next_indent: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ShapedBoundary { + pub run_ix: usize, + pub glyph_ix: usize, +} + +impl Boundary { + fn new(ix: usize, next_indent: u32) -> Self { + Self { ix, next_indent } + } +} + +pub struct LineWrapper { + font_system: Arc, + pub(crate) font_id: FontId, + pub(crate) font_size: Pixels, + cached_ascii_char_widths: [Option; 128], + cached_other_char_widths: HashMap, +} + +impl LineWrapper { + pub const MAX_INDENT: u32 = 256; + + pub fn new( + font_id: FontId, + font_size: Pixels, + font_system: Arc, + ) -> Self { + Self { + font_system, + font_id, + font_size, + cached_ascii_char_widths: [None; 128], + cached_other_char_widths: HashMap::new(), + } + } + + pub fn wrap_line<'a>( + &'a mut self, + line: &'a str, + wrap_width: Pixels, + ) -> impl Iterator + 'a { + let mut width = px(0.); + let mut first_non_whitespace_ix = None; + let mut indent = None; + let mut last_candidate_ix = 0; + let mut last_candidate_width = px(0.); + let mut last_wrap_ix = 0; + let mut prev_c = '\0'; + let mut char_indices = line.char_indices(); + iter::from_fn(move || { + for (ix, c) in char_indices.by_ref() { + if c == '\n' { + continue; + } + + if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + let char_width = self.width_for_char(c); + width += char_width; + if width > wrap_width && ix > last_wrap_ix { + if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix) + { + indent = Some( + Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32), + ); + } + + if last_candidate_ix > 0 { + last_wrap_ix = last_candidate_ix; + width -= last_candidate_width; + last_candidate_ix = 0; + } else { + last_wrap_ix = ix; + width = char_width; + } + + if let Some(indent) = indent { + width += self.width_for_char(' ') * indent as f32; + } + + return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); + } + prev_c = c; + } + + None + }) + } + + pub fn wrap_shaped_line<'a>( + &'a mut self, + str: &'a str, + line: &'a Line, + wrap_width: Pixels, + ) -> impl Iterator + 'a { + let mut first_non_whitespace_ix = None; + let mut last_candidate_ix = None; + let mut last_candidate_x = px(0.); + let mut last_wrap_ix = ShapedBoundary { + run_ix: 0, + glyph_ix: 0, + }; + let mut last_wrap_x = px(0.); + let mut prev_c = '\0'; + let mut glyphs = line + .runs() + .iter() + .enumerate() + .flat_map(move |(run_ix, run)| { + run.glyphs() + .iter() + .enumerate() + .map(move |(glyph_ix, glyph)| { + let character = str[glyph.index..].chars().next().unwrap(); + ( + ShapedBoundary { run_ix, glyph_ix }, + character, + glyph.position.x, + ) + }) + }) + .peekable(); + + iter::from_fn(move || { + while let Some((ix, c, x)) = glyphs.next() { + if c == '\n' { + continue; + } + + if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { + last_candidate_ix = Some(ix); + last_candidate_x = x; + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + let next_x = glyphs.peek().map_or(line.width(), |(_, _, x)| *x); + let width = next_x - last_wrap_x; + if width > wrap_width && ix > last_wrap_ix { + if let Some(last_candidate_ix) = last_candidate_ix.take() { + last_wrap_ix = last_candidate_ix; + last_wrap_x = last_candidate_x; + } else { + last_wrap_ix = ix; + last_wrap_x = x; + } + + return Some(last_wrap_ix); + } + prev_c = c; + } + + None + }) + } + + fn is_boundary(&self, prev: char, next: char) -> bool { + (prev == ' ') && (next != ' ') + } + + #[inline(always)] + fn width_for_char(&mut self, c: char) -> Pixels { + if (c as u32) < 128 { + if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] { + cached_width + } else { + let width = self.compute_width_for_char(c); + self.cached_ascii_char_widths[c as usize] = Some(width); + width + } + } else { + if let Some(cached_width) = self.cached_other_char_widths.get(&c) { + *cached_width + } else { + let width = self.compute_width_for_char(c); + self.cached_other_char_widths.insert(c, width); + width + } + } + } + + fn compute_width_for_char(&self, c: char) -> Pixels { + self.font_system + .layout_line( + &c.to_string(), + self.font_size, + &[( + 1, + RunStyle { + font_id: self.font_id, + color: Default::default(), + underline: Default::default(), + }, + )], + ) + .width + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AppContext, FontWeight}; + + #[test] + fn test_wrap_line() { + let cx = AppContext::test(); + + let font_cache = cx.font_cache().clone(); + let font_system = cx.platform().font_system(); + let family = font_cache + .load_family(&["Courier"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family, Default::default(), Default::default()) + .unwrap(); + + let mut wrapper = LineWrapper::new(font_id, px(16.), font_system); + assert_eq!( + wrapper + .wrap_line("aa bbb cccc ddddd eeee", px(72.)) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0)) + .collect::>(), + &[ + Boundary::new(4, 0), + Boundary::new(11, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line(" aaaaaaa", px(72.)) + .collect::>(), + &[ + Boundary::new(7, 5), + Boundary::new(9, 5), + Boundary::new(11, 5), + ] + ); + assert_eq!( + wrapper + .wrap_line(" ", px(72.)) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 0), + Boundary::new(21, 0) + ] + ); + assert_eq!( + wrapper + .wrap_line(" aaaaaaaaaaaaaa", px(72.)) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 3), + Boundary::new(18, 3), + Boundary::new(22, 3), + ] + ); + } + + // todo! repeat this test + #[test] + fn test_wrap_shaped_line() { + let cx = AppContext::test(); + let font_cache = cx.font_cache().clone(); + let font_system = cx.platform().font_system(); + let text_layout_cache = TextLayoutCache::new(font_system.clone()); + + let family = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family, Default::default(), Default::default()) + .unwrap(); + let normal = RunStyle { + font_id, + color: Default::default(), + underline: Default::default(), + }; + let bold = RunStyle { + font_id: font_cache + .select_font(family, FontWeight::BOLD, Default::default()) + .unwrap(), + color: Default::default(), + underline: Default::default(), + }; + + let text = "aa bbb cccc ddddd eeee"; + let line = text_layout_cache.layout_str( + text, + px(16.), + &[ + (4, normal.clone()), + (5, bold.clone()), + (6, normal.clone()), + (1, bold), + (7, normal), + ], + ); + + let mut wrapper = LineWrapper::new(font_id, px(16.), font_system); + assert_eq!( + wrapper + .wrap_shaped_line(text, &line, px(72.)) + .collect::>(), + &[ + ShapedBoundary { + run_ix: 1, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 2, + glyph_ix: 3 + }, + ShapedBoundary { + run_ix: 4, + glyph_ix: 2 + } + ], + ); + } +} diff --git a/crates/gpui3/src/window.rs b/crates/gpui3/src/window.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b3389260173959ecd358104059e5241d389f5d9 --- /dev/null +++ b/crates/gpui3/src/window.rs @@ -0,0 +1,251 @@ +use super::{ + px, taffy::LayoutId, AppContext, Bounds, Context, EntityId, Handle, Pixels, Reference, Style, + TaffyLayoutEngine, +}; +use anyhow::Result; +use derive_more::{Deref, DerefMut}; +use std::{ + any::{Any, TypeId}, + marker::PhantomData, +}; + +pub struct AnyWindow {} + +pub struct Window { + id: WindowId, + rem_size: Pixels, + layout_engine: TaffyLayoutEngine, + pub(crate) root_view: Option>, +} + +impl Window { + pub fn new(id: WindowId) -> Window { + Window { + id, + layout_engine: TaffyLayoutEngine::new(), + rem_size: px(16.), + root_view: None, + } + } +} + +#[derive(Deref, DerefMut)] +pub struct WindowContext<'a, 'b> { + #[deref] + #[deref_mut] + app: Reference<'a, AppContext>, + window: Reference<'b, Window>, +} + +impl<'a, 'w> WindowContext<'a, 'w> { + pub(crate) fn mutable(app: &'a mut AppContext, window: &'w mut Window) -> Self { + Self { + app: Reference::Mutable(app), + window: Reference::Mutable(window), + } + } + + pub(crate) fn immutable(app: &'a AppContext, window: &'w Window) -> Self { + Self { + app: Reference::Immutable(app), + window: Reference::Immutable(window), + } + } + + pub fn request_layout( + &mut self, + style: Style, + children: impl IntoIterator, + ) -> Result { + self.app.layout_id_buffer.clear(); + self.app.layout_id_buffer.extend(children.into_iter()); + let rem_size = self.rem_size(); + + self.window + .layout_engine + .request_layout(style, rem_size, &self.app.layout_id_buffer) + } + + pub fn layout(&mut self, layout_id: LayoutId) -> Result { + Ok(self + .window + .layout_engine + .layout(layout_id) + .map(Into::into)?) + } + + pub fn rem_size(&self) -> Pixels { + self.window.rem_size + } + + fn update_window( + &mut self, + window_id: WindowId, + update: impl FnOnce(&mut WindowContext) -> R, + ) -> Result { + if window_id == self.window.id { + Ok(update(self)) + } else { + self.app.update_window(window_id, update) + } + } +} + +impl Context for WindowContext<'_, '_> { + type EntityContext<'a, 'w, T: 'static> = ViewContext<'a, 'w, T>; + + fn entity( + &mut self, + build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T, + ) -> Handle { + let id = self.entities.insert(None); + let entity = Box::new(build_entity(&mut ViewContext::mutable( + &mut *self.app, + &mut self.window, + id, + ))); + self.entities.get_mut(id).unwrap().replace(entity); + + Handle { + id, + entity_type: PhantomData, + } + } + + fn update_entity( + &mut self, + handle: &Handle, + update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R, + ) -> R { + let mut entity = self + .app + .entities + .get_mut(handle.id) + .unwrap() + .take() + .unwrap() + .downcast::() + .unwrap(); + + let result = update( + &mut *entity, + &mut ViewContext::mutable(&mut *self.app, &mut *self.window, handle.id), + ); + + self.app + .entities + .get_mut(handle.id) + .unwrap() + .replace(entity); + + result + } +} + +#[derive(Deref, DerefMut)] +pub struct ViewContext<'a, 'w, T> { + #[deref] + #[deref_mut] + window_cx: WindowContext<'a, 'w>, + entity_type: PhantomData, + entity_id: EntityId, +} + +impl<'a, 'w, T: 'static> ViewContext<'a, 'w, T> { + // fn update(&mut self, update: impl FnOnce(&mut T, &mut Self) -> R) -> R { + + // self.window_cx.update_entity(handle, update) + + // let mut entity = self.window_cx.app.entities.remove(&self.entity_id).unwrap(); + // let result = update(entity.downcast_mut::().unwrap(), self); + // self.window_cx + // .app + // .entities + // .insert(self.entity_id, Box::new(entity)); + // result + // } + + fn mutable(app: &'a mut AppContext, window: &'w mut Window, entity_id: EntityId) -> Self { + Self { + window_cx: WindowContext::mutable(app, window), + entity_id, + entity_type: PhantomData, + } + } + + fn immutable(app: &'a AppContext, window: &'w Window, entity_id: EntityId) -> Self { + Self { + window_cx: WindowContext::immutable(app, window), + entity_id, + entity_type: PhantomData, + } + } +} + +impl<'a, 'w, T: 'static> Context for ViewContext<'a, 'w, T> { + type EntityContext<'b, 'c, U: 'static> = ViewContext<'b, 'c, U>; + + fn entity( + &mut self, + build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T2>) -> T2, + ) -> Handle { + self.window_cx.entity(build_entity) + } + + fn update_entity( + &mut self, + handle: &Handle, + update: impl FnOnce(&mut U, &mut Self::EntityContext<'_, '_, U>) -> R, + ) -> R { + self.window_cx.update_entity(handle, update) + } +} + +// #[derive(Clone, Copy, Eq, PartialEq, Hash)] +slotmap::new_key_type! { pub struct WindowId; } + +#[derive(PartialEq, Eq)] +pub struct WindowHandle { + id: WindowId, + state_type: PhantomData, +} + +impl Copy for WindowHandle {} + +impl Clone for WindowHandle { + fn clone(&self) -> Self { + WindowHandle { + id: self.id, + state_type: PhantomData, + } + } +} + +impl WindowHandle { + pub fn new(id: WindowId) -> Self { + WindowHandle { + id, + state_type: PhantomData, + } + } +} + +impl Into for WindowHandle { + fn into(self) -> AnyWindowHandle { + AnyWindowHandle { + id: self.id, + state_type: TypeId::of::(), + } + } +} + +pub struct AnyWindowHandle { + id: WindowId, + state_type: TypeId, +} + +#[derive(Clone)] +pub struct Layout { + pub order: u32, + pub bounds: Bounds, +}