Detailed changes
@@ -11,7 +11,7 @@ env:
jobs:
publish:
- name: Publish collab server image
+ name: Publish collab server image
runs-on:
- self-hosted
- deploy
@@ -22,6 +22,9 @@ jobs:
- name: Sign into DigitalOcean docker registry
run: doctl registry login
+ - name: Prune Docker system
+ run: docker system prune
+
- name: Checkout repo
uses: actions/checkout@v3
with:
@@ -41,6 +44,6 @@ jobs:
- name: Build docker image
run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
-
+
- name: Publish docker image
run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
@@ -1063,6 +1063,7 @@ dependencies = [
"anyhow",
"async-broadcast",
"audio",
+ "channel",
"client",
"collections",
"fs",
@@ -1190,6 +1191,41 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "channel"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "collections",
+ "db",
+ "futures 0.3.28",
+ "gpui",
+ "image",
+ "language",
+ "lazy_static",
+ "log",
+ "parking_lot 0.11.2",
+ "postage",
+ "rand 0.8.5",
+ "rpc",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "settings",
+ "smol",
+ "staff_mode",
+ "sum_tree",
+ "tempfile",
+ "text",
+ "thiserror",
+ "time 0.3.24",
+ "tiny_http",
+ "url",
+ "util",
+ "uuid 1.4.1",
+]
+
[[package]]
name = "chrono"
version = "0.4.26"
@@ -1354,6 +1390,7 @@ dependencies = [
"staff_mode",
"sum_tree",
"tempfile",
+ "text",
"thiserror",
"time 0.3.24",
"tiny_http",
@@ -1409,7 +1446,7 @@ dependencies = [
[[package]]
name = "collab"
-version = "0.17.0"
+version = "0.18.0"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1418,8 +1455,11 @@ dependencies = [
"axum-extra",
"base64 0.13.1",
"call",
+ "channel",
"clap 3.2.25",
"client",
+ "clock",
+ "collab_ui",
"collections",
"ctor",
"dashmap",
@@ -1444,6 +1484,7 @@ dependencies = [
"pretty_assertions",
"project",
"prometheus",
+ "prost 0.8.0",
"rand 0.8.5",
"reqwest",
"rpc",
@@ -1456,6 +1497,7 @@ dependencies = [
"settings",
"sha-1 0.9.8",
"sqlx",
+ "text",
"theme",
"time 0.3.24",
"tokio",
@@ -1478,6 +1520,7 @@ dependencies = [
"anyhow",
"auto_update",
"call",
+ "channel",
"client",
"clock",
"collections",
@@ -1488,6 +1531,7 @@ dependencies = [
"futures 0.3.28",
"fuzzy",
"gpui",
+ "language",
"log",
"menu",
"picker",
@@ -1556,6 +1600,19 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "component_test"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "project",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
[[package]]
name = "concurrent-queue"
version = "2.2.0"
@@ -2085,6 +2142,15 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "derive_refineable"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "dhat"
version = "0.3.2"
@@ -3067,6 +3133,7 @@ dependencies = [
"png",
"postage",
"rand 0.8.5",
+ "refineable",
"resvg",
"schemars",
"seahash",
@@ -3078,6 +3145,7 @@ dependencies = [
"smol",
"sqlez",
"sum_tree",
+ "taffy",
"time 0.3.24",
"tiny-skia",
"usvg",
@@ -3090,11 +3158,18 @@ dependencies = [
name = "gpui_macros"
version = "0.1.0"
dependencies = [
+ "lazy_static",
"proc-macro2",
"quote",
"syn 1.0.109",
]
+[[package]]
+name = "grid"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c"
+
[[package]]
name = "h2"
version = "0.3.20"
@@ -5087,6 +5162,33 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+[[package]]
+name = "playground"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "derive_more",
+ "gpui",
+ "log",
+ "parking_lot 0.11.2",
+ "playground_macros",
+ "refineable",
+ "serde",
+ "simplelog",
+ "smallvec",
+ "taffy",
+ "util",
+]
+
+[[package]]
+name = "playground_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "plist"
version = "1.5.0"
@@ -5764,6 +5866,16 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "refineable"
+version = "0.1.0"
+dependencies = [
+ "derive_refineable",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "regalloc2"
version = "0.2.3"
@@ -6075,9 +6187,9 @@ dependencies = [
[[package]]
name = "rust-embed"
-version = "6.8.1"
+version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
+checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
@@ -6086,9 +6198,9 @@ dependencies = [
[[package]]
name = "rust-embed-impl"
-version = "6.8.1"
+version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
+checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29"
dependencies = [
"proc-macro2",
"quote",
@@ -6099,9 +6211,9 @@ dependencies = [
[[package]]
name = "rust-embed-utils"
-version = "7.8.1"
+version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
+checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada"
dependencies = [
"globset",
"sha2 0.10.7",
@@ -6929,6 +7041,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7"
+[[package]]
+name = "slotmap"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
+dependencies = [
+ "version_check",
+]
+
[[package]]
name = "sluice"
version = "0.5.5"
@@ -7368,6 +7489,17 @@ dependencies = [
"winx",
]
+[[package]]
+name = "taffy"
+version = "0.3.11"
+source = "git+https://github.com/DioxusLabs/taffy?rev=dab541d6104d58e2e10ce90c4a1dad0b703160cd#dab541d6104d58e2e10ce90c4a1dad0b703160cd"
+dependencies = [
+ "arrayvec 0.7.4",
+ "grid",
+ "num-traits",
+ "slotmap",
+]
+
[[package]]
name = "take-until"
version = "0.2.0"
@@ -9446,6 +9578,7 @@ dependencies = [
"async-recursion 1.0.4",
"bincode",
"call",
+ "channel",
"client",
"collections",
"context_menu",
@@ -9557,7 +9690,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.101.0"
+version = "0.102.0"
dependencies = [
"activity_indicator",
"ai",
@@ -9571,6 +9704,7 @@ dependencies = [
"backtrace",
"breadcrumbs",
"call",
+ "channel",
"chrono",
"cli",
"client",
@@ -9578,6 +9712,7 @@ dependencies = [
"collab_ui",
"collections",
"command_palette",
+ "component_test",
"context_menu",
"copilot",
"copilot_button",
@@ -6,6 +6,7 @@ members = [
"crates/auto_update",
"crates/breadcrumbs",
"crates/call",
+ "crates/channel",
"crates/cli",
"crates/client",
"crates/clock",
@@ -13,10 +14,13 @@ members = [
"crates/collab_ui",
"crates/collections",
"crates/command_palette",
+ "crates/component_test",
"crates/context_menu",
"crates/copilot",
"crates/copilot_button",
"crates/db",
+ "crates/refineable",
+ "crates/refineable/derive_refineable",
"crates/diagnostics",
"crates/drag_and_drop",
"crates/editor",
@@ -28,6 +32,8 @@ members = [
"crates/git",
"crates/go_to_line",
"crates/gpui",
+ "crates/gpui/playground",
+ "crates/gpui/playground_macros",
"crates/gpui_macros",
"crates/install_cli",
"crates/journal",
@@ -91,9 +97,11 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = { version = "2.1.1" }
parking_lot = { version = "0.11.1" }
postage = { version = "0.5", features = ["futures-traits"] }
+prost = { version = "0.8" }
rand = { version = "0.8.5" }
+refineable = { path = "./crates/refineable" }
regex = { version = "1.5" }
-rust-embed = { version = "6.3", features = ["include-exclude"] }
+rust-embed = { version = "8.0", features = ["include-exclude"] }
schemars = { version = "0.8" }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -543,6 +543,8 @@
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
+ "cmd-n": "project_panel::NewFile",
+ "alt-cmd-n": "project_panel::NewDirectory",
"cmd-x": "project_panel::Cut",
"cmd-c": "project_panel::Copy",
"cmd-v": "project_panel::Paste",
@@ -2,7 +2,6 @@
{
"bindings": {
"cmd-shift-o": "projects::OpenRecent",
- "cmd-shift-b": "branches::OpenRecent",
"cmd-alt-tab": "project_panel::ToggleFocus"
}
},
@@ -12,8 +11,9 @@
"cmd-l": "go_to_line::Toggle",
"ctrl-shift-d": "editor::DuplicateLine",
"cmd-b": "editor::GoToDefinition",
- "alt-cmd-b": "editor::GoToDefinition",
"cmd-j": "editor::ScrollCursorCenter",
+ "cmd-enter": "editor::NewlineBelow",
+ "cmd-alt-enter": "editor::NewLineAbove",
"cmd-shift-l": "editor::SelectLine",
"cmd-shift-t": "outline::Toggle",
"alt-backspace": "editor::DeleteToPreviousWordStart",
@@ -51,14 +51,17 @@
}
],
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
- "ctrl-shift-right": "editor::SelectToNextSubwordEnd"
+ "ctrl-shift-right": "editor::SelectToNextSubwordEnd",
+ "ctrl-w": "editor::SelectNext",
+ "ctrl-u": "editor::ConvertToUpperCase",
+ "ctrl-shift-u": "editor::ConvertToLowerCase",
+ "ctrl-alt-u": "editor::ConvertToUpperCamelCase",
+ "ctrl-_": "editor::ConvertToSnakeCase"
}
},
{
"context": "Editor && mode == full",
- "bindings": {
- "cmd-alt-enter": "editor::NewlineAbove"
- }
+ "bindings": {}
},
{
"context": "BufferSearchBar",
@@ -85,5 +88,9 @@
{
"context": "ProjectPanel",
"bindings": {}
+ },
+ {
+ "context": "Dock",
+ "bindings": {}
}
]
@@ -287,6 +287,12 @@
"shift-o": "vim::InsertLineAbove",
"~": "vim::ChangeCase",
"p": "vim::Paste",
+ "shift-p": [
+ "vim::Paste",
+ {
+ "before": true
+ }
+ ],
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"/": "vim::Search",
@@ -375,7 +381,13 @@
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
- "p": "vim::VisualPaste",
+ "p": "vim::Paste",
+ "shift-p": [
+ "vim::Paste",
+ {
+ "preserveClipboard": true
+ }
+ ],
"s": "vim::Substitute",
"c": "vim::Substitute",
"~": "vim::ChangeCase",
@@ -421,7 +433,7 @@
}
},
{
- "context": "Editor && vim_mode == insert",
+ "context": "Editor && vim_mode == insert && !menu",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -20,6 +20,7 @@ test-support = [
[dependencies]
audio = { path = "../audio" }
+channel = { path = "../channel" }
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
@@ -7,9 +7,8 @@ use std::sync::Arc;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
-use client::{
- proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
-};
+use channel::ChannelId;
+use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
use collections::HashSet;
use futures::{future::Shared, FutureExt};
use postage::watch;
@@ -0,0 +1,51 @@
+[package]
+name = "channel"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/channel.rs"
+doctest = false
+
+[features]
+test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
+
+[dependencies]
+client = { path = "../client" }
+collections = { path = "../collections" }
+db = { path = "../db" }
+gpui = { path = "../gpui" }
+util = { path = "../util" }
+rpc = { path = "../rpc" }
+text = { path = "../text" }
+language = { path = "../language" }
+settings = { path = "../settings" }
+staff_mode = { path = "../staff_mode" }
+sum_tree = { path = "../sum_tree" }
+
+anyhow.workspace = true
+futures.workspace = true
+image = "0.23"
+lazy_static.workspace = true
+log.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+rand.workspace = true
+schemars.workspace = true
+smol.workspace = true
+thiserror.workspace = true
+time.workspace = true
+tiny_http = "0.8"
+uuid = { version = "1.1.2", features = ["v4"] }
+url = "2.2"
+serde.workspace = true
+serde_derive.workspace = true
+tempfile = "3"
+
+[dev-dependencies]
+collections = { path = "../collections", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
@@ -0,0 +1,14 @@
+mod channel_store;
+
+pub mod channel_buffer;
+use std::sync::Arc;
+
+pub use channel_store::*;
+use client::Client;
+
+#[cfg(test)]
+mod channel_store_tests;
+
+pub fn init(client: &Arc<Client>) {
+ channel_buffer::init(client);
+}
@@ -0,0 +1,197 @@
+use crate::Channel;
+use anyhow::Result;
+use client::Client;
+use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
+use rpc::{proto, TypedEnvelope};
+use std::sync::Arc;
+use util::ResultExt;
+
+pub(crate) fn init(client: &Arc<Client>) {
+ client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
+ client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
+ client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
+}
+
+pub struct ChannelBuffer {
+ pub(crate) channel: Arc<Channel>,
+ connected: bool,
+ collaborators: Vec<proto::Collaborator>,
+ buffer: ModelHandle<language::Buffer>,
+ client: Arc<Client>,
+ subscription: Option<client::Subscription>,
+}
+
+pub enum Event {
+ CollaboratorsChanged,
+ Disconnected,
+}
+
+impl Entity for ChannelBuffer {
+ type Event = Event;
+
+ fn release(&mut self, _: &mut AppContext) {
+ if self.connected {
+ self.client
+ .send(proto::LeaveChannelBuffer {
+ channel_id: self.channel.id,
+ })
+ .log_err();
+ }
+ }
+}
+
+impl ChannelBuffer {
+ pub(crate) async fn new(
+ channel: Arc<Channel>,
+ client: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<ModelHandle<Self>> {
+ let response = client
+ .request(proto::JoinChannelBuffer {
+ channel_id: channel.id,
+ })
+ .await?;
+
+ let base_text = response.base_text;
+ let operations = response
+ .operations
+ .into_iter()
+ .map(language::proto::deserialize_operation)
+ .collect::<Result<Vec<_>, _>>()?;
+
+ let collaborators = response.collaborators;
+
+ let buffer = cx.add_model(|_| {
+ language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
+ });
+ buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
+
+ let subscription = client.subscribe_to_entity(channel.id)?;
+
+ anyhow::Ok(cx.add_model(|cx| {
+ cx.subscribe(&buffer, Self::on_buffer_update).detach();
+
+ Self {
+ buffer,
+ client,
+ connected: true,
+ collaborators,
+ channel,
+ subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
+ }
+ }))
+ }
+
+ async fn handle_update_channel_buffer(
+ this: ModelHandle<Self>,
+ update_channel_buffer: TypedEnvelope<proto::UpdateChannelBuffer>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<()> {
+ let ops = update_channel_buffer
+ .payload
+ .operations
+ .into_iter()
+ .map(language::proto::deserialize_operation)
+ .collect::<Result<Vec<_>, _>>()?;
+
+ this.update(&mut cx, |this, cx| {
+ cx.notify();
+ this.buffer
+ .update(cx, |buffer, cx| buffer.apply_ops(ops, cx))
+ })?;
+
+ Ok(())
+ }
+
+ async fn handle_add_channel_buffer_collaborator(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<()> {
+ let collaborator = envelope.payload.collaborator.ok_or_else(|| {
+ anyhow::anyhow!(
+ "Should have gotten a collaborator in the AddChannelBufferCollaborator message"
+ )
+ })?;
+
+ this.update(&mut cx, |this, cx| {
+ this.collaborators.push(collaborator);
+ cx.emit(Event::CollaboratorsChanged);
+ cx.notify();
+ });
+
+ Ok(())
+ }
+
+ async fn handle_remove_channel_buffer_collaborator(
+ this: ModelHandle<Self>,
+ message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<()> {
+ this.update(&mut cx, |this, cx| {
+ this.collaborators.retain(|collaborator| {
+ if collaborator.peer_id == message.payload.peer_id {
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.remove_peer(collaborator.replica_id as u16, cx)
+ });
+ false
+ } else {
+ true
+ }
+ });
+ cx.emit(Event::CollaboratorsChanged);
+ cx.notify();
+ });
+
+ Ok(())
+ }
+
+ fn on_buffer_update(
+ &mut self,
+ _: ModelHandle<language::Buffer>,
+ event: &language::Event,
+ _: &mut ModelContext<Self>,
+ ) {
+ if let language::Event::Operation(operation) = event {
+ let operation = language::proto::serialize_operation(operation);
+ self.client
+ .send(proto::UpdateChannelBuffer {
+ channel_id: self.channel.id,
+ operations: vec![operation],
+ })
+ .log_err();
+ }
+ }
+
+ pub fn buffer(&self) -> ModelHandle<language::Buffer> {
+ self.buffer.clone()
+ }
+
+ pub fn collaborators(&self) -> &[proto::Collaborator] {
+ &self.collaborators
+ }
+
+ pub fn channel(&self) -> Arc<Channel> {
+ self.channel.clone()
+ }
+
+ pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
+ if self.connected {
+ self.connected = false;
+ self.subscription.take();
+ cx.emit(Event::Disconnected);
+ cx.notify()
+ }
+ }
+
+ pub fn is_connected(&self) -> bool {
+ self.connected
+ }
+
+ pub fn replica_id(&self, cx: &AppContext) -> u16 {
+ self.buffer.read(cx).replica_id()
+ }
+}
@@ -1,19 +1,14 @@
-use crate::Status;
-use crate::{Client, Subscription, User, UserStore};
-use anyhow::anyhow;
-use anyhow::Result;
-use collections::HashMap;
-use collections::HashSet;
-use futures::channel::mpsc;
-use futures::Future;
-use futures::StreamExt;
-use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
+use crate::channel_buffer::ChannelBuffer;
+use anyhow::{anyhow, Result};
+use client::{Client, Status, Subscription, User, UserId, UserStore};
+use collections::{hash_map, HashMap, HashSet};
+use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
+use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use rpc::{proto, TypedEnvelope};
use std::sync::Arc;
use util::ResultExt;
pub type ChannelId = u64;
-pub type UserId = u64;
pub struct ChannelStore {
channels_by_id: HashMap<ChannelId, Arc<Channel>>,
@@ -23,6 +18,7 @@ pub struct ChannelStore {
channels_with_admin_privileges: HashSet<ChannelId>,
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
+ opened_buffers: HashMap<ChannelId, OpenedChannelBuffer>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
_rpc_subscription: Subscription,
@@ -57,6 +53,11 @@ pub enum ChannelMemberStatus {
NotMember,
}
+enum OpenedChannelBuffer {
+ Open(WeakModelHandle<ChannelBuffer>),
+ Loading(Shared<Task<Result<ModelHandle<ChannelBuffer>, Arc<anyhow::Error>>>>),
+}
+
impl ChannelStore {
pub fn new(
client: Arc<Client>,
@@ -70,16 +71,14 @@ impl ChannelStore {
let mut connection_status = client.status();
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
while let Some(status) = connection_status.next().await {
- if matches!(status, Status::ConnectionLost | Status::SignedOut) {
+ if !status.is_connected() {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
- this.channels_by_id.clear();
- this.channel_invitations.clear();
- this.channel_participants.clear();
- this.channels_with_admin_privileges.clear();
- this.channel_paths.clear();
- this.outgoing_invites.clear();
- cx.notify();
+ if matches!(status, Status::ConnectionLost | Status::SignedOut) {
+ this.handle_disconnect(cx);
+ } else {
+ this.disconnect_buffers(cx);
+ }
});
} else {
break;
@@ -87,6 +86,7 @@ impl ChannelStore {
}
}
});
+
Self {
channels_by_id: HashMap::default(),
channel_invitations: Vec::default(),
@@ -94,6 +94,7 @@ impl ChannelStore {
channel_participants: Default::default(),
channels_with_admin_privileges: Default::default(),
outgoing_invites: Default::default(),
+ opened_buffers: Default::default(),
update_channels_tx,
client,
user_store,
@@ -114,6 +115,16 @@ impl ChannelStore {
}
}
+ pub fn has_children(&self, channel_id: ChannelId) -> bool {
+ self.channel_paths.iter().any(|path| {
+ if let Some(ix) = path.iter().position(|id| *id == channel_id) {
+ path.len() > ix + 1
+ } else {
+ false
+ }
+ })
+ }
+
pub fn channel_count(&self) -> usize {
self.channel_paths.len()
}
@@ -141,6 +152,74 @@ impl ChannelStore {
self.channels_by_id.get(&channel_id)
}
+ pub fn open_channel_buffer(
+ &mut self,
+ channel_id: ChannelId,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
+ // Make sure that a given channel buffer is only opened once per
+ // app instance, even if this method is called multiple times
+ // with the same channel id while the first task is still running.
+ let task = loop {
+ match self.opened_buffers.entry(channel_id) {
+ hash_map::Entry::Occupied(e) => match e.get() {
+ OpenedChannelBuffer::Open(buffer) => {
+ if let Some(buffer) = buffer.upgrade(cx) {
+ break Task::ready(Ok(buffer)).shared();
+ } else {
+ self.opened_buffers.remove(&channel_id);
+ continue;
+ }
+ }
+ OpenedChannelBuffer::Loading(task) => break task.clone(),
+ },
+ hash_map::Entry::Vacant(e) => {
+ let client = self.client.clone();
+ let task = cx
+ .spawn(|this, cx| async move {
+ let channel = this.read_with(&cx, |this, _| {
+ this.channel_for_id(channel_id).cloned().ok_or_else(|| {
+ Arc::new(anyhow!("no channel for id: {}", channel_id))
+ })
+ })?;
+
+ ChannelBuffer::new(channel, client, cx)
+ .await
+ .map_err(Arc::new)
+ })
+ .shared();
+ e.insert(OpenedChannelBuffer::Loading(task.clone()));
+ cx.spawn({
+ let task = task.clone();
+ |this, mut cx| async move {
+ let result = task.await;
+ this.update(&mut cx, |this, cx| match result {
+ Ok(buffer) => {
+ cx.observe_release(&buffer, move |this, _, _| {
+ this.opened_buffers.remove(&channel_id);
+ })
+ .detach();
+ this.opened_buffers.insert(
+ channel_id,
+ OpenedChannelBuffer::Open(buffer.downgrade()),
+ );
+ }
+ Err(error) => {
+ log::error!("failed to open channel buffer {error:?}");
+ this.opened_buffers.remove(&channel_id);
+ }
+ });
+ }
+ })
+ .detach();
+ break task;
+ }
+ }
+ };
+ cx.foreground()
+ .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) })
+ }
+
pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
self.channel_paths.iter().any(|path| {
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
@@ -403,6 +482,27 @@ impl ChannelStore {
Ok(())
}
+ fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) {
+ self.disconnect_buffers(cx);
+ self.channels_by_id.clear();
+ self.channel_invitations.clear();
+ self.channel_participants.clear();
+ self.channels_with_admin_privileges.clear();
+ self.channel_paths.clear();
+ self.outgoing_invites.clear();
+ cx.notify();
+ }
+
+ fn disconnect_buffers(&mut self, cx: &mut ModelContext<ChannelStore>) {
+ for (_, buffer) in self.opened_buffers.drain() {
+ if let OpenedChannelBuffer::Open(buffer) = buffer {
+ if let Some(buffer) = buffer.upgrade(cx) {
+ buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
+ }
+ }
+ }
+ }
+
pub(crate) fn update_channels(
&mut self,
payload: proto::UpdateChannels,
@@ -437,38 +537,44 @@ impl ChannelStore {
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
self.channels_with_admin_privileges
.retain(|channel_id| !payload.remove_channels.contains(channel_id));
- }
- for channel in payload.channels {
- if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) {
- // FIXME: We may be missing a path for this existing channel in certain cases
- let existing_channel = Arc::make_mut(existing_channel);
- existing_channel.name = channel.name;
- continue;
+ for channel_id in &payload.remove_channels {
+ let channel_id = *channel_id;
+ if let Some(OpenedChannelBuffer::Open(buffer)) =
+ self.opened_buffers.remove(&channel_id)
+ {
+ if let Some(buffer) = buffer.upgrade(cx) {
+ buffer.update(cx, ChannelBuffer::disconnect);
+ }
+ }
}
+ }
- self.channels_by_id.insert(
- channel.id,
- Arc::new(Channel {
- id: channel.id,
- name: channel.name,
- }),
- );
-
- if let Some(parent_id) = channel.parent_id {
- let mut ix = 0;
- while ix < self.channel_paths.len() {
- let path = &self.channel_paths[ix];
- if path.ends_with(&[parent_id]) {
- let mut new_path = path.clone();
- new_path.push(channel.id);
- self.channel_paths.insert(ix + 1, new_path);
+ for channel_proto in payload.channels {
+ if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
+ Arc::make_mut(existing_channel).name = channel_proto.name;
+ } else {
+ let channel = Arc::new(Channel {
+ id: channel_proto.id,
+ name: channel_proto.name,
+ });
+ self.channels_by_id.insert(channel.id, channel.clone());
+
+ if let Some(parent_id) = channel_proto.parent_id {
+ let mut ix = 0;
+ while ix < self.channel_paths.len() {
+ let path = &self.channel_paths[ix];
+ if path.ends_with(&[parent_id]) {
+ let mut new_path = path.clone();
+ new_path.push(channel.id);
+ self.channel_paths.insert(ix + 1, new_path);
+ ix += 1;
+ }
ix += 1;
}
- ix += 1;
+ } else {
+ self.channel_paths.push(vec![channel.id]);
}
- } else {
- self.channel_paths.push(vec![channel.id]);
}
}
@@ -1,4 +1,7 @@
use super::*;
+use client::{Client, UserStore};
+use gpui::{AppContext, ModelHandle};
+use rpc::proto;
use util::http::FakeHttpClient;
#[gpui::test]
@@ -17,6 +17,7 @@ db = { path = "../db" }
gpui = { path = "../gpui" }
util = { path = "../util" }
rpc = { path = "../rpc" }
+text = { path = "../text" }
settings = { path = "../settings" }
staff_mode = { path = "../staff_mode" }
sum_tree = { path = "../sum_tree" }
@@ -1,10 +1,6 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
-#[cfg(test)]
-mod channel_store_tests;
-
-pub mod channel_store;
pub mod telemetry;
pub mod user;
@@ -48,7 +44,6 @@ use util::channel::ReleaseChannel;
use util::http::HttpClient;
use util::{ResultExt, TryFutureExt};
-pub use channel_store::*;
pub use rpc::*;
pub use telemetry::ClickhouseEvent;
pub use user::*;
@@ -10,9 +10,11 @@ use std::sync::{Arc, Weak};
use util::http::HttpClient;
use util::TryFutureExt as _;
+pub type UserId = u64;
+
#[derive(Default, Debug)]
pub struct User {
- pub id: u64,
+ pub id: UserId,
pub github_login: String,
pub avatar: Option<Arc<ImageData>>,
}
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
-version = "0.17.0"
+version = "0.18.0"
publish = false
[[bin]]
@@ -14,8 +14,10 @@ name = "seed"
required-features = ["seed-support"]
[dependencies]
+clock = { path = "../clock" }
collections = { path = "../collections" }
live_kit_server = { path = "../live_kit_server" }
+text = { path = "../text" }
rpc = { path = "../rpc" }
util = { path = "../util" }
@@ -35,6 +37,7 @@ log.workspace = true
nanoid = "0.4"
parking_lot.workspace = true
prometheus = "0.13"
+prost.workspace = true
rand.workspace = true
reqwest = { version = "0.11", features = ["json"], optional = true }
scrypt = "0.7"
@@ -62,6 +65,7 @@ collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
+channel = { path = "../channel" }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
@@ -74,6 +78,7 @@ rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }
+collab_ui = { path = "../collab_ui", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
@@ -208,3 +208,44 @@ CREATE TABLE "channel_members" (
);
CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");
+
+CREATE TABLE "buffers" (
+ "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+ "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+ "epoch" INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id");
+
+CREATE TABLE "buffer_operations" (
+ "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+ "epoch" INTEGER NOT NULL,
+ "replica_id" INTEGER NOT NULL,
+ "lamport_timestamp" INTEGER NOT NULL,
+ "value" BLOB NOT NULL,
+ PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id)
+);
+
+CREATE TABLE "buffer_snapshots" (
+ "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+ "epoch" INTEGER NOT NULL,
+ "text" TEXT NOT NULL,
+ "operation_serialization_version" INTEGER NOT NULL,
+ PRIMARY KEY(buffer_id, epoch)
+);
+
+CREATE TABLE "channel_buffer_collaborators" (
+ "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+ "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+ "connection_id" INTEGER NOT NULL,
+ "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+ "connection_lost" BOOLEAN NOT NULL DEFAULT false,
+ "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+ "replica_id" INTEGER NOT NULL
+);
+
+CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id");
+CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id");
+CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id");
+CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id");
+CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id");
@@ -0,0 +1,40 @@
+CREATE TABLE "buffers" (
+ "id" SERIAL PRIMARY KEY,
+ "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+ "epoch" INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id");
+
+CREATE TABLE "buffer_operations" (
+ "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+ "epoch" INTEGER NOT NULL,
+ "replica_id" INTEGER NOT NULL,
+ "lamport_timestamp" INTEGER NOT NULL,
+ "value" BYTEA NOT NULL,
+ PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id)
+);
+
+CREATE TABLE "buffer_snapshots" (
+ "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+ "epoch" INTEGER NOT NULL,
+ "text" TEXT NOT NULL,
+ "operation_serialization_version" INTEGER NOT NULL,
+ PRIMARY KEY(buffer_id, epoch)
+);
+
+CREATE TABLE "channel_buffer_collaborators" (
+ "id" SERIAL PRIMARY KEY,
+ "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+ "connection_id" INTEGER NOT NULL,
+ "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+ "connection_lost" BOOLEAN NOT NULL DEFAULT FALSE,
+ "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+ "replica_id" INTEGER NOT NULL
+);
+
+CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id");
+CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id");
+CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id");
+CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id");
+CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id");
@@ -1,7 +1,8 @@
#[cfg(test)]
-mod db_tests;
+pub mod tests;
+
#[cfg(test)]
-pub mod test_db;
+pub use tests::TestDb;
mod ids;
mod queries;
@@ -52,6 +53,8 @@ pub struct Database {
runtime: Option<tokio::runtime::Runtime>,
}
+// The `Database` type has so many methods that its impl blocks are split into
+// separate files in the `queries` folder.
impl Database {
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
Ok(Self {
@@ -110,6 +110,7 @@ fn value_to_integer(v: Value) -> Result<i32, ValueTypeErr> {
}
}
+id_type!(BufferId);
id_type!(AccessTokenId);
id_type!(ChannelId);
id_type!(ChannelMemberId);
@@ -123,3 +124,4 @@ id_type!(ReplicaId);
id_type!(ServerId);
id_type!(SignupId);
id_type!(UserId);
+id_type!(ChannelBufferCollaboratorId);
@@ -1,6 +1,7 @@
use super::*;
pub mod access_tokens;
+pub mod buffers;
pub mod channels;
pub mod contacts;
pub mod projects;
@@ -0,0 +1,588 @@
+use super::*;
+use prost::Message;
+use text::{EditOperation, InsertionTimestamp, UndoOperation};
+
+impl Database {
+ pub async fn join_channel_buffer(
+ &self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ connection: ConnectionId,
+ ) -> Result<proto::JoinChannelBufferResponse> {
+ self.transaction(|tx| async move {
+ let tx = tx;
+
+ self.check_user_is_channel_member(channel_id, user_id, &tx)
+ .await?;
+
+ let buffer = channel::Model {
+ id: channel_id,
+ ..Default::default()
+ }
+ .find_related(buffer::Entity)
+ .one(&*tx)
+ .await?;
+
+ let buffer = if let Some(buffer) = buffer {
+ buffer
+ } else {
+ let buffer = buffer::ActiveModel {
+ channel_id: ActiveValue::Set(channel_id),
+ ..Default::default()
+ }
+ .insert(&*tx)
+ .await?;
+ buffer_snapshot::ActiveModel {
+ buffer_id: ActiveValue::Set(buffer.id),
+ epoch: ActiveValue::Set(0),
+ text: ActiveValue::Set(String::new()),
+ operation_serialization_version: ActiveValue::Set(
+ storage::SERIALIZATION_VERSION,
+ ),
+ }
+ .insert(&*tx)
+ .await?;
+ buffer
+ };
+
+ // Join the collaborators
+ let mut collaborators = channel_buffer_collaborator::Entity::find()
+ .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
+ .all(&*tx)
+ .await?;
+ let replica_ids = collaborators
+ .iter()
+ .map(|c| c.replica_id)
+ .collect::<HashSet<_>>();
+ let mut replica_id = ReplicaId(0);
+ while replica_ids.contains(&replica_id) {
+ replica_id.0 += 1;
+ }
+ let collaborator = channel_buffer_collaborator::ActiveModel {
+ channel_id: ActiveValue::Set(channel_id),
+ connection_id: ActiveValue::Set(connection.id as i32),
+ connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)),
+ user_id: ActiveValue::Set(user_id),
+ replica_id: ActiveValue::Set(replica_id),
+ ..Default::default()
+ }
+ .insert(&*tx)
+ .await?;
+ collaborators.push(collaborator);
+
+ // Assemble the buffer state
+ let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?;
+
+ Ok(proto::JoinChannelBufferResponse {
+ buffer_id: buffer.id.to_proto(),
+ replica_id: replica_id.to_proto() as u32,
+ base_text,
+ operations,
+ collaborators: collaborators
+ .into_iter()
+ .map(|collaborator| proto::Collaborator {
+ peer_id: Some(collaborator.connection().into()),
+ user_id: collaborator.user_id.to_proto(),
+ replica_id: collaborator.replica_id.0 as u32,
+ })
+ .collect(),
+ })
+ })
+ .await
+ }
+
+ pub async fn leave_channel_buffer(
+ &self,
+ channel_id: ChannelId,
+ connection: ConnectionId,
+ ) -> Result<Vec<ConnectionId>> {
+ self.transaction(|tx| async move {
+ self.leave_channel_buffer_internal(channel_id, connection, &*tx)
+ .await
+ })
+ .await
+ }
+
+ pub async fn leave_channel_buffer_internal(
+ &self,
+ channel_id: ChannelId,
+ connection: ConnectionId,
+ tx: &DatabaseTransaction,
+ ) -> Result<Vec<ConnectionId>> {
+ let result = channel_buffer_collaborator::Entity::delete_many()
+ .filter(
+ Condition::all()
+ .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
+ .add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32))
+ .add(
+ channel_buffer_collaborator::Column::ConnectionServerId
+ .eq(connection.owner_id as i32),
+ ),
+ )
+ .exec(&*tx)
+ .await?;
+ if result.rows_affected == 0 {
+ Err(anyhow!("not a collaborator on this project"))?;
+ }
+
+ let mut connections = Vec::new();
+ let mut rows = channel_buffer_collaborator::Entity::find()
+ .filter(
+ Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
+ )
+ .stream(&*tx)
+ .await?;
+ while let Some(row) = rows.next().await {
+ let row = row?;
+ connections.push(ConnectionId {
+ id: row.connection_id as u32,
+ owner_id: row.connection_server_id.0 as u32,
+ });
+ }
+
+ drop(rows);
+
+ if connections.is_empty() {
+ self.snapshot_buffer(channel_id, &tx).await?;
+ }
+
+ Ok(connections)
+ }
+
+ pub async fn leave_channel_buffers(
+ &self,
+ connection: ConnectionId,
+ ) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
+ self.transaction(|tx| async move {
+ #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+ enum QueryChannelIds {
+ ChannelId,
+ }
+
+ let channel_ids: Vec<ChannelId> = channel_buffer_collaborator::Entity::find()
+ .select_only()
+ .column(channel_buffer_collaborator::Column::ChannelId)
+ .filter(Condition::all().add(
+ channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32),
+ ))
+ .into_values::<_, QueryChannelIds>()
+ .all(&*tx)
+ .await?;
+
+ let mut result = Vec::new();
+ for channel_id in channel_ids {
+ let collaborators = self
+ .leave_channel_buffer_internal(channel_id, connection, &*tx)
+ .await?;
+ result.push((channel_id, collaborators));
+ }
+
+ Ok(result)
+ })
+ .await
+ }
+
+ #[cfg(debug_assertions)]
+ pub async fn get_channel_buffer_collaborators(
+ &self,
+ channel_id: ChannelId,
+ ) -> Result<Vec<UserId>> {
+ self.transaction(|tx| async move {
+ #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+ enum QueryUserIds {
+ UserId,
+ }
+
+ let users: Vec<UserId> = channel_buffer_collaborator::Entity::find()
+ .select_only()
+ .column(channel_buffer_collaborator::Column::UserId)
+ .filter(
+ Condition::all()
+ .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
+ )
+ .into_values::<_, QueryUserIds>()
+ .all(&*tx)
+ .await?;
+
+ Ok(users)
+ })
+ .await
+ }
+
+ pub async fn update_channel_buffer(
+ &self,
+ channel_id: ChannelId,
+ user: UserId,
+ operations: &[proto::Operation],
+ ) -> Result<Vec<ConnectionId>> {
+ self.transaction(move |tx| async move {
+ self.check_user_is_channel_member(channel_id, user, &*tx)
+ .await?;
+
+ let buffer = buffer::Entity::find()
+ .filter(buffer::Column::ChannelId.eq(channel_id))
+ .one(&*tx)
+ .await?
+ .ok_or_else(|| anyhow!("no such buffer"))?;
+
+ #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+ enum QueryVersion {
+ OperationSerializationVersion,
+ }
+
+ let serialization_version: i32 = buffer
+ .find_related(buffer_snapshot::Entity)
+ .select_only()
+ .column(buffer_snapshot::Column::OperationSerializationVersion)
+ .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch))
+ .into_values::<_, QueryVersion>()
+ .one(&*tx)
+ .await?
+ .ok_or_else(|| anyhow!("missing buffer snapshot"))?;
+
+ let operations = operations
+ .iter()
+ .filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
+ .collect::<Vec<_>>();
+ if !operations.is_empty() {
+ buffer_operation::Entity::insert_many(operations)
+ .exec(&*tx)
+ .await?;
+ }
+
+ let mut connections = Vec::new();
+ let mut rows = channel_buffer_collaborator::Entity::find()
+ .filter(
+ Condition::all()
+ .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
+ )
+ .stream(&*tx)
+ .await?;
+ while let Some(row) = rows.next().await {
+ let row = row?;
+ connections.push(ConnectionId {
+ id: row.connection_id as u32,
+ owner_id: row.connection_server_id.0 as u32,
+ });
+ }
+
+ Ok(connections)
+ })
+ .await
+ }
+
+ async fn get_buffer_state(
+ &self,
+ buffer: &buffer::Model,
+ tx: &DatabaseTransaction,
+ ) -> Result<(String, Vec<proto::Operation>)> {
+ let id = buffer.id;
+ let (base_text, version) = if buffer.epoch > 0 {
+ let snapshot = buffer_snapshot::Entity::find()
+ .filter(
+ buffer_snapshot::Column::BufferId
+ .eq(id)
+ .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)),
+ )
+ .one(&*tx)
+ .await?
+ .ok_or_else(|| anyhow!("no such snapshot"))?;
+
+ let version = snapshot.operation_serialization_version;
+ (snapshot.text, version)
+ } else {
+ (String::new(), storage::SERIALIZATION_VERSION)
+ };
+
+ let mut rows = buffer_operation::Entity::find()
+ .filter(
+ buffer_operation::Column::BufferId
+ .eq(id)
+ .and(buffer_operation::Column::Epoch.eq(buffer.epoch)),
+ )
+ .stream(&*tx)
+ .await?;
+ let mut operations = Vec::new();
+ while let Some(row) = rows.next().await {
+ let row = row?;
+
+ let operation = operation_from_storage(row, version)?;
+ operations.push(proto::Operation {
+ variant: Some(operation),
+ })
+ }
+
+ Ok((base_text, operations))
+ }
+
+ async fn snapshot_buffer(&self, channel_id: ChannelId, tx: &DatabaseTransaction) -> Result<()> {
+ let buffer = channel::Model {
+ id: channel_id,
+ ..Default::default()
+ }
+ .find_related(buffer::Entity)
+ .one(&*tx)
+ .await?
+ .ok_or_else(|| anyhow!("no such buffer"))?;
+
+ let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?;
+ if operations.is_empty() {
+ return Ok(());
+ }
+
+ let mut text_buffer = text::Buffer::new(0, 0, base_text);
+ text_buffer
+ .apply_ops(operations.into_iter().filter_map(operation_from_wire))
+ .unwrap();
+
+ let base_text = text_buffer.text();
+ let epoch = buffer.epoch + 1;
+
+ buffer_snapshot::Model {
+ buffer_id: buffer.id,
+ epoch,
+ text: base_text,
+ operation_serialization_version: storage::SERIALIZATION_VERSION,
+ }
+ .into_active_model()
+ .insert(tx)
+ .await?;
+
+ buffer::ActiveModel {
+ id: ActiveValue::Unchanged(buffer.id),
+ epoch: ActiveValue::Set(epoch),
+ ..Default::default()
+ }
+ .save(tx)
+ .await?;
+
+ Ok(())
+ }
+}
+
+fn operation_to_storage(
+ operation: &proto::Operation,
+ buffer: &buffer::Model,
+ _format: i32,
+) -> Option<buffer_operation::ActiveModel> {
+ let (replica_id, lamport_timestamp, value) = match operation.variant.as_ref()? {
+ proto::operation::Variant::Edit(operation) => (
+ operation.replica_id,
+ operation.lamport_timestamp,
+ storage::Operation {
+ local_timestamp: operation.local_timestamp,
+ version: version_to_storage(&operation.version),
+ is_undo: false,
+ edit_ranges: operation
+ .ranges
+ .iter()
+ .map(|range| storage::Range {
+ start: range.start,
+ end: range.end,
+ })
+ .collect(),
+ edit_texts: operation.new_text.clone(),
+ undo_counts: Vec::new(),
+ },
+ ),
+ proto::operation::Variant::Undo(operation) => (
+ operation.replica_id,
+ operation.lamport_timestamp,
+ storage::Operation {
+ local_timestamp: operation.local_timestamp,
+ version: version_to_storage(&operation.version),
+ is_undo: true,
+ edit_ranges: Vec::new(),
+ edit_texts: Vec::new(),
+ undo_counts: operation
+ .counts
+ .iter()
+ .map(|entry| storage::UndoCount {
+ replica_id: entry.replica_id,
+ local_timestamp: entry.local_timestamp,
+ count: entry.count,
+ })
+ .collect(),
+ },
+ ),
+ _ => None?,
+ };
+
+ Some(buffer_operation::ActiveModel {
+ buffer_id: ActiveValue::Set(buffer.id),
+ epoch: ActiveValue::Set(buffer.epoch),
+ replica_id: ActiveValue::Set(replica_id as i32),
+ lamport_timestamp: ActiveValue::Set(lamport_timestamp as i32),
+ value: ActiveValue::Set(value.encode_to_vec()),
+ })
+}
+
+fn operation_from_storage(
+ row: buffer_operation::Model,
+ _format_version: i32,
+) -> Result<proto::operation::Variant, Error> {
+ let operation =
+ storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{}", error))?;
+ let version = version_from_storage(&operation.version);
+ Ok(if operation.is_undo {
+ proto::operation::Variant::Undo(proto::operation::Undo {
+ replica_id: row.replica_id as u32,
+ local_timestamp: operation.local_timestamp as u32,
+ lamport_timestamp: row.lamport_timestamp as u32,
+ version,
+ counts: operation
+ .undo_counts
+ .iter()
+ .map(|entry| proto::UndoCount {
+ replica_id: entry.replica_id,
+ local_timestamp: entry.local_timestamp,
+ count: entry.count,
+ })
+ .collect(),
+ })
+ } else {
+ proto::operation::Variant::Edit(proto::operation::Edit {
+ replica_id: row.replica_id as u32,
+ local_timestamp: operation.local_timestamp as u32,
+ lamport_timestamp: row.lamport_timestamp as u32,
+ version,
+ ranges: operation
+ .edit_ranges
+ .into_iter()
+ .map(|range| proto::Range {
+ start: range.start,
+ end: range.end,
+ })
+ .collect(),
+ new_text: operation.edit_texts,
+ })
+ })
+}
+
+fn version_to_storage(version: &Vec<proto::VectorClockEntry>) -> Vec<storage::VectorClockEntry> {
+ version
+ .iter()
+ .map(|entry| storage::VectorClockEntry {
+ replica_id: entry.replica_id,
+ timestamp: entry.timestamp,
+ })
+ .collect()
+}
+
+fn version_from_storage(version: &Vec<storage::VectorClockEntry>) -> Vec<proto::VectorClockEntry> {
+ version
+ .iter()
+ .map(|entry| proto::VectorClockEntry {
+ replica_id: entry.replica_id,
+ timestamp: entry.timestamp,
+ })
+ .collect()
+}
+
+// This is currently a manual copy of the deserialization code in the client's langauge crate
+pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operation> {
+ match operation.variant? {
+ proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation {
+ timestamp: InsertionTimestamp {
+ replica_id: edit.replica_id as text::ReplicaId,
+ local: edit.local_timestamp,
+ lamport: edit.lamport_timestamp,
+ },
+ version: version_from_wire(&edit.version),
+ ranges: edit
+ .ranges
+ .into_iter()
+ .map(|range| {
+ text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize)
+ })
+ .collect(),
+ new_text: edit.new_text.into_iter().map(Arc::from).collect(),
+ })),
+ proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo {
+ lamport_timestamp: clock::Lamport {
+ replica_id: undo.replica_id as text::ReplicaId,
+ value: undo.lamport_timestamp,
+ },
+ undo: UndoOperation {
+ id: clock::Local {
+ replica_id: undo.replica_id as text::ReplicaId,
+ value: undo.local_timestamp,
+ },
+ version: version_from_wire(&undo.version),
+ counts: undo
+ .counts
+ .into_iter()
+ .map(|c| {
+ (
+ clock::Local {
+ replica_id: c.replica_id as text::ReplicaId,
+ value: c.local_timestamp,
+ },
+ c.count,
+ )
+ })
+ .collect(),
+ },
+ }),
+ _ => None,
+ }
+}
+
+fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global {
+ let mut version = clock::Global::new();
+ for entry in message {
+ version.observe(clock::Local {
+ replica_id: entry.replica_id as text::ReplicaId,
+ value: entry.timestamp,
+ });
+ }
+ version
+}
+
+mod storage {
+ #![allow(non_snake_case)]
+ use prost::Message;
+ pub const SERIALIZATION_VERSION: i32 = 1;
+
+ #[derive(Message)]
+ pub struct Operation {
+ #[prost(uint32, tag = "1")]
+ pub local_timestamp: u32,
+ #[prost(message, repeated, tag = "2")]
+ pub version: Vec<VectorClockEntry>,
+ #[prost(bool, tag = "3")]
+ pub is_undo: bool,
+ #[prost(message, repeated, tag = "4")]
+ pub edit_ranges: Vec<Range>,
+ #[prost(string, repeated, tag = "5")]
+ pub edit_texts: Vec<String>,
+ #[prost(message, repeated, tag = "6")]
+ pub undo_counts: Vec<UndoCount>,
+ }
+
+ #[derive(Message)]
+ pub struct VectorClockEntry {
+ #[prost(uint32, tag = "1")]
+ pub replica_id: u32,
+ #[prost(uint32, tag = "2")]
+ pub timestamp: u32,
+ }
+
+ #[derive(Message)]
+ pub struct Range {
+ #[prost(uint64, tag = "1")]
+ pub start: u64,
+ #[prost(uint64, tag = "2")]
+ pub end: u64,
+ }
+
+ #[derive(Message)]
+ pub struct UndoCount {
+ #[prost(uint32, tag = "1")]
+ pub replica_id: u32,
+ #[prost(uint32, tag = "2")]
+ pub local_timestamp: u32,
+ #[prost(uint32, tag = "3")]
+ pub count: u32,
+ }
+}
@@ -903,15 +903,35 @@ impl Database {
),
)
.one(&*tx)
- .await?
- .ok_or_else(|| anyhow!("not a participant in any room"))?;
+ .await?;
- room_participant::Entity::update(room_participant::ActiveModel {
- answering_connection_lost: ActiveValue::set(true),
- ..participant.into_active_model()
- })
- .exec(&*tx)
- .await?;
+ if let Some(participant) = participant {
+ room_participant::Entity::update(room_participant::ActiveModel {
+ answering_connection_lost: ActiveValue::set(true),
+ ..participant.into_active_model()
+ })
+ .exec(&*tx)
+ .await?;
+ }
+
+ channel_buffer_collaborator::Entity::update_many()
+ .filter(
+ Condition::all()
+ .add(
+ channel_buffer_collaborator::Column::ConnectionId
+ .eq(connection.id as i32),
+ )
+ .add(
+ channel_buffer_collaborator::Column::ConnectionServerId
+ .eq(connection.owner_id as i32),
+ ),
+ )
+ .set(channel_buffer_collaborator::ActiveModel {
+ connection_lost: ActiveValue::set(true),
+ ..Default::default()
+ })
+ .exec(&*tx)
+ .await?;
Ok(())
})
@@ -1,5 +1,9 @@
pub mod access_token;
+pub mod buffer;
+pub mod buffer_operation;
+pub mod buffer_snapshot;
pub mod channel;
+pub mod channel_buffer_collaborator;
pub mod channel_member;
pub mod channel_path;
pub mod contact;
@@ -0,0 +1,45 @@
+use crate::db::{BufferId, ChannelId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "buffers")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: BufferId,
+ pub epoch: i32,
+ pub channel_id: ChannelId,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(has_many = "super::buffer_operation::Entity")]
+ Operations,
+ #[sea_orm(has_many = "super::buffer_snapshot::Entity")]
+ Snapshots,
+ #[sea_orm(
+ belongs_to = "super::channel::Entity",
+ from = "Column::ChannelId",
+ to = "super::channel::Column::Id"
+ )]
+ Channel,
+}
+
+impl Related<super::buffer_operation::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::Operations.def()
+ }
+}
+
+impl Related<super::buffer_snapshot::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::Snapshots.def()
+ }
+}
+
+impl Related<super::channel::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::Channel.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
@@ -0,0 +1,34 @@
+use crate::db::BufferId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "buffer_operations")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub buffer_id: BufferId,
+ #[sea_orm(primary_key)]
+ pub epoch: i32,
+ #[sea_orm(primary_key)]
+ pub lamport_timestamp: i32,
+ #[sea_orm(primary_key)]
+ pub replica_id: i32,
+ pub value: Vec<u8>,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::buffer::Entity",
+ from = "Column::BufferId",
+ to = "super::buffer::Column::Id"
+ )]
+ Buffer,
+}
+
+impl Related<super::buffer::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::Buffer.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
@@ -0,0 +1,31 @@
+use crate::db::BufferId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "buffer_snapshots")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub buffer_id: BufferId,
+ #[sea_orm(primary_key)]
+ pub epoch: i32,
+ pub text: String,
+ pub operation_serialization_version: i32,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::buffer::Entity",
+ from = "Column::BufferId",
+ to = "super::buffer::Column::Id"
+ )]
+ Buffer,
+}
+
+impl Related<super::buffer::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::Buffer.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
@@ -15,8 +15,12 @@ impl ActiveModelBehavior for ActiveModel {}
pub enum Relation {
#[sea_orm(has_one = "super::room::Entity")]
Room,
+ #[sea_orm(has_one = "super::buffer::Entity")]
+ Buffer,
#[sea_orm(has_many = "super::channel_member::Entity")]
Member,
+ #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")]
+ BufferCollaborators,
}
impl Related<super::channel_member::Entity> for Entity {
@@ -30,3 +34,15 @@ impl Related<super::room::Entity> for Entity {
Relation::Room.def()
}
}
+
+impl Related<super::buffer::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::Buffer.def()
+ }
+}
+
+impl Related<super::channel_buffer_collaborator::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::BufferCollaborators.def()
+ }
+}
@@ -0,0 +1,43 @@
+use crate::db::{ChannelBufferCollaboratorId, ChannelId, ReplicaId, ServerId, UserId};
+use rpc::ConnectionId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "channel_buffer_collaborators")]
+pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: ChannelBufferCollaboratorId,
+ pub channel_id: ChannelId,
+ pub connection_id: i32,
+ pub connection_server_id: ServerId,
+ pub connection_lost: bool,
+ pub user_id: UserId,
+ pub replica_id: ReplicaId,
+}
+
+impl Model {
+ pub fn connection(&self) -> ConnectionId {
+ ConnectionId {
+ owner_id: self.connection_server_id.0 as u32,
+ id: self.connection_id as u32,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+ #[sea_orm(
+ belongs_to = "super::channel::Entity",
+ from = "Column::ChannelId",
+ to = "super::channel::Column::Id"
+ )]
+ Channel,
+}
+
+impl Related<super::channel::Entity> for Entity {
+ fn to() -> RelationDef {
+ Relation::Channel.def()
+ }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
@@ -1,3 +1,6 @@
+mod buffer_tests;
+mod db_tests;
+
use super::*;
use gpui::executor::Background;
use parking_lot::Mutex;
@@ -91,6 +94,26 @@ impl TestDb {
}
}
+#[macro_export]
+macro_rules! test_both_dbs {
+ ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
+ #[gpui::test]
+ async fn $postgres_test_name() {
+ let test_db = crate::db::TestDb::postgres(
+ gpui::executor::Deterministic::new(0).build_background(),
+ );
+ $test_name(test_db.db()).await;
+ }
+
+ #[gpui::test]
+ async fn $sqlite_test_name() {
+ let test_db =
+ crate::db::TestDb::sqlite(gpui::executor::Deterministic::new(0).build_background());
+ $test_name(test_db.db()).await;
+ }
+ };
+}
+
impl Drop for TestDb {
fn drop(&mut self) {
let db = self.db.take().unwrap();
@@ -0,0 +1,165 @@
+use super::*;
+use crate::test_both_dbs;
+use language::proto;
+use text::Buffer;
+
+test_both_dbs!(
+ test_channel_buffers,
+ test_channel_buffers_postgres,
+ test_channel_buffers_sqlite
+);
+
+async fn test_channel_buffers(db: &Arc<Database>) {
+ let a_id = db
+ .create_user(
+ "user_a@example.com",
+ false,
+ NewUserParams {
+ github_login: "user_a".into(),
+ github_user_id: 101,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+ let b_id = db
+ .create_user(
+ "user_b@example.com",
+ false,
+ NewUserParams {
+ github_login: "user_b".into(),
+ github_user_id: 102,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+
+ // This user will not be a part of the channel
+ let c_id = db
+ .create_user(
+ "user_c@example.com",
+ false,
+ NewUserParams {
+ github_login: "user_c".into(),
+ github_user_id: 102,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+
+ let owner_id = db.create_server("production").await.unwrap().0 as u32;
+
+ let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
+
+ db.invite_channel_member(zed_id, b_id, a_id, false)
+ .await
+ .unwrap();
+
+ db.respond_to_channel_invite(zed_id, b_id, true)
+ .await
+ .unwrap();
+
+ let connection_id_a = ConnectionId { owner_id, id: 1 };
+ let _ = db
+ .join_channel_buffer(zed_id, a_id, connection_id_a)
+ .await
+ .unwrap();
+
+ let mut buffer_a = Buffer::new(0, 0, "".to_string());
+ let mut operations = Vec::new();
+ operations.push(buffer_a.edit([(0..0, "hello world")]));
+ operations.push(buffer_a.edit([(5..5, ", cruel")]));
+ operations.push(buffer_a.edit([(0..5, "goodbye")]));
+ operations.push(buffer_a.undo().unwrap().1);
+ assert_eq!(buffer_a.text(), "hello, cruel world");
+
+ let operations = operations
+ .into_iter()
+ .map(|op| proto::serialize_operation(&language::Operation::Buffer(op)))
+ .collect::<Vec<_>>();
+
+ db.update_channel_buffer(zed_id, a_id, &operations)
+ .await
+ .unwrap();
+
+ let connection_id_b = ConnectionId { owner_id, id: 2 };
+ let buffer_response_b = db
+ .join_channel_buffer(zed_id, b_id, connection_id_b)
+ .await
+ .unwrap();
+
+ let mut buffer_b = Buffer::new(0, 0, buffer_response_b.base_text);
+ buffer_b
+ .apply_ops(buffer_response_b.operations.into_iter().map(|operation| {
+ let operation = proto::deserialize_operation(operation).unwrap();
+ if let language::Operation::Buffer(operation) = operation {
+ operation
+ } else {
+ unreachable!()
+ }
+ }))
+ .unwrap();
+
+ assert_eq!(buffer_b.text(), "hello, cruel world");
+
+ // Ensure that C fails to open the buffer
+ assert!(db
+ .join_channel_buffer(zed_id, c_id, ConnectionId { owner_id, id: 3 })
+ .await
+ .is_err());
+
+ // Ensure that both collaborators have shown up
+ assert_eq!(
+ buffer_response_b.collaborators,
+ &[
+ rpc::proto::Collaborator {
+ user_id: a_id.to_proto(),
+ peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
+ replica_id: 0,
+ },
+ rpc::proto::Collaborator {
+ user_id: b_id.to_proto(),
+ peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
+ replica_id: 1,
+ }
+ ]
+ );
+
+ // Ensure that get_channel_buffer_collaborators works
+ let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
+ assert_eq!(zed_collaborats, &[a_id, b_id]);
+
+ let collaborators = db
+ .leave_channel_buffer(zed_id, connection_id_b)
+ .await
+ .unwrap();
+
+ assert_eq!(collaborators, &[connection_id_a],);
+
+ let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
+ let _ = db
+ .join_channel_buffer(cargo_id, a_id, connection_id_a)
+ .await
+ .unwrap();
+
+ db.leave_channel_buffers(connection_id_a).await.unwrap();
+
+ let zed_collaborators = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
+ let cargo_collaborators = db.get_channel_buffer_collaborators(cargo_id).await.unwrap();
+ assert_eq!(zed_collaborators, &[]);
+ assert_eq!(cargo_collaborators, &[]);
+
+ // When everyone has left the channel, the operations are collapsed into
+ // a new base text.
+ let buffer_response_b = db
+ .join_channel_buffer(zed_id, b_id, connection_id_b)
+ .await
+ .unwrap();
+ assert_eq!(buffer_response_b.base_text, "hello, cruel world");
+ assert_eq!(buffer_response_b.operations, &[]);
+}
@@ -1,242 +1,234 @@
use super::*;
+use crate::test_both_dbs;
use gpui::executor::{Background, Deterministic};
use pretty_assertions::{assert_eq, assert_ne};
use std::sync::Arc;
-use test_db::TestDb;
-
-macro_rules! test_both_dbs {
- ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => {
- #[gpui::test]
- async fn $postgres_test_name() {
- let test_db = TestDb::postgres(Deterministic::new(0).build_background());
- let $db = test_db.db();
- $body
- }
-
- #[gpui::test]
- async fn $sqlite_test_name() {
- let test_db = TestDb::sqlite(Deterministic::new(0).build_background());
- let $db = test_db.db();
- $body
- }
- };
-}
+use tests::TestDb;
test_both_dbs!(
+ test_get_users,
test_get_users_by_ids_postgres,
- test_get_users_by_ids_sqlite,
- db,
- {
- let mut user_ids = Vec::new();
- let mut user_metric_ids = Vec::new();
- for i in 1..=4 {
- let user = db
- .create_user(
- &format!("user{i}@example.com"),
- false,
- NewUserParams {
- github_login: format!("user{i}"),
- github_user_id: i,
- invite_count: 0,
- },
- )
- .await
- .unwrap();
- user_ids.push(user.user_id);
- user_metric_ids.push(user.metrics_id);
- }
-
- assert_eq!(
- db.get_users_by_ids(user_ids.clone()).await.unwrap(),
- vec![
- User {
- id: user_ids[0],
- github_login: "user1".to_string(),
- github_user_id: Some(1),
- email_address: Some("user1@example.com".to_string()),
- admin: false,
- metrics_id: user_metric_ids[0].parse().unwrap(),
- ..Default::default()
- },
- User {
- id: user_ids[1],
- github_login: "user2".to_string(),
- github_user_id: Some(2),
- email_address: Some("user2@example.com".to_string()),
- admin: false,
- metrics_id: user_metric_ids[1].parse().unwrap(),
- ..Default::default()
- },
- User {
- id: user_ids[2],
- github_login: "user3".to_string(),
- github_user_id: Some(3),
- email_address: Some("user3@example.com".to_string()),
- admin: false,
- metrics_id: user_metric_ids[2].parse().unwrap(),
- ..Default::default()
- },
- User {
- id: user_ids[3],
- github_login: "user4".to_string(),
- github_user_id: Some(4),
- email_address: Some("user4@example.com".to_string()),
- admin: false,
- metrics_id: user_metric_ids[3].parse().unwrap(),
- ..Default::default()
- }
- ]
- );
- }
+ test_get_users_by_ids_sqlite
);
-test_both_dbs!(
- test_get_or_create_user_by_github_account_postgres,
- test_get_or_create_user_by_github_account_sqlite,
- db,
- {
- let user_id1 = db
- .create_user(
- "user1@example.com",
- false,
- NewUserParams {
- github_login: "login1".into(),
- github_user_id: 101,
- invite_count: 0,
- },
- )
- .await
- .unwrap()
- .user_id;
- let user_id2 = db
+async fn test_get_users(db: &Arc<Database>) {
+ let mut user_ids = Vec::new();
+ let mut user_metric_ids = Vec::new();
+ for i in 1..=4 {
+ let user = db
.create_user(
- "user2@example.com",
+ &format!("user{i}@example.com"),
false,
NewUserParams {
- github_login: "login2".into(),
- github_user_id: 102,
+ github_login: format!("user{i}"),
+ github_user_id: i,
invite_count: 0,
},
)
.await
- .unwrap()
- .user_id;
-
- let user = db
- .get_or_create_user_by_github_account("login1", None, None)
- .await
- .unwrap()
.unwrap();
- assert_eq!(user.id, user_id1);
- assert_eq!(&user.github_login, "login1");
- assert_eq!(user.github_user_id, Some(101));
-
- assert!(db
- .get_or_create_user_by_github_account("non-existent-login", None, None)
- .await
- .unwrap()
- .is_none());
+ user_ids.push(user.user_id);
+ user_metric_ids.push(user.metrics_id);
+ }
- let user = db
- .get_or_create_user_by_github_account("the-new-login2", Some(102), None)
- .await
- .unwrap()
- .unwrap();
- assert_eq!(user.id, user_id2);
- assert_eq!(&user.github_login, "the-new-login2");
- assert_eq!(user.github_user_id, Some(102));
+ assert_eq!(
+ db.get_users_by_ids(user_ids.clone()).await.unwrap(),
+ vec![
+ User {
+ id: user_ids[0],
+ github_login: "user1".to_string(),
+ github_user_id: Some(1),
+ email_address: Some("user1@example.com".to_string()),
+ admin: false,
+ metrics_id: user_metric_ids[0].parse().unwrap(),
+ ..Default::default()
+ },
+ User {
+ id: user_ids[1],
+ github_login: "user2".to_string(),
+ github_user_id: Some(2),
+ email_address: Some("user2@example.com".to_string()),
+ admin: false,
+ metrics_id: user_metric_ids[1].parse().unwrap(),
+ ..Default::default()
+ },
+ User {
+ id: user_ids[2],
+ github_login: "user3".to_string(),
+ github_user_id: Some(3),
+ email_address: Some("user3@example.com".to_string()),
+ admin: false,
+ metrics_id: user_metric_ids[2].parse().unwrap(),
+ ..Default::default()
+ },
+ User {
+ id: user_ids[3],
+ github_login: "user4".to_string(),
+ github_user_id: Some(4),
+ email_address: Some("user4@example.com".to_string()),
+ admin: false,
+ metrics_id: user_metric_ids[3].parse().unwrap(),
+ ..Default::default()
+ }
+ ]
+ );
+}
- let user = db
- .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com"))
- .await
- .unwrap()
- .unwrap();
- assert_eq!(&user.github_login, "login3");
- assert_eq!(user.github_user_id, Some(103));
- assert_eq!(user.email_address, Some("user3@example.com".into()));
- }
+test_both_dbs!(
+ test_get_or_create_user_by_github_account,
+ test_get_or_create_user_by_github_account_postgres,
+ test_get_or_create_user_by_github_account_sqlite
);
+async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
+ let user_id1 = db
+ .create_user(
+ "user1@example.com",
+ false,
+ NewUserParams {
+ github_login: "login1".into(),
+ github_user_id: 101,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+ let user_id2 = db
+ .create_user(
+ "user2@example.com",
+ false,
+ NewUserParams {
+ github_login: "login2".into(),
+ github_user_id: 102,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+
+ let user = db
+ .get_or_create_user_by_github_account("login1", None, None)
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(user.id, user_id1);
+ assert_eq!(&user.github_login, "login1");
+ assert_eq!(user.github_user_id, Some(101));
+
+ assert!(db
+ .get_or_create_user_by_github_account("non-existent-login", None, None)
+ .await
+ .unwrap()
+ .is_none());
+
+ let user = db
+ .get_or_create_user_by_github_account("the-new-login2", Some(102), None)
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(user.id, user_id2);
+ assert_eq!(&user.github_login, "the-new-login2");
+ assert_eq!(user.github_user_id, Some(102));
+
+ let user = db
+ .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com"))
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(&user.github_login, "login3");
+ assert_eq!(user.github_user_id, Some(103));
+ assert_eq!(user.email_address, Some("user3@example.com".into()));
+}
+
test_both_dbs!(
+ test_create_access_tokens,
test_create_access_tokens_postgres,
- test_create_access_tokens_sqlite,
- db,
- {
- let user = db
- .create_user(
- "u1@example.com",
- false,
- NewUserParams {
- github_login: "u1".into(),
- github_user_id: 1,
- invite_count: 0,
- },
- )
- .await
- .unwrap()
- .user_id;
-
- let token_1 = db.create_access_token(user, "h1", 2).await.unwrap();
- let token_2 = db.create_access_token(user, "h2", 2).await.unwrap();
- assert_eq!(
- db.get_access_token(token_1).await.unwrap(),
- access_token::Model {
- id: token_1,
- user_id: user,
- hash: "h1".into(),
- }
- );
- assert_eq!(
- db.get_access_token(token_2).await.unwrap(),
- access_token::Model {
- id: token_2,
- user_id: user,
- hash: "h2".into()
- }
- );
+ test_create_access_tokens_sqlite
+);
- let token_3 = db.create_access_token(user, "h3", 2).await.unwrap();
- assert_eq!(
- db.get_access_token(token_3).await.unwrap(),
- access_token::Model {
- id: token_3,
- user_id: user,
- hash: "h3".into()
- }
- );
- assert_eq!(
- db.get_access_token(token_2).await.unwrap(),
- access_token::Model {
- id: token_2,
- user_id: user,
- hash: "h2".into()
- }
- );
- assert!(db.get_access_token(token_1).await.is_err());
-
- let token_4 = db.create_access_token(user, "h4", 2).await.unwrap();
- assert_eq!(
- db.get_access_token(token_4).await.unwrap(),
- access_token::Model {
- id: token_4,
- user_id: user,
- hash: "h4".into()
- }
- );
- assert_eq!(
- db.get_access_token(token_3).await.unwrap(),
- access_token::Model {
- id: token_3,
- user_id: user,
- hash: "h3".into()
- }
- );
- assert!(db.get_access_token(token_2).await.is_err());
- assert!(db.get_access_token(token_1).await.is_err());
- }
+async fn test_create_access_tokens(db: &Arc<Database>) {
+ let user = db
+ .create_user(
+ "u1@example.com",
+ false,
+ NewUserParams {
+ github_login: "u1".into(),
+ github_user_id: 1,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+
+ let token_1 = db.create_access_token(user, "h1", 2).await.unwrap();
+ let token_2 = db.create_access_token(user, "h2", 2).await.unwrap();
+ assert_eq!(
+ db.get_access_token(token_1).await.unwrap(),
+ access_token::Model {
+ id: token_1,
+ user_id: user,
+ hash: "h1".into(),
+ }
+ );
+ assert_eq!(
+ db.get_access_token(token_2).await.unwrap(),
+ access_token::Model {
+ id: token_2,
+ user_id: user,
+ hash: "h2".into()
+ }
+ );
+
+ let token_3 = db.create_access_token(user, "h3", 2).await.unwrap();
+ assert_eq!(
+ db.get_access_token(token_3).await.unwrap(),
+ access_token::Model {
+ id: token_3,
+ user_id: user,
+ hash: "h3".into()
+ }
+ );
+ assert_eq!(
+ db.get_access_token(token_2).await.unwrap(),
+ access_token::Model {
+ id: token_2,
+ user_id: user,
+ hash: "h2".into()
+ }
+ );
+ assert!(db.get_access_token(token_1).await.is_err());
+
+ let token_4 = db.create_access_token(user, "h4", 2).await.unwrap();
+ assert_eq!(
+ db.get_access_token(token_4).await.unwrap(),
+ access_token::Model {
+ id: token_4,
+ user_id: user,
+ hash: "h4".into()
+ }
+ );
+ assert_eq!(
+ db.get_access_token(token_3).await.unwrap(),
+ access_token::Model {
+ id: token_3,
+ user_id: user,
+ hash: "h3".into()
+ }
+ );
+ assert!(db.get_access_token(token_2).await.is_err());
+ assert!(db.get_access_token(token_1).await.is_err());
+}
+
+test_both_dbs!(
+ test_add_contacts,
+ test_add_contacts_postgres,
+ test_add_contacts_sqlite
);
-test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
+async fn test_add_contacts(db: &Arc<Database>) {
let mut user_ids = Vec::new();
for i in 0..3 {
user_ids.push(
@@ -403,9 +395,15 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
busy: false,
}],
);
-});
+}
-test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
+test_both_dbs!(
+ test_metrics_id,
+ test_metrics_id_postgres,
+ test_metrics_id_sqlite
+);
+
+async fn test_metrics_id(db: &Arc<Database>) {
let NewUserResult {
user_id: user1,
metrics_id: metrics_id1,
@@ -444,82 +442,83 @@ test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
assert_eq!(metrics_id1.len(), 36);
assert_eq!(metrics_id2.len(), 36);
assert_ne!(metrics_id1, metrics_id2);
-});
+}
test_both_dbs!(
+ test_project_count,
test_project_count_postgres,
- test_project_count_sqlite,
- db,
- {
- let owner_id = db.create_server("test").await.unwrap().0 as u32;
+ test_project_count_sqlite
+);
- let user1 = db
- .create_user(
- &format!("admin@example.com"),
- true,
- NewUserParams {
- github_login: "admin".into(),
- github_user_id: 0,
- invite_count: 0,
- },
- )
- .await
- .unwrap();
- let user2 = db
- .create_user(
- &format!("user@example.com"),
- false,
- NewUserParams {
- github_login: "user".into(),
- github_user_id: 1,
- invite_count: 0,
- },
- )
- .await
- .unwrap();
+async fn test_project_count(db: &Arc<Database>) {
+ let owner_id = db.create_server("test").await.unwrap().0 as u32;
- let room_id = RoomId::from_proto(
- db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
- .await
- .unwrap()
- .id,
- );
- db.call(
- room_id,
- user1.user_id,
- ConnectionId { owner_id, id: 0 },
- user2.user_id,
- None,
+ let user1 = db
+ .create_user(
+ &format!("admin@example.com"),
+ true,
+ NewUserParams {
+ github_login: "admin".into(),
+ github_user_id: 0,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap();
+ let user2 = db
+ .create_user(
+ &format!("user@example.com"),
+ false,
+ NewUserParams {
+ github_login: "user".into(),
+ github_user_id: 1,
+ invite_count: 0,
+ },
)
.await
.unwrap();
- db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
- .await
- .unwrap();
- assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
- db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
+ let room_id = RoomId::from_proto(
+ db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
.await
- .unwrap();
- assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
+ .unwrap()
+ .id,
+ );
+ db.call(
+ room_id,
+ user1.user_id,
+ ConnectionId { owner_id, id: 0 },
+ user2.user_id,
+ None,
+ )
+ .await
+ .unwrap();
+ db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
+ .await
+ .unwrap();
+ assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
- db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
- .await
- .unwrap();
- assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
+ db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
+ .await
+ .unwrap();
+ assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
- // Projects shared by admins aren't counted.
- db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
- .await
- .unwrap();
- assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
+ db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
+ .await
+ .unwrap();
+ assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
- db.leave_room(ConnectionId { owner_id, id: 1 })
- .await
- .unwrap();
- assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
- }
-);
+ // Projects shared by admins aren't counted.
+ db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
+ .await
+ .unwrap();
+ assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
+
+ db.leave_room(ConnectionId { owner_id, id: 1 })
+ .await
+ .unwrap();
+ assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
+}
#[test]
fn test_fuzzy_like_string() {
@@ -878,7 +877,9 @@ async fn test_invite_codes() {
assert!(db.has_contact(user5, user1).await.unwrap());
}
-test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
+test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
+
+async fn test_channels(db: &Arc<Database>) {
let a_id = db
.create_user(
"user1@example.com",
@@ -1063,267 +1064,270 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
-});
+}
test_both_dbs!(
+ test_joining_channels,
test_joining_channels_postgres,
- test_joining_channels_sqlite,
- db,
- {
- let owner_id = db.create_server("test").await.unwrap().0 as u32;
+ test_joining_channels_sqlite
+);
- let user_1 = db
- .create_user(
- "user1@example.com",
- false,
- NewUserParams {
- github_login: "user1".into(),
- github_user_id: 5,
- invite_count: 0,
- },
- )
- .await
- .unwrap()
- .user_id;
- let user_2 = db
- .create_user(
- "user2@example.com",
- false,
- NewUserParams {
- github_login: "user2".into(),
- github_user_id: 6,
- invite_count: 0,
- },
- )
- .await
- .unwrap()
- .user_id;
+async fn test_joining_channels(db: &Arc<Database>) {
+ let owner_id = db.create_server("test").await.unwrap().0 as u32;
- let channel_1 = db
- .create_root_channel("channel_1", "1", user_1)
- .await
- .unwrap();
- let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
+ let user_1 = db
+ .create_user(
+ "user1@example.com",
+ false,
+ NewUserParams {
+ github_login: "user1".into(),
+ github_user_id: 5,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+ let user_2 = db
+ .create_user(
+ "user2@example.com",
+ false,
+ NewUserParams {
+ github_login: "user2".into(),
+ github_user_id: 6,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
- // can join a room with membership to its channel
- let joined_room = db
- .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
- .await
- .unwrap();
- assert_eq!(joined_room.room.participants.len(), 1);
+ let channel_1 = db
+ .create_root_channel("channel_1", "1", user_1)
+ .await
+ .unwrap();
+ let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
- drop(joined_room);
- // cannot join a room without membership to its channel
- assert!(db
- .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
- .await
- .is_err());
- }
-);
+ // can join a room with membership to its channel
+ let joined_room = db
+ .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
+ .await
+ .unwrap();
+ assert_eq!(joined_room.room.participants.len(), 1);
+
+ drop(joined_room);
+ // cannot join a room without membership to its channel
+ assert!(db
+ .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
+ .await
+ .is_err());
+}
test_both_dbs!(
+ test_channel_invites,
test_channel_invites_postgres,
- test_channel_invites_sqlite,
- db,
- {
- db.create_server("test").await.unwrap();
+ test_channel_invites_sqlite
+);
- let user_1 = db
- .create_user(
- "user1@example.com",
- false,
- NewUserParams {
- github_login: "user1".into(),
- github_user_id: 5,
- invite_count: 0,
- },
- )
- .await
- .unwrap()
- .user_id;
- let user_2 = db
- .create_user(
- "user2@example.com",
- false,
- NewUserParams {
- github_login: "user2".into(),
- github_user_id: 6,
- invite_count: 0,
- },
- )
- .await
- .unwrap()
- .user_id;
+async fn test_channel_invites(db: &Arc<Database>) {
+ db.create_server("test").await.unwrap();
- let user_3 = db
- .create_user(
- "user3@example.com",
- false,
- NewUserParams {
- github_login: "user3".into(),
- github_user_id: 7,
- invite_count: 0,
- },
- )
- .await
- .unwrap()
- .user_id;
+ let user_1 = db
+ .create_user(
+ "user1@example.com",
+ false,
+ NewUserParams {
+ github_login: "user1".into(),
+ github_user_id: 5,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+ let user_2 = db
+ .create_user(
+ "user2@example.com",
+ false,
+ NewUserParams {
+ github_login: "user2".into(),
+ github_user_id: 6,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
- let channel_1_1 = db
- .create_root_channel("channel_1", "1", user_1)
- .await
- .unwrap();
+ let user_3 = db
+ .create_user(
+ "user3@example.com",
+ false,
+ NewUserParams {
+ github_login: "user3".into(),
+ github_user_id: 7,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
- let channel_1_2 = db
- .create_root_channel("channel_2", "2", user_1)
- .await
- .unwrap();
+ let channel_1_1 = db
+ .create_root_channel("channel_1", "1", user_1)
+ .await
+ .unwrap();
- db.invite_channel_member(channel_1_1, user_2, user_1, false)
- .await
- .unwrap();
- db.invite_channel_member(channel_1_2, user_2, user_1, false)
- .await
- .unwrap();
- db.invite_channel_member(channel_1_1, user_3, user_1, true)
- .await
- .unwrap();
+ let channel_1_2 = db
+ .create_root_channel("channel_2", "2", user_1)
+ .await
+ .unwrap();
- let user_2_invites = db
- .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
- .await
- .unwrap()
- .into_iter()
- .map(|channel| channel.id)
- .collect::<Vec<_>>();
+ db.invite_channel_member(channel_1_1, user_2, user_1, false)
+ .await
+ .unwrap();
+ db.invite_channel_member(channel_1_2, user_2, user_1, false)
+ .await
+ .unwrap();
+ db.invite_channel_member(channel_1_1, user_3, user_1, true)
+ .await
+ .unwrap();
- assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
+ let user_2_invites = db
+ .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
+ .await
+ .unwrap()
+ .into_iter()
+ .map(|channel| channel.id)
+ .collect::<Vec<_>>();
- let user_3_invites = db
- .get_channel_invites_for_user(user_3) // -> [channel_1_1]
- .await
- .unwrap()
- .into_iter()
- .map(|channel| channel.id)
- .collect::<Vec<_>>();
+ assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
- assert_eq!(user_3_invites, &[channel_1_1]);
+ let user_3_invites = db
+ .get_channel_invites_for_user(user_3) // -> [channel_1_1]
+ .await
+ .unwrap()
+ .into_iter()
+ .map(|channel| channel.id)
+ .collect::<Vec<_>>();
- let members = db
- .get_channel_member_details(channel_1_1, user_1)
- .await
- .unwrap();
- assert_eq!(
- members,
- &[
- proto::ChannelMember {
- user_id: user_1.to_proto(),
- kind: proto::channel_member::Kind::Member.into(),
- admin: true,
- },
- proto::ChannelMember {
- user_id: user_2.to_proto(),
- kind: proto::channel_member::Kind::Invitee.into(),
- admin: false,
- },
- proto::ChannelMember {
- user_id: user_3.to_proto(),
- kind: proto::channel_member::Kind::Invitee.into(),
- admin: true,
- },
- ]
- );
+ assert_eq!(user_3_invites, &[channel_1_1]);
- db.respond_to_channel_invite(channel_1_1, user_2, true)
- .await
- .unwrap();
+ let members = db
+ .get_channel_member_details(channel_1_1, user_1)
+ .await
+ .unwrap();
+ assert_eq!(
+ members,
+ &[
+ proto::ChannelMember {
+ user_id: user_1.to_proto(),
+ kind: proto::channel_member::Kind::Member.into(),
+ admin: true,
+ },
+ proto::ChannelMember {
+ user_id: user_2.to_proto(),
+ kind: proto::channel_member::Kind::Invitee.into(),
+ admin: false,
+ },
+ proto::ChannelMember {
+ user_id: user_3.to_proto(),
+ kind: proto::channel_member::Kind::Invitee.into(),
+ admin: true,
+ },
+ ]
+ );
- let channel_1_3 = db
- .create_channel("channel_3", Some(channel_1_1), "1", user_1)
- .await
- .unwrap();
+ db.respond_to_channel_invite(channel_1_1, user_2, true)
+ .await
+ .unwrap();
- let members = db
- .get_channel_member_details(channel_1_3, user_1)
- .await
- .unwrap();
- assert_eq!(
- members,
- &[
- proto::ChannelMember {
- user_id: user_1.to_proto(),
- kind: proto::channel_member::Kind::Member.into(),
- admin: true,
- },
- proto::ChannelMember {
- user_id: user_2.to_proto(),
- kind: proto::channel_member::Kind::AncestorMember.into(),
- admin: false,
- },
- ]
- );
- }
-);
+ let channel_1_3 = db
+ .create_channel("channel_3", Some(channel_1_1), "1", user_1)
+ .await
+ .unwrap();
+
+ let members = db
+ .get_channel_member_details(channel_1_3, user_1)
+ .await
+ .unwrap();
+ assert_eq!(
+ members,
+ &[
+ proto::ChannelMember {
+ user_id: user_1.to_proto(),
+ kind: proto::channel_member::Kind::Member.into(),
+ admin: true,
+ },
+ proto::ChannelMember {
+ user_id: user_2.to_proto(),
+ kind: proto::channel_member::Kind::AncestorMember.into(),
+ admin: false,
+ },
+ ]
+ );
+}
test_both_dbs!(
+ test_channel_renames,
test_channel_renames_postgres,
- test_channel_renames_sqlite,
- db,
- {
- db.create_server("test").await.unwrap();
+ test_channel_renames_sqlite
+);
- let user_1 = db
- .create_user(
- "user1@example.com",
- false,
- NewUserParams {
- github_login: "user1".into(),
- github_user_id: 5,
- invite_count: 0,
- },
- )
- .await
- .unwrap()
- .user_id;
+async fn test_channel_renames(db: &Arc<Database>) {
+ db.create_server("test").await.unwrap();
- let user_2 = db
- .create_user(
- "user2@example.com",
- false,
- NewUserParams {
- github_login: "user2".into(),
- github_user_id: 6,
- invite_count: 0,
- },
- )
- .await
- .unwrap()
- .user_id;
+ let user_1 = db
+ .create_user(
+ "user1@example.com",
+ false,
+ NewUserParams {
+ github_login: "user1".into(),
+ github_user_id: 5,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
- let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
+ let user_2 = db
+ .create_user(
+ "user2@example.com",
+ false,
+ NewUserParams {
+ github_login: "user2".into(),
+ github_user_id: 6,
+ invite_count: 0,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
- db.rename_channel(zed_id, user_1, "#zed-archive")
- .await
- .unwrap();
+ let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
- let zed_archive_id = zed_id;
+ db.rename_channel(zed_id, user_1, "#zed-archive")
+ .await
+ .unwrap();
- let (channel, _) = db
- .get_channel(zed_archive_id, user_1)
- .await
- .unwrap()
- .unwrap();
- assert_eq!(channel.name, "zed-archive");
+ let zed_archive_id = zed_id;
- let non_permissioned_rename = db
- .rename_channel(zed_archive_id, user_2, "hacked-lol")
- .await;
- assert!(non_permissioned_rename.is_err());
+ let (channel, _) = db
+ .get_channel(zed_archive_id, user_1)
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(channel.name, "zed-archive");
- let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
- assert!(bad_name_rename.is_err())
- }
-);
+ let non_permissioned_rename = db
+ .rename_channel(zed_archive_id, user_2, "hacked-lol")
+ .await;
+ assert!(non_permissioned_rename.is_err());
+
+ let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
+ assert!(bad_name_rename.is_err())
+}
#[gpui::test]
async fn test_multiple_signup_overwrite() {
@@ -35,8 +35,8 @@ use lazy_static::lazy_static;
use prometheus::{register_int_gauge, IntGauge};
use rpc::{
proto::{
- self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
- RequestMessage,
+ self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage,
+ LiveKitConnectionInfo, RequestMessage,
},
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
};
@@ -248,6 +248,9 @@ impl Server {
.add_request_handler(remove_channel_member)
.add_request_handler(set_channel_member_admin)
.add_request_handler(rename_channel)
+ .add_request_handler(join_channel_buffer)
+ .add_request_handler(leave_channel_buffer)
+ .add_message_handler(update_channel_buffer)
.add_request_handler(get_channel_members)
.add_request_handler(respond_to_channel_invite)
.add_request_handler(join_channel)
@@ -851,6 +854,10 @@ async fn connection_lost(
.await
.trace_err();
+ leave_channel_buffers_for_session(&session)
+ .await
+ .trace_err();
+
futures::select_biased! {
_ = executor.sleep(RECONNECT_TIMEOUT).fuse() => {
leave_room_for_session(&session).await.trace_err();
@@ -866,6 +873,8 @@ async fn connection_lost(
}
}
update_user_contacts(session.user_id, &session).await?;
+
+
}
_ = teardown.changed().fuse() => {}
}
@@ -2478,6 +2487,104 @@ async fn join_channel(
Ok(())
}
+async fn join_channel_buffer(
+ request: proto::JoinChannelBuffer,
+ response: Response<proto::JoinChannelBuffer>,
+ session: Session,
+) -> Result<()> {
+ let db = session.db().await;
+ let channel_id = ChannelId::from_proto(request.channel_id);
+
+ let open_response = db
+ .join_channel_buffer(channel_id, session.user_id, session.connection_id)
+ .await?;
+
+ let replica_id = open_response.replica_id;
+ let collaborators = open_response.collaborators.clone();
+
+ response.send(open_response)?;
+
+ let update = AddChannelBufferCollaborator {
+ channel_id: channel_id.to_proto(),
+ collaborator: Some(proto::Collaborator {
+ user_id: session.user_id.to_proto(),
+ peer_id: Some(session.connection_id.into()),
+ replica_id,
+ }),
+ };
+ channel_buffer_updated(
+ session.connection_id,
+ collaborators
+ .iter()
+ .filter_map(|collaborator| Some(collaborator.peer_id?.into())),
+ &update,
+ &session.peer,
+ );
+
+ Ok(())
+}
+
+async fn update_channel_buffer(
+ request: proto::UpdateChannelBuffer,
+ session: Session,
+) -> Result<()> {
+ let db = session.db().await;
+ let channel_id = ChannelId::from_proto(request.channel_id);
+
+ let collaborators = db
+ .update_channel_buffer(channel_id, session.user_id, &request.operations)
+ .await?;
+
+ channel_buffer_updated(
+ session.connection_id,
+ collaborators,
+ &proto::UpdateChannelBuffer {
+ channel_id: channel_id.to_proto(),
+ operations: request.operations,
+ },
+ &session.peer,
+ );
+ Ok(())
+}
+
+async fn leave_channel_buffer(
+ request: proto::LeaveChannelBuffer,
+ response: Response<proto::LeaveChannelBuffer>,
+ session: Session,
+) -> Result<()> {
+ let db = session.db().await;
+ let channel_id = ChannelId::from_proto(request.channel_id);
+
+ let collaborators_to_notify = db
+ .leave_channel_buffer(channel_id, session.connection_id)
+ .await?;
+
+ response.send(Ack {})?;
+
+ channel_buffer_updated(
+ session.connection_id,
+ collaborators_to_notify,
+ &proto::RemoveChannelBufferCollaborator {
+ channel_id: channel_id.to_proto(),
+ peer_id: Some(session.connection_id.into()),
+ },
+ &session.peer,
+ );
+
+ Ok(())
+}
+
+fn channel_buffer_updated<T: EnvelopedMessage>(
+ sender_id: ConnectionId,
+ collaborators: impl IntoIterator<Item = ConnectionId>,
+ message: &T,
+ peer: &Peer,
+) {
+ broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| {
+ peer.send(peer_id.into(), message.clone())
+ });
+}
+
async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let project_connection_ids = session
@@ -2803,6 +2910,28 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
Ok(())
}
+async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
+ let left_channel_buffers = session
+ .db()
+ .await
+ .leave_channel_buffers(session.connection_id)
+ .await?;
+
+ for (channel_id, connections) in left_channel_buffers {
+ channel_buffer_updated(
+ session.connection_id,
+ connections,
+ &proto::RemoveChannelBufferCollaborator {
+ channel_id: channel_id.to_proto(),
+ peer_id: Some(session.connection_id.into()),
+ },
+ &session.peer,
+ );
+ }
+
+ Ok(())
+}
+
fn project_left(project: &db::LeftProject, session: &Session) {
for connection_id in &project.connection_ids {
if project.host_user_id == session.user_id {
@@ -1,14 +1,14 @@
use crate::{
- db::{test_db::TestDb, NewUserParams, UserId},
+ db::{tests::TestDb, NewUserParams, UserId},
executor::Executor,
rpc::{Server, CLEANUP_TIMEOUT},
AppState,
};
use anyhow::anyhow;
use call::{ActiveCall, Room};
+use channel::ChannelStore;
use client::{
- self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError,
- UserStore,
+ self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
};
use collections::{HashMap, HashSet};
use fs::FakeFs;
@@ -31,6 +31,7 @@ use std::{
use util::http::FakeHttpClient;
use workspace::Workspace;
+mod channel_buffer_tests;
mod channel_tests;
mod integration_tests;
mod randomized_integration_tests;
@@ -210,6 +211,7 @@ impl TestServer {
workspace::init(app_state.clone(), cx);
audio::init((), cx);
call::init(client.clone(), user_store.clone(), cx);
+ channel::init(&client);
});
client
@@ -0,0 +1,426 @@
+use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+use call::ActiveCall;
+use channel::Channel;
+use client::UserId;
+use collab_ui::channel_view::ChannelView;
+use collections::HashMap;
+use futures::future;
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
+use rpc::{proto, RECEIVE_TIMEOUT};
+use serde_json::json;
+use std::sync::Arc;
+
+#[gpui::test]
+async fn test_core_channel_buffers(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+
+ let zed_id = server
+ .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+ .await;
+
+ // Client A joins the channel buffer
+ let channel_buffer_a = client_a
+ .channel_store()
+ .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx))
+ .await
+ .unwrap();
+
+ // Client A edits the buffer
+ let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer());
+
+ buffer_a.update(cx_a, |buffer, cx| {
+ buffer.edit([(0..0, "hello world")], None, cx)
+ });
+ buffer_a.update(cx_a, |buffer, cx| {
+ buffer.edit([(5..5, ", cruel")], None, cx)
+ });
+ buffer_a.update(cx_a, |buffer, cx| {
+ buffer.edit([(0..5, "goodbye")], None, cx)
+ });
+ buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx));
+ deterministic.run_until_parked();
+
+ assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world");
+
+ // Client B joins the channel buffer
+ let channel_buffer_b = client_b
+ .channel_store()
+ .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx))
+ .await
+ .unwrap();
+
+ channel_buffer_b.read_with(cx_b, |buffer, _| {
+ assert_collaborators(
+ buffer.collaborators(),
+ &[client_a.user_id(), client_b.user_id()],
+ );
+ });
+
+ // Client B sees the correct text, and then edits it
+ let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer());
+ assert_eq!(
+ buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()),
+ buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id())
+ );
+ assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world");
+ buffer_b.update(cx_b, |buffer, cx| {
+ buffer.edit([(7..12, "beautiful")], None, cx)
+ });
+
+ // Both A and B see the new edit
+ deterministic.run_until_parked();
+ assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world");
+ assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world");
+
+ // Client A closes the channel buffer.
+ cx_a.update(|_| drop(channel_buffer_a));
+ deterministic.run_until_parked();
+
+ // Client B sees that client A is gone from the channel buffer.
+ channel_buffer_b.read_with(cx_b, |buffer, _| {
+ assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
+ });
+
+ // Client A rejoins the channel buffer
+ let _channel_buffer_a = client_a
+ .channel_store()
+ .update(cx_a, |channels, cx| {
+ channels.open_channel_buffer(zed_id, cx)
+ })
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+
+ // Sanity test, make sure we saw A rejoining
+ channel_buffer_b.read_with(cx_b, |buffer, _| {
+ assert_collaborators(
+ &buffer.collaborators(),
+ &[client_b.user_id(), client_a.user_id()],
+ );
+ });
+
+ // Client A loses connection.
+ server.forbid_connections();
+ server.disconnect_client(client_a.peer_id().unwrap());
+ deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+ // Client B observes A disconnect
+ channel_buffer_b.read_with(cx_b, |buffer, _| {
+ assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
+ });
+
+ // TODO:
+ // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects
+ // - Test interaction with channel deletion while buffer is open
+}
+
+#[gpui::test]
+async fn test_channel_buffer_replica_ids(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ cx_c: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ let client_c = server.create_client(cx_c, "user_c").await;
+
+ let channel_id = server
+ .make_channel(
+ "zed",
+ (&client_a, cx_a),
+ &mut [(&client_b, cx_b), (&client_c, cx_c)],
+ )
+ .await;
+
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+ let active_call_c = cx_c.read(ActiveCall::global);
+
+ // Clients A and B join a channel.
+ active_call_a
+ .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
+ .await
+ .unwrap();
+ active_call_b
+ .update(cx_b, |call, cx| call.join_channel(channel_id, cx))
+ .await
+ .unwrap();
+
+ // Clients A, B, and C join a channel buffer
+ // C first so that the replica IDs in the project and the channel buffer are different
+ let channel_buffer_c = client_c
+ .channel_store()
+ .update(cx_c, |channel, cx| {
+ channel.open_channel_buffer(channel_id, cx)
+ })
+ .await
+ .unwrap();
+ let channel_buffer_b = client_b
+ .channel_store()
+ .update(cx_b, |channel, cx| {
+ channel.open_channel_buffer(channel_id, cx)
+ })
+ .await
+ .unwrap();
+ let channel_buffer_a = client_a
+ .channel_store()
+ .update(cx_a, |channel, cx| {
+ channel.open_channel_buffer(channel_id, cx)
+ })
+ .await
+ .unwrap();
+
+ // Client B shares a project
+ client_b
+ .fs()
+ .insert_tree("/dir", json!({ "file.txt": "contents" }))
+ .await;
+ let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
+ let shared_project_id = active_call_b
+ .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+ .await
+ .unwrap();
+
+ // Client A joins the project
+ let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
+ deterministic.run_until_parked();
+
+ // Client C is in a separate project.
+ client_c.fs().insert_tree("/dir", json!({})).await;
+ let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
+
+ // Note that each user has a different replica id in the projects vs the
+ // channel buffer.
+ channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
+ assert_eq!(project_a.read(cx).replica_id(), 1);
+ assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
+ });
+ channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
+ assert_eq!(project_b.read(cx).replica_id(), 0);
+ assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
+ });
+ channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
+ // C is not in the project
+ assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
+ });
+
+ let channel_window_a =
+ cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
+ let channel_window_b =
+ cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
+ let channel_window_c = cx_c.add_window(|cx| {
+ ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
+ });
+
+ let channel_view_a = channel_window_a.root(cx_a);
+ let channel_view_b = channel_window_b.root(cx_b);
+ let channel_view_c = channel_window_c.root(cx_c);
+
+ // For clients A and B, the replica ids in the channel buffer are mapped
+ // so that they match the same users' replica ids in their shared project.
+ channel_view_a.read_with(cx_a, |view, cx| {
+ assert_eq!(
+ view.editor.read(cx).replica_id_map().unwrap(),
+ &[(1, 0), (2, 1)].into_iter().collect::<HashMap<_, _>>()
+ );
+ });
+ channel_view_b.read_with(cx_b, |view, cx| {
+ assert_eq!(
+ view.editor.read(cx).replica_id_map().unwrap(),
+ &[(1, 0), (2, 1)].into_iter().collect::<HashMap<u16, u16>>(),
+ )
+ });
+
+ // Client C only sees themself, as they're not part of any shared project
+ channel_view_c.read_with(cx_c, |view, cx| {
+ assert_eq!(
+ view.editor.read(cx).replica_id_map().unwrap(),
+ &[(0, 0)].into_iter().collect::<HashMap<u16, u16>>(),
+ );
+ });
+
+ // Client C joins the project that clients A and B are in.
+ active_call_c
+ .update(cx_c, |call, cx| call.join_channel(channel_id, cx))
+ .await
+ .unwrap();
+ let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
+ deterministic.run_until_parked();
+ project_c.read_with(cx_c, |project, _| {
+ assert_eq!(project.replica_id(), 2);
+ });
+
+ // For clients A and B, client C's replica id in the channel buffer is
+ // now mapped to their replica id in the shared project.
+ channel_view_a.read_with(cx_a, |view, cx| {
+ assert_eq!(
+ view.editor.read(cx).replica_id_map().unwrap(),
+ &[(1, 0), (2, 1), (0, 2)]
+ .into_iter()
+ .collect::<HashMap<_, _>>()
+ );
+ });
+ channel_view_b.read_with(cx_b, |view, cx| {
+ assert_eq!(
+ view.editor.read(cx).replica_id_map().unwrap(),
+ &[(1, 0), (2, 1), (0, 2)]
+ .into_iter()
+ .collect::<HashMap<_, _>>(),
+ )
+ });
+}
+
+#[gpui::test]
+async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+
+ let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await;
+
+ let channel_buffer_1 = client_a
+ .channel_store()
+ .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
+ let channel_buffer_2 = client_a
+ .channel_store()
+ .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
+ let channel_buffer_3 = client_a
+ .channel_store()
+ .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
+
+ // All concurrent tasks for opening a channel buffer return the same model handle.
+ let (channel_buffer_1, channel_buffer_2, channel_buffer_3) =
+ future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3)
+ .await
+ .unwrap();
+ let model_id = channel_buffer_1.id();
+ assert_eq!(channel_buffer_1, channel_buffer_2);
+ assert_eq!(channel_buffer_1, channel_buffer_3);
+
+ channel_buffer_1.update(cx_a, |buffer, cx| {
+ buffer.buffer().update(cx, |buffer, cx| {
+ buffer.edit([(0..0, "hello")], None, cx);
+ })
+ });
+ deterministic.run_until_parked();
+
+ cx_a.update(|_| {
+ drop(channel_buffer_1);
+ drop(channel_buffer_2);
+ drop(channel_buffer_3);
+ });
+ deterministic.run_until_parked();
+
+ // The channel buffer can be reopened after dropping it.
+ let channel_buffer = client_a
+ .channel_store()
+ .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx))
+ .await
+ .unwrap();
+ assert_ne!(channel_buffer.id(), model_id);
+ channel_buffer.update(cx_a, |buffer, cx| {
+ buffer.buffer().update(cx, |buffer, _| {
+ assert_eq!(buffer.text(), "hello");
+ })
+ });
+}
+
+#[gpui::test]
+async fn test_channel_buffer_disconnect(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+
+ let channel_id = server
+ .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+ .await;
+
+ let channel_buffer_a = client_a
+ .channel_store()
+ .update(cx_a, |channel, cx| {
+ channel.open_channel_buffer(channel_id, cx)
+ })
+ .await
+ .unwrap();
+
+ let channel_buffer_b = client_b
+ .channel_store()
+ .update(cx_b, |channel, cx| {
+ channel.open_channel_buffer(channel_id, cx)
+ })
+ .await
+ .unwrap();
+
+ server.forbid_connections();
+ server.disconnect_client(client_a.peer_id().unwrap());
+ deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+ channel_buffer_a.update(cx_a, |buffer, _| {
+ assert_eq!(
+ buffer.channel().as_ref(),
+ &Channel {
+ id: channel_id,
+ name: "zed".to_string()
+ }
+ );
+ assert!(!buffer.is_connected());
+ });
+
+ deterministic.run_until_parked();
+
+ server.allow_connections();
+ deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+ deterministic.run_until_parked();
+
+ client_a
+ .channel_store()
+ .update(cx_a, |channel_store, _| {
+ channel_store.remove_channel(channel_id)
+ })
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+
+ // Channel buffer observed the deletion
+ channel_buffer_b.update(cx_b, |buffer, _| {
+ assert_eq!(
+ buffer.channel().as_ref(),
+ &Channel {
+ id: channel_id,
+ name: "zed".to_string()
+ }
+ );
+ assert!(!buffer.is_connected());
+ });
+}
+
+#[track_caller]
+fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
+ assert_eq!(
+ collaborators
+ .into_iter()
+ .map(|collaborator| collaborator.user_id)
+ .collect::<Vec<_>>(),
+ ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
+ );
+}
+
+fn buffer_text(channel_buffer: &ModelHandle<language::Buffer>, cx: &mut TestAppContext) -> String {
+ channel_buffer.read_with(cx, |buffer, _| buffer.text())
+}
@@ -3,7 +3,8 @@ use crate::{
tests::{room_participants, RoomParticipants, TestServer},
};
use call::ActiveCall;
-use client::{ChannelId, ChannelMembership, ChannelStore, User};
+use channel::{ChannelId, ChannelMembership, ChannelStore};
+use client::User;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
use rpc::{proto, RECEIVE_TIMEOUT};
use std::sync::Arc;
@@ -798,7 +799,7 @@ async fn test_lost_channel_creation(
deterministic.run_until_parked();
- // Sanity check
+ // Sanity check, B has the invitation
assert_channel_invitations(
client_b.channel_store(),
cx_b,
@@ -810,6 +811,7 @@ async fn test_lost_channel_creation(
}],
);
+ // A creates a subchannel while the invite is still pending.
let subchannel_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
@@ -840,7 +842,7 @@ async fn test_lost_channel_creation(
],
);
- // Accept the invite
+ // Client B accepts the invite
client_b
.channel_store()
.update(cx_b, |channel_store, _| {
@@ -851,7 +853,7 @@ async fn test_lost_channel_creation(
deterministic.run_until_parked();
- // B should now see the channel
+ // Client B should now see the channel
assert_channels(
client_b.channel_store(),
cx_b,
@@ -26,6 +26,7 @@ auto_update = { path = "../auto_update" }
db = { path = "../db" }
call = { path = "../call" }
client = { path = "../client" }
+channel = { path = "../channel" }
clock = { path = "../clock" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
@@ -33,6 +34,7 @@ editor = { path = "../editor" }
feedback = { path = "../feedback" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
+language = { path = "../language" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
@@ -0,0 +1,351 @@
+use anyhow::{anyhow, Result};
+use channel::{
+ channel_buffer::{self, ChannelBuffer},
+ ChannelId,
+};
+use client::proto;
+use clock::ReplicaId;
+use collections::HashMap;
+use editor::Editor;
+use gpui::{
+ actions,
+ elements::{ChildView, Label},
+ geometry::vector::Vector2F,
+ AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
+ ViewContext, ViewHandle,
+};
+use project::Project;
+use std::any::Any;
+use workspace::{
+ item::{FollowableItem, Item, ItemHandle},
+ register_followable_item,
+ searchable::SearchableItemHandle,
+ ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
+};
+
+actions!(channel_view, [Deploy]);
+
+pub(crate) fn init(cx: &mut AppContext) {
+ register_followable_item::<ChannelView>(cx)
+}
+
+pub struct ChannelView {
+ pub editor: ViewHandle<Editor>,
+ project: ModelHandle<Project>,
+ channel_buffer: ModelHandle<ChannelBuffer>,
+ remote_id: Option<ViewId>,
+ _editor_event_subscription: Subscription,
+}
+
+impl ChannelView {
+ pub fn open(
+ channel_id: ChannelId,
+ pane: ViewHandle<Pane>,
+ workspace: ViewHandle<Workspace>,
+ cx: &mut AppContext,
+ ) -> Task<Result<ViewHandle<Self>>> {
+ let workspace = workspace.read(cx);
+ let project = workspace.project().to_owned();
+ let channel_store = workspace.app_state().channel_store.clone();
+ let markdown = workspace
+ .app_state()
+ .languages
+ .language_for_name("Markdown");
+ let channel_buffer =
+ channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
+
+ cx.spawn(|mut cx| async move {
+ let channel_buffer = channel_buffer.await?;
+ let markdown = markdown.await?;
+ channel_buffer.update(&mut cx, |buffer, cx| {
+ buffer.buffer().update(cx, |buffer, cx| {
+ buffer.set_language(Some(markdown), cx);
+ })
+ });
+
+ pane.update(&mut cx, |pane, cx| {
+ pane.items_of_type::<Self>()
+ .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
+ .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx)))
+ })
+ .ok_or_else(|| anyhow!("pane was dropped"))
+ })
+ }
+
+ pub fn new(
+ project: ModelHandle<Project>,
+ channel_buffer: ModelHandle<ChannelBuffer>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let buffer = channel_buffer.read(cx).buffer();
+ // buffer.update(cx, |buffer, cx| buffer.set_language(language, cx));
+ let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
+ let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
+
+ cx.subscribe(&project, Self::handle_project_event).detach();
+ cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
+ .detach();
+
+ let this = Self {
+ editor,
+ project,
+ channel_buffer,
+ remote_id: None,
+ _editor_event_subscription,
+ };
+ this.refresh_replica_id_map(cx);
+ this
+ }
+
+ fn handle_project_event(
+ &mut self,
+ _: ModelHandle<Project>,
+ event: &project::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ project::Event::RemoteIdChanged(_) => {}
+ project::Event::DisconnectedFromHost => {}
+ project::Event::Closed => {}
+ project::Event::CollaboratorUpdated { .. } => {}
+ project::Event::CollaboratorLeft(_) => {}
+ project::Event::CollaboratorJoined(_) => {}
+ _ => return,
+ }
+ self.refresh_replica_id_map(cx);
+ }
+
+ fn handle_channel_buffer_event(
+ &mut self,
+ _: ModelHandle<ChannelBuffer>,
+ event: &channel_buffer::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ channel_buffer::Event::CollaboratorsChanged => {
+ self.refresh_replica_id_map(cx);
+ }
+ channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| {
+ editor.set_read_only(true);
+ cx.notify();
+ }),
+ }
+ }
+
+ /// Build a mapping of channel buffer replica ids to the corresponding
+ /// replica ids in the current project.
+ ///
+ /// Using this mapping, a given user can be displayed with the same color
+ /// in the channel buffer as in other files in the project. Users who are
+ /// in the channel buffer but not the project will not have a color.
+ fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
+ let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
+ let project = self.project.read(cx);
+ let channel_buffer = self.channel_buffer.read(cx);
+ project_replica_ids_by_channel_buffer_replica_id
+ .insert(channel_buffer.replica_id(cx), project.replica_id());
+ project_replica_ids_by_channel_buffer_replica_id.extend(
+ channel_buffer
+ .collaborators()
+ .iter()
+ .filter_map(|channel_buffer_collaborator| {
+ project
+ .collaborators()
+ .values()
+ .find_map(|project_collaborator| {
+ (project_collaborator.user_id == channel_buffer_collaborator.user_id)
+ .then_some((
+ channel_buffer_collaborator.replica_id as ReplicaId,
+ project_collaborator.replica_id,
+ ))
+ })
+ }),
+ );
+
+ self.editor.update(cx, |editor, cx| {
+ editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
+ });
+ }
+}
+
+impl Entity for ChannelView {
+ type Event = editor::Event;
+}
+
+impl View for ChannelView {
+ fn ui_name() -> &'static str {
+ "ChannelView"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ ChildView::new(self.editor.as_any(), cx).into_any()
+ }
+
+ fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ if cx.is_self_focused() {
+ cx.focus(self.editor.as_any())
+ }
+ }
+}
+
+impl Item for ChannelView {
+ fn tab_content<V: 'static>(
+ &self,
+ _: Option<usize>,
+ style: &theme::Tab,
+ cx: &gpui::AppContext,
+ ) -> AnyElement<V> {
+ let channel_name = &self.channel_buffer.read(cx).channel().name;
+ let label = if self.channel_buffer.read(cx).is_connected() {
+ format!("#{}", channel_name)
+ } else {
+ format!("#{} (disconnected)", channel_name)
+ };
+ Label::new(label, style.label.to_owned()).into_any()
+ }
+
+ fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
+ Some(Self::new(
+ self.project.clone(),
+ self.channel_buffer.clone(),
+ cx,
+ ))
+ }
+
+ fn is_singleton(&self, _cx: &AppContext) -> bool {
+ true
+ }
+
+ fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
+ self.editor
+ .update(cx, |editor, cx| editor.navigate(data, cx))
+ }
+
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ self.editor
+ .update(cx, |editor, cx| Item::deactivated(editor, cx))
+ }
+
+ fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+ self.editor
+ .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
+ }
+
+ fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ Some(Box::new(self.editor.clone()))
+ }
+
+ fn show_toolbar(&self) -> bool {
+ true
+ }
+
+ fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+ self.editor.read(cx).pixel_position_of_cursor(cx)
+ }
+}
+
+impl FollowableItem for ChannelView {
+ fn remote_id(&self) -> Option<workspace::ViewId> {
+ self.remote_id
+ }
+
+ fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+ let channel = self.channel_buffer.read(cx).channel();
+ Some(proto::view::Variant::ChannelView(
+ proto::view::ChannelView {
+ channel_id: channel.id,
+ editor: if let Some(proto::view::Variant::Editor(proto)) =
+ self.editor.read(cx).to_state_proto(cx)
+ {
+ Some(proto)
+ } else {
+ None
+ },
+ },
+ ))
+ }
+
+ fn from_state_proto(
+ pane: ViewHandle<workspace::Pane>,
+ workspace: ViewHandle<workspace::Workspace>,
+ remote_id: workspace::ViewId,
+ state: &mut Option<proto::view::Variant>,
+ cx: &mut AppContext,
+ ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
+ let Some(proto::view::Variant::ChannelView(_)) = state else { return None };
+ let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() };
+
+ let open = ChannelView::open(state.channel_id, pane, workspace, cx);
+
+ Some(cx.spawn(|mut cx| async move {
+ let this = open.await?;
+
+ let task = this
+ .update(&mut cx, |this, cx| {
+ this.remote_id = Some(remote_id);
+
+ if let Some(state) = state.editor {
+ Some(this.editor.update(cx, |editor, cx| {
+ editor.apply_update_proto(
+ &this.project,
+ proto::update_view::Variant::Editor(proto::update_view::Editor {
+ selections: state.selections,
+ pending_selection: state.pending_selection,
+ scroll_top_anchor: state.scroll_top_anchor,
+ scroll_x: state.scroll_x,
+ scroll_y: state.scroll_y,
+ ..Default::default()
+ }),
+ cx,
+ )
+ }))
+ } else {
+ None
+ }
+ })
+ .ok_or_else(|| anyhow!("window was closed"))?;
+
+ if let Some(task) = task {
+ task.await?;
+ }
+
+ Ok(this)
+ }))
+ }
+
+ fn add_event_to_update_proto(
+ &self,
+ event: &Self::Event,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &AppContext,
+ ) -> bool {
+ self.editor
+ .read(cx)
+ .add_event_to_update_proto(event, update, cx)
+ }
+
+ fn apply_update_proto(
+ &mut self,
+ project: &ModelHandle<Project>,
+ message: proto::update_view::Variant,
+ cx: &mut ViewContext<Self>,
+ ) -> gpui::Task<anyhow::Result<()>> {
+ self.editor.update(cx, |editor, cx| {
+ editor.apply_update_proto(project, message, cx)
+ })
+ }
+
+ fn set_leader_replica_id(
+ &mut self,
+ leader_replica_id: Option<u16>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.editor.update(cx, |editor, cx| {
+ editor.set_leader_replica_id(leader_replica_id, cx)
+ })
+ }
+
+ fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
+ Editor::should_unfollow_on_event(event, cx)
+ }
+}
@@ -4,10 +4,8 @@ mod panel_settings;
use anyhow::Result;
use call::ActiveCall;
-use client::{
- proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore,
-};
-
+use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+use client::{proto::PeerId, Client, Contact, User, UserStore};
use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE;
use editor::{Cancel, Editor};
@@ -16,16 +14,18 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions,
elements::{
- Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState,
- MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg,
+ Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
+ MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
+ Stack, Svg,
},
+ fonts::TextStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
impl_actions,
platform::{CursorStyle, MouseButton, PromptLevel},
- serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle,
+ serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
@@ -35,7 +35,7 @@ use serde_derive::{Deserialize, Serialize};
use settings::SettingsStore;
use staff_mode::StaffMode;
use std::{borrow::Cow, mem, sync::Arc};
-use theme::IconButton;
+use theme::{components::ComponentExt, IconButton};
use util::{iife, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel},
@@ -43,7 +43,10 @@ use workspace::{
Workspace,
};
-use crate::face_pile::FacePile;
+use crate::{
+ channel_view::{self, ChannelView},
+ face_pile::FacePile,
+};
use channel_modal::ChannelModal;
use self::contact_finder::ContactFinder;
@@ -53,6 +56,11 @@ struct RemoveChannel {
channel_id: u64,
}
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct ToggleCollapse {
+ channel_id: u64,
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct NewChannel {
channel_id: u64,
@@ -73,7 +81,21 @@ struct RenameChannel {
channel_id: u64,
}
-actions!(collab_panel, [ToggleFocus, Remove, Secondary]);
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct OpenChannelBuffer {
+ channel_id: u64,
+}
+
+actions!(
+ collab_panel,
+ [
+ ToggleFocus,
+ Remove,
+ Secondary,
+ CollapseSelectedChannel,
+ ExpandSelectedChannel
+ ]
+);
impl_actions!(
collab_panel,
@@ -82,7 +104,9 @@ impl_actions!(
NewChannel,
InviteMembers,
ManageMembers,
- RenameChannel
+ RenameChannel,
+ ToggleCollapse,
+ OpenChannelBuffer
]
);
@@ -92,6 +116,7 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
contact_finder::init(cx);
channel_modal::init(cx);
+ channel_view::init(cx);
cx.add_action(CollabPanel::cancel);
cx.add_action(CollabPanel::select_next);
@@ -105,6 +130,10 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
cx.add_action(CollabPanel::manage_members);
cx.add_action(CollabPanel::rename_selected_channel);
cx.add_action(CollabPanel::rename_channel);
+ cx.add_action(CollabPanel::toggle_channel_collapsed);
+ cx.add_action(CollabPanel::collapse_selected_channel);
+ cx.add_action(CollabPanel::expand_selected_channel);
+ cx.add_action(CollabPanel::open_channel_buffer);
}
#[derive(Debug)]
@@ -147,6 +176,7 @@ pub struct CollabPanel {
list_state: ListState<Self>,
subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>,
+ collapsed_channels: Vec<ChannelId>,
workspace: WeakViewHandle<Workspace>,
context_menu_on_selected: bool,
}
@@ -154,6 +184,7 @@ pub struct CollabPanel {
#[derive(Serialize, Deserialize)]
struct SerializedChannelsPanel {
width: Option<f32>,
+ collapsed_channels: Vec<ChannelId>,
}
#[derive(Debug)]
@@ -198,6 +229,9 @@ enum ListEntry {
channel: Arc<Channel>,
depth: usize,
},
+ ChannelNotes {
+ channel_id: ChannelId,
+ },
ChannelEditor {
depth: usize,
},
@@ -341,6 +375,12 @@ impl CollabPanel {
return channel_row;
}
}
+ ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
+ *channel_id,
+ &theme.collab_panel,
+ is_selected,
+ cx,
+ ),
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
channel.clone(),
this.channel_store.clone(),
@@ -398,6 +438,7 @@ impl CollabPanel {
subscriptions: Vec::default(),
match_candidates: Vec::default(),
collapsed_sections: vec![Section::Offline],
+ collapsed_channels: Vec::default(),
workspace: workspace.weak_handle(),
client: workspace.app_state().client.clone(),
context_menu_on_selected: true,
@@ -479,6 +520,7 @@ impl CollabPanel {
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
+ panel.collapsed_channels = serialized_panel.collapsed_channels;
cx.notify();
});
}
@@ -489,12 +531,16 @@ impl CollabPanel {
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
let width = self.width;
+ let collapsed_channels = self.collapsed_channels.clone();
self.pending_serialization = cx.background().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
COLLABORATION_PANEL_KEY.into(),
- serde_json::to_string(&SerializedChannelsPanel { width })?,
+ serde_json::to_string(&SerializedChannelsPanel {
+ width,
+ collapsed_channels,
+ })?,
)
.await?;
anyhow::Ok(())
@@ -518,6 +564,10 @@ impl CollabPanel {
if !self.collapsed_sections.contains(&Section::ActiveCall) {
let room = room.read(cx);
+ if let Some(channel_id) = room.channel_id() {
+ self.entries.push(ListEntry::ChannelNotes { channel_id })
+ }
+
// Populate the active user.
if let Some(user) = user_store.current_user() {
self.match_candidates.clear();
@@ -657,10 +707,24 @@ impl CollabPanel {
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
}
}
+ let mut collapse_depth = None;
for mat in matches {
let (depth, channel) =
channel_store.channel_at_index(mat.candidate_id).unwrap();
+ if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+ collapse_depth = Some(depth);
+ } else if let Some(collapsed_depth) = collapse_depth {
+ if depth > collapsed_depth {
+ continue;
+ }
+ if self.is_channel_collapsed(channel.id) {
+ collapse_depth = Some(depth);
+ } else {
+ collapse_depth = None;
+ }
+ }
+
match &self.channel_editing_state {
Some(ChannelEditingState::Create { parent_id, .. })
if *parent_id == Some(channel.id) =>
@@ -963,25 +1027,19 @@ impl CollabPanel {
) -> AnyElement<Self> {
enum JoinProject {}
- let font_cache = cx.font_cache();
- let host_avatar_height = theme
+ let host_avatar_width = theme
.contact_avatar
.width
.or(theme.contact_avatar.height)
.unwrap_or(0.);
- let row = &theme.project_row.inactive_state().default;
let tree_branch = theme.tree_branch;
- let line_height = row.name.text.line_height(font_cache);
- let cap_height = row.name.text.cap_height(font_cache);
- let baseline_offset =
- row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
let project_name = if worktree_root_names.is_empty() {
"untitled".to_string()
} else {
worktree_root_names.join(", ")
};
- MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, _| {
+ MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
let row = theme
.project_row
@@ -989,39 +1047,20 @@ impl CollabPanel {
.style_for(mouse_state);
Flex::row()
+ .with_child(render_tree_branch(
+ tree_branch,
+ &row.name.text,
+ is_last,
+ vec2f(host_avatar_width, theme.row_height),
+ cx.font_cache(),
+ ))
.with_child(
- Stack::new()
- .with_child(Canvas::new(move |scene, bounds, _, _, _| {
- let start_x =
- bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
- let end_x = bounds.max_x();
- let start_y = bounds.min_y();
- let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
-
- scene.push_quad(gpui::Quad {
- bounds: RectF::from_points(
- vec2f(start_x, start_y),
- vec2f(
- start_x + tree_branch.width,
- if is_last { end_y } else { bounds.max_y() },
- ),
- ),
- background: Some(tree_branch.color),
- border: gpui::Border::default(),
- corner_radii: (0.).into(),
- });
- scene.push_quad(gpui::Quad {
- bounds: RectF::from_points(
- vec2f(start_x, end_y),
- vec2f(end_x, end_y + tree_branch.width),
- ),
- background: Some(tree_branch.color),
- border: gpui::Border::default(),
- corner_radii: (0.).into(),
- });
- }))
+ Svg::new("icons/file_icons/folder.svg")
+ .with_color(theme.channel_hash.color)
.constrained()
- .with_width(host_avatar_height),
+ .with_width(theme.channel_hash.width)
+ .aligned()
+ .left(),
)
.with_child(
Label::new(project_name, row.name.text.clone())
@@ -1196,7 +1235,7 @@ impl CollabPanel {
});
if let Some(name) = channel_name {
- Cow::Owned(format!("Current Call - #{}", name))
+ Cow::Owned(format!("#{}", name))
} else {
Cow::Borrowed("Current Call")
}
@@ -1332,7 +1371,7 @@ impl CollabPanel {
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if can_collapse {
- this.toggle_expanded(section, cx);
+ this.toggle_section_expanded(section, cx);
}
})
}
@@ -1479,6 +1518,11 @@ impl CollabPanel {
cx: &AppContext,
) -> AnyElement<Self> {
Flex::row()
+ .with_child(
+ Empty::new()
+ .constrained()
+ .with_width(theme.collab_panel.disclosure.button_space()),
+ )
.with_child(
Svg::new("icons/hash.svg")
.with_color(theme.collab_panel.channel_hash.color)
@@ -1537,6 +1581,10 @@ impl CollabPanel {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let channel_id = channel.id;
+ let has_children = self.channel_store.read(cx).has_children(channel_id);
+ let disclosed =
+ has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
+
let is_active = iife!({
let call_channel = ActiveCall::global(cx)
.read(cx)
@@ -1550,7 +1598,7 @@ impl CollabPanel {
const FACEPILE_LIMIT: usize = 3;
MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
- Flex::row()
+ Flex::<Self>::row()
.with_child(
Svg::new("icons/hash.svg")
.with_color(theme.channel_hash.color)
@@ -1599,6 +1647,11 @@ impl CollabPanel {
}
})
.align_children_center()
+ .styleable_component()
+ .disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
+ .with_id(channel_id as usize)
+ .with_style(theme.disclosure.clone())
+ .element()
.constrained()
.with_height(theme.row_height)
.contained()
@@ -1618,6 +1671,61 @@ impl CollabPanel {
.into_any()
}
+ fn render_channel_notes(
+ &self,
+ channel_id: ChannelId,
+ theme: &theme::CollabPanel,
+ is_selected: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> AnyElement<Self> {
+ enum ChannelNotes {}
+ let host_avatar_width = theme
+ .contact_avatar
+ .width
+ .or(theme.contact_avatar.height)
+ .unwrap_or(0.);
+
+ MouseEventHandler::new::<ChannelNotes, _>(channel_id as usize, cx, |state, cx| {
+ let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+ let row = theme.project_row.in_state(is_selected).style_for(state);
+
+ Flex::<Self>::row()
+ .with_child(render_tree_branch(
+ tree_branch,
+ &row.name.text,
+ true,
+ vec2f(host_avatar_width, theme.row_height),
+ cx.font_cache(),
+ ))
+ .with_child(
+ Svg::new("icons/radix/file.svg")
+ .with_color(theme.channel_hash.color)
+ .constrained()
+ .with_width(theme.channel_hash.width)
+ .aligned()
+ .left(),
+ )
+ .with_child(
+ Label::new("notes", theme.channel_name.text.clone())
+ .contained()
+ .with_style(theme.channel_name.container)
+ .aligned()
+ .left()
+ .flex(1., true),
+ )
+ .constrained()
+ .with_height(theme.row_height)
+ .contained()
+ .with_style(*theme.channel_row.style_for(is_selected, state))
+ .with_padding_left(theme.channel_row.default_style().padding.left)
+ })
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx);
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .into_any()
+ }
+
fn render_channel_invite(
channel: Arc<Channel>,
channel_store: ModelHandle<ChannelStore>,
@@ -1815,39 +1923,52 @@ impl CollabPanel {
channel_id: u64,
cx: &mut ViewContext<Self>,
) {
- if self.channel_store.read(cx).is_user_admin(channel_id) {
- self.context_menu_on_selected = position.is_none();
-
- self.context_menu.update(cx, |context_menu, cx| {
- context_menu.set_position_mode(if self.context_menu_on_selected {
- OverlayPositionMode::Local
- } else {
- OverlayPositionMode::Window
- });
+ self.context_menu_on_selected = position.is_none();
- context_menu.show(
- position.unwrap_or_default(),
- if self.context_menu_on_selected {
- gpui::elements::AnchorCorner::TopRight
- } else {
- gpui::elements::AnchorCorner::BottomLeft
- },
- vec![
- ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
- ContextMenuItem::Separator,
- ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }),
- ContextMenuItem::Separator,
- ContextMenuItem::action("Rename", RenameChannel { channel_id }),
- ContextMenuItem::action("Manage", ManageMembers { channel_id }),
- ContextMenuItem::Separator,
- ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
- ],
- cx,
- );
+ self.context_menu.update(cx, |context_menu, cx| {
+ context_menu.set_position_mode(if self.context_menu_on_selected {
+ OverlayPositionMode::Local
+ } else {
+ OverlayPositionMode::Window
});
- cx.notify();
- }
+ let expand_action_name = if self.is_channel_collapsed(channel_id) {
+ "Expand Subchannels"
+ } else {
+ "Collapse Subchannels"
+ };
+
+ let mut items = vec![
+ ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
+ ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
+ ];
+
+ if self.channel_store.read(cx).is_user_admin(channel_id) {
+ items.extend([
+ ContextMenuItem::Separator,
+ ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
+ ContextMenuItem::action("Rename", RenameChannel { channel_id }),
+ ContextMenuItem::Separator,
+ ContextMenuItem::action("Invite Members", InviteMembers { channel_id }),
+ ContextMenuItem::action("Manage Members", ManageMembers { channel_id }),
+ ContextMenuItem::Separator,
+ ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
+ ]);
+ }
+
+ context_menu.show(
+ position.unwrap_or_default(),
+ if self.context_menu_on_selected {
+ gpui::elements::AnchorCorner::TopRight
+ } else {
+ gpui::elements::AnchorCorner::BottomLeft
+ },
+ items,
+ cx,
+ );
+ });
+
+ cx.notify();
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
@@ -1912,7 +2033,7 @@ impl CollabPanel {
| Section::Online
| Section::Offline
| Section::ChannelInvites => {
- self.toggle_expanded(*section, cx);
+ self.toggle_section_expanded(*section, cx);
}
},
ListEntry::Contact { contact, calling } => {
@@ -2000,7 +2121,7 @@ impl CollabPanel {
}
}
- fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
+ fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
self.collapsed_sections.remove(ix);
} else {
@@ -2009,6 +2130,55 @@ impl CollabPanel {
self.update_entries(false, cx);
}
+ fn collapse_selected_channel(
+ &mut self,
+ _: &CollapseSelectedChannel,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+ return;
+ };
+
+ if self.is_channel_collapsed(channel_id) {
+ return;
+ }
+
+ self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+ }
+
+ fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
+ let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+ return;
+ };
+
+ if !self.is_channel_collapsed(channel_id) {
+ return;
+ }
+
+ self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+ }
+
+ fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
+ let channel_id = action.channel_id;
+
+ match self.collapsed_channels.binary_search(&channel_id) {
+ Ok(ix) => {
+ self.collapsed_channels.remove(ix);
+ }
+ Err(ix) => {
+ self.collapsed_channels.insert(ix, channel_id);
+ }
+ };
+ self.serialize(cx);
+ self.update_entries(true, cx);
+ cx.notify();
+ cx.focus_self();
+ }
+
+ fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
+ self.collapsed_channels.binary_search(&channel).is_ok()
+ }
+
fn leave_call(cx: &mut ViewContext<Self>) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
@@ -2048,6 +2218,8 @@ impl CollabPanel {
}
fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
+ self.collapsed_channels
+ .retain(|&channel| channel != action.channel_id);
self.channel_editing_state = Some(ChannelEditingState::Create {
parent_id: Some(action.channel_id),
pending_name: None,
@@ -2103,6 +2275,21 @@ impl CollabPanel {
}
}
+ fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext<Self>) {
+ if let Some(workspace) = self.workspace.upgrade(cx) {
+ let pane = workspace.read(cx).active_pane().clone();
+ let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx);
+ cx.spawn(|_, mut cx| async move {
+ let channel_view = channel_view.await?;
+ pane.update(&mut cx, |pane, cx| {
+ pane.add_item(Box::new(channel_view), true, true, None, cx)
+ });
+ anyhow::Ok(())
+ })
+ .detach();
+ }
+ }
+
fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
let Some(channel) = self.selected_channel() else {
return;
@@ -2261,6 +2448,51 @@ impl CollabPanel {
}
}
+fn render_tree_branch(
+ branch_style: theme::TreeBranch,
+ row_style: &TextStyle,
+ is_last: bool,
+ size: Vector2F,
+ font_cache: &FontCache,
+) -> gpui::elements::ConstrainedBox<CollabPanel> {
+ let line_height = row_style.line_height(font_cache);
+ let cap_height = row_style.cap_height(font_cache);
+ let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
+
+ Canvas::new(move |scene, bounds, _, _, _| {
+ scene.paint_layer(None, |scene| {
+ let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
+ let end_x = bounds.max_x();
+ let start_y = bounds.min_y();
+ let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
+
+ scene.push_quad(gpui::Quad {
+ bounds: RectF::from_points(
+ vec2f(start_x, start_y),
+ vec2f(
+ start_x + branch_style.width,
+ if is_last { end_y } else { bounds.max_y() },
+ ),
+ ),
+ background: Some(branch_style.color),
+ border: gpui::Border::default(),
+ corner_radii: (0.).into(),
+ });
+ scene.push_quad(gpui::Quad {
+ bounds: RectF::from_points(
+ vec2f(start_x, end_y),
+ vec2f(end_x, end_y + branch_style.width),
+ ),
+ background: Some(branch_style.color),
+ border: gpui::Border::default(),
+ corner_radii: (0.).into(),
+ });
+ })
+ })
+ .constrained()
+ .with_width(size.x())
+}
+
impl View for CollabPanel {
fn ui_name() -> &'static str {
"CollabPanel"
@@ -2470,6 +2702,14 @@ impl PartialEq for ListEntry {
return channel_1.id == channel_2.id && depth_1 == depth_2;
}
}
+ ListEntry::ChannelNotes { channel_id } => {
+ if let ListEntry::ChannelNotes {
+ channel_id: other_id,
+ } = other
+ {
+ return channel_id == other_id;
+ }
+ }
ListEntry::ChannelInvite(channel_1) => {
if let ListEntry::ChannelInvite(channel_2) = other {
return channel_1.id == channel_2.id;
@@ -1,4 +1,5 @@
-use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore};
+use channel::{ChannelId, ChannelMembership, ChannelStore};
+use client::{proto, User, UserId, UserStore};
use context_menu::{ContextMenu, ContextMenuItem};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
@@ -1096,7 +1096,7 @@ impl CollabTitlebarItem {
style
}
- fn render_face<V: View>(
+ fn render_face<V: 'static>(
avatar: Arc<ImageData>,
avatar_style: AvatarStyle,
background_color: Color,
@@ -1,3 +1,4 @@
+pub mod channel_view;
pub mod collab_panel;
mod collab_titlebar_item;
mod contact_notification;
@@ -2,14 +2,14 @@ use client::User;
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
- AnyElement, Element, View, ViewContext,
+ AnyElement, Element, ViewContext,
};
use std::sync::Arc;
enum Dismiss {}
enum Button {}
-pub fn render_user_notification<F, V>(
+pub fn render_user_notification<F, V: 'static>(
user: Arc<User>,
title: &'static str,
body: Option<&'static str>,
@@ -19,7 +19,6 @@ pub fn render_user_notification<F, V>(
) -> AnyElement<V>
where
F: 'static + Fn(&mut V, &mut ViewContext<V>),
- V: View,
{
let theme = theme::current(cx).clone();
let theme = &theme.contact_notification;
@@ -0,0 +1,18 @@
+[package]
+name = "component_test"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/component_test.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+gpui = { path = "../gpui" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+project = { path = "../project" }
@@ -0,0 +1,121 @@
+use gpui::{
+ actions,
+ elements::{Component, Flex, ParentElement, SafeStylable},
+ AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use project::Project;
+use theme::components::{action_button::Button, label::Label, ComponentExt};
+use workspace::{
+ item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId,
+};
+
+pub fn init(cx: &mut AppContext) {
+ cx.add_action(ComponentTest::toggle_disclosure);
+ cx.add_action(ComponentTest::toggle_toggle);
+ cx.add_action(ComponentTest::deploy);
+ register_deserializable_item::<ComponentTest>(cx);
+}
+
+actions!(
+ test,
+ [NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest]
+);
+
+struct ComponentTest {
+ disclosed: bool,
+ toggled: bool,
+}
+
+impl ComponentTest {
+ fn new() -> Self {
+ Self {
+ disclosed: false,
+ toggled: false,
+ }
+ }
+
+ fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext<Workspace>) {
+ workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx);
+ }
+
+ fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext<Self>) {
+ self.disclosed = !self.disclosed;
+ cx.notify();
+ }
+
+ fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext<Self>) {
+ self.toggled = !self.toggled;
+ cx.notify();
+ }
+}
+
+impl Entity for ComponentTest {
+ type Event = ();
+}
+
+impl View for ComponentTest {
+ fn ui_name() -> &'static str {
+ "Component Test"
+ }
+
+ fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
+ let theme = theme::current(cx);
+
+ PaneBackdrop::new(
+ cx.view_id(),
+ Flex::column()
+ .with_spacing(10.)
+ .with_child(
+ Button::action(NoAction)
+ .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
+ .with_contents(Label::new("Click me!"))
+ .with_style(theme.component_test.button.clone())
+ .element(),
+ )
+ .with_child(
+ Button::action(ToggleToggle)
+ .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
+ .with_contents(Label::new("Toggle me!"))
+ .toggleable(self.toggled)
+ .with_style(theme.component_test.toggle.clone())
+ .element(),
+ )
+ .with_child(
+ Label::new("A disclosure")
+ .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure))
+ .with_style(theme.component_test.disclosure.clone())
+ .element(),
+ )
+ .constrained()
+ .with_width(200.)
+ .aligned()
+ .into_any(),
+ )
+ .into_any()
+ }
+}
+
+impl Item for ComponentTest {
+ fn tab_content<V: 'static>(
+ &self,
+ _: Option<usize>,
+ style: &theme::Tab,
+ _: &AppContext,
+ ) -> gpui::AnyElement<V> {
+ gpui::elements::Label::new("Component test", style.label.clone()).into_any()
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("ComponentTest")
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ _workspace_id: WorkspaceId,
+ _item_id: ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+ Task::ready(Ok(cx.add_view(|_| Self::new())))
+ }
+}
@@ -538,7 +538,7 @@ impl ProjectDiagnosticsEditor {
}
impl Item for ProjectDiagnosticsEditor {
- fn tab_content<T: View>(
+ fn tab_content<T: 'static>(
&self,
_detail: Option<usize>,
style: &theme::Tab,
@@ -735,7 +735,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
})
}
-pub(crate) fn render_summary<T: View>(
+pub(crate) fn render_summary<T: 'static>(
summary: &DiagnosticSummary,
text_style: &TextStyle,
theme: &theme::ProjectDiagnostics,
@@ -11,7 +11,7 @@ use gpui::{
const DEAD_ZONE: f32 = 4.;
-enum State<V: View> {
+enum State<V> {
Down {
region_offset: Vector2F,
region: RectF,
@@ -31,7 +31,7 @@ enum State<V: View> {
Canceled,
}
-impl<V: View> Clone for State<V> {
+impl<V> Clone for State<V> {
fn clone(&self) -> Self {
match self {
&State::Down {
@@ -68,12 +68,12 @@ impl<V: View> Clone for State<V> {
}
}
-pub struct DragAndDrop<V: View> {
+pub struct DragAndDrop<V> {
containers: HashSet<WeakViewHandle<V>>,
currently_dragged: Option<State<V>>,
}
-impl<V: View> Default for DragAndDrop<V> {
+impl<V> Default for DragAndDrop<V> {
fn default() -> Self {
Self {
containers: Default::default(),
@@ -82,7 +82,7 @@ impl<V: View> Default for DragAndDrop<V> {
}
}
-impl<V: View> DragAndDrop<V> {
+impl<V: 'static> DragAndDrop<V> {
pub fn register_container(&mut self, handle: WeakViewHandle<V>) {
self.containers.insert(handle);
}
@@ -291,7 +291,7 @@ impl<V: View> DragAndDrop<V> {
}
}
-pub trait Draggable<V: View> {
+pub trait Draggable<V> {
fn as_draggable<D: View, P: Any>(
self,
payload: P,
@@ -301,7 +301,7 @@ pub trait Draggable<V: View> {
Self: Sized;
}
-impl<V: View> Draggable<V> for MouseEventHandler<V> {
+impl<V: 'static> Draggable<V> for MouseEventHandler<V> {
fn as_draggable<D: View, P: Any>(
self,
payload: P,
@@ -559,6 +559,7 @@ pub struct Editor {
blink_manager: ModelHandle<BlinkManager>,
show_local_selections: bool,
mode: EditorMode,
+ replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
show_gutter: bool,
show_wrap_guides: Option<bool>,
placeholder_text: Option<Arc<str>>,
@@ -1394,6 +1395,7 @@ impl Editor {
blink_manager: blink_manager.clone(),
show_local_selections: true,
mode,
+ replica_id_mapping: None,
show_gutter: mode == EditorMode::Full,
show_wrap_guides: None,
placeholder_text: None,
@@ -1604,6 +1606,19 @@ impl Editor {
self.read_only = read_only;
}
+ pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
+ self.replica_id_mapping.as_ref()
+ }
+
+ pub fn set_replica_id_map(
+ &mut self,
+ mapping: Option<HashMap<ReplicaId, ReplicaId>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.replica_id_mapping = mapping;
+ cx.notify();
+ }
+
fn selections_did_change(
&mut self,
local: bool,
@@ -1736,6 +1751,31 @@ impl Editor {
});
}
+ pub fn edit_with_block_indent<I, S, T>(
+ &mut self,
+ edits: I,
+ original_indent_columns: Vec<u32>,
+ cx: &mut ViewContext<Self>,
+ ) where
+ I: IntoIterator<Item = (Range<S>, T)>,
+ S: ToOffset,
+ T: Into<Arc<str>>,
+ {
+ if self.read_only {
+ return;
+ }
+
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ edits,
+ Some(AutoindentMode::Block {
+ original_indent_columns,
+ }),
+ cx,
+ )
+ });
+ }
+
fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext<Self>) {
self.hide_context_menu(cx);
@@ -2667,7 +2707,6 @@ impl Editor {
false
});
}
-
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
let offset = position.to_offset(buffer);
let (word_range, kind) = buffer.surrounding_word(offset);
@@ -4742,6 +4781,7 @@ impl Editor {
let mut clipboard_selections = Vec::with_capacity(selections.len());
{
let max_point = buffer.max_point();
+ let mut is_first = true;
for selection in &mut selections {
let is_entire_line = selection.is_empty() || self.selections.line_mode;
if is_entire_line {
@@ -4749,6 +4789,11 @@ impl Editor {
selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
selection.goal = SelectionGoal::None;
}
+ if is_first {
+ is_first = false;
+ } else {
+ text += "\n";
+ }
let mut len = 0;
for chunk in buffer.text_for_range(selection.start..selection.end) {
text.push_str(chunk);
@@ -4779,6 +4824,7 @@ impl Editor {
let mut clipboard_selections = Vec::with_capacity(selections.len());
{
let max_point = buffer.max_point();
+ let mut is_first = true;
for selection in selections.iter() {
let mut start = selection.start;
let mut end = selection.end;
@@ -4787,6 +4833,11 @@ impl Editor {
start = Point::new(start.row, 0);
end = cmp::min(max_point, Point::new(end.row + 1, 0));
}
+ if is_first {
+ is_first = false;
+ } else {
+ text += "\n";
+ }
let mut len = 0;
for chunk in buffer.text_for_range(start..end) {
text.push_str(chunk);
@@ -4806,7 +4857,7 @@ impl Editor {
pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
if let Some(item) = cx.read_from_clipboard() {
- let mut clipboard_text = Cow::Borrowed(item.text());
+ let clipboard_text = Cow::Borrowed(item.text());
if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
let old_selections = this.selections.all::<usize>(cx);
let all_selections_were_entire_line =
@@ -4814,18 +4865,7 @@ impl Editor {
let first_selection_indent_column =
clipboard_selections.first().map(|s| s.first_line_indent);
if clipboard_selections.len() != old_selections.len() {
- let mut newline_separated_text = String::new();
- let mut clipboard_selections = clipboard_selections.drain(..).peekable();
- let mut ix = 0;
- while let Some(clipboard_selection) = clipboard_selections.next() {
- newline_separated_text
- .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
- ix += clipboard_selection.len;
- if clipboard_selections.peek().is_some() {
- newline_separated_text.push('\n');
- }
- }
- clipboard_text = Cow::Owned(newline_separated_text);
+ clipboard_selections.drain(..);
}
this.buffer.update(cx, |buffer, cx| {
@@ -4841,8 +4881,9 @@ impl Editor {
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
let end_offset = start_offset + clipboard_selection.len;
to_insert = &clipboard_text[start_offset..end_offset];
+ dbg!(start_offset, end_offset, &clipboard_text, &to_insert);
entire_line = clipboard_selection.is_entire_line;
- start_offset = end_offset;
+ start_offset = end_offset + 1;
original_indent_column =
Some(clipboard_selection.first_line_indent);
} else {
@@ -8537,6 +8578,7 @@ fn build_style(
font_size,
font_properties,
underline: Default::default(),
+ soft_wrap: false,
},
placeholder_text: None,
line_height_scalar,
@@ -6384,7 +6384,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
.update(|cx| {
Editor::from_state_proto(
pane.clone(),
- project.clone(),
+ workspace.clone(),
ViewId {
creator: Default::default(),
id: 0,
@@ -6479,7 +6479,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
.update(|cx| {
Editor::from_state_proto(
pane.clone(),
- project.clone(),
+ workspace.clone(),
ViewId {
creator: Default::default(),
id: 0,
@@ -62,6 +62,7 @@ struct SelectionLayout {
head: DisplayPoint,
cursor_shape: CursorShape,
is_newest: bool,
+ is_local: bool,
range: Range<DisplayPoint>,
active_rows: Range<u32>,
}
@@ -73,6 +74,7 @@ impl SelectionLayout {
cursor_shape: CursorShape,
map: &DisplaySnapshot,
is_newest: bool,
+ is_local: bool,
) -> Self {
let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
let display_selection = point_selection.map(|p| p.to_display_point(map));
@@ -109,6 +111,7 @@ impl SelectionLayout {
head,
cursor_shape,
is_newest,
+ is_local,
range,
active_rows,
}
@@ -605,7 +608,7 @@ impl EditorElement {
visible_bounds: RectF,
layout: &mut LayoutState,
editor: &mut Editor,
- cx: &mut ViewContext<Editor>,
+ cx: &mut PaintContext<Editor>,
) {
let line_height = layout.position_map.line_height;
@@ -760,10 +763,9 @@ impl EditorElement {
visible_bounds: RectF,
layout: &mut LayoutState,
editor: &mut Editor,
- cx: &mut ViewContext<Editor>,
+ cx: &mut PaintContext<Editor>,
) {
let style = &self.style;
- let local_replica_id = editor.replica_id(cx);
let scroll_position = layout.position_map.snapshot.scroll_position();
let start_row = layout.visible_display_row_range.start;
let scroll_top = scroll_position.y() * layout.position_map.line_height;
@@ -852,15 +854,13 @@ impl EditorElement {
for (replica_id, selections) in &layout.selections {
let replica_id = *replica_id;
- let selection_style = style.replica_selection_style(replica_id);
+ let selection_style = if let Some(replica_id) = replica_id {
+ style.replica_selection_style(replica_id)
+ } else {
+ &style.absent_selection
+ };
for selection in selections {
- if !selection.range.is_empty()
- && (replica_id == local_replica_id
- || Some(replica_id) == editor.leader_replica_id)
- {
- invisible_display_ranges.push(selection.range.clone());
- }
self.paint_highlighted_range(
scene,
selection.range.clone(),
@@ -874,7 +874,10 @@ impl EditorElement {
bounds,
);
- if editor.show_local_cursors(cx) || replica_id != local_replica_id {
+ if selection.is_local && !selection.range.is_empty() {
+ invisible_display_ranges.push(selection.range.clone());
+ }
+ if !selection.is_local || editor.show_local_cursors(cx) {
let cursor_position = selection.head;
if layout
.visible_display_row_range
@@ -1337,7 +1340,7 @@ impl EditorElement {
visible_bounds: RectF,
layout: &mut LayoutState,
editor: &mut Editor,
- cx: &mut ViewContext<Editor>,
+ cx: &mut PaintContext<Editor>,
) {
let scroll_position = layout.position_map.snapshot.scroll_position();
let scroll_left = scroll_position.x() * layout.position_map.em_width;
@@ -2124,7 +2127,7 @@ impl Element<Editor> for EditorElement {
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
};
- let mut selections: Vec<(ReplicaId, Vec<SelectionLayout>)> = Vec::new();
+ let mut selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)> = Vec::new();
let mut active_rows = BTreeMap::new();
let mut fold_ranges = Vec::new();
let is_singleton = editor.is_singleton(cx);
@@ -2155,8 +2158,14 @@ impl Element<Editor> for EditorElement {
.buffer_snapshot
.remote_selections_in_range(&(start_anchor..end_anchor))
{
+ let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
+ mapping.get(&replica_id).copied()
+ } else {
+ None
+ };
+
// The local selections match the leader's selections.
- if Some(replica_id) == editor.leader_replica_id {
+ if replica_id.is_some() && replica_id == editor.leader_replica_id {
continue;
}
remote_selections
@@ -2168,6 +2177,7 @@ impl Element<Editor> for EditorElement {
cursor_shape,
&snapshot.display_snapshot,
false,
+ false,
));
}
selections.extend(remote_selections);
@@ -2191,6 +2201,7 @@ impl Element<Editor> for EditorElement {
editor.cursor_shape,
&snapshot.display_snapshot,
is_newest,
+ true,
);
if is_newest {
newest_selection_head = Some(layout.head);
@@ -2206,11 +2217,18 @@ impl Element<Editor> for EditorElement {
}
// Render the local selections in the leader's color when following.
- let local_replica_id = editor
- .leader_replica_id
- .unwrap_or_else(|| editor.replica_id(cx));
+ let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id {
+ leader_replica_id
+ } else {
+ let replica_id = editor.replica_id(cx);
+ if let Some(mapping) = &editor.replica_id_mapping {
+ mapping.get(&replica_id).copied().unwrap_or(replica_id)
+ } else {
+ replica_id
+ }
+ };
- selections.push((local_replica_id, layouts));
+ selections.push((Some(local_replica_id), layouts));
}
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
@@ -2591,7 +2609,7 @@ pub struct LayoutState {
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
- selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
+ selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)>,
scrollbar_row_range: Range<f32>,
show_scrollbars: bool,
is_singleton: bool,
@@ -49,11 +49,12 @@ impl FollowableItem for Editor {
fn from_state_proto(
pane: ViewHandle<workspace::Pane>,
- project: ModelHandle<Project>,
+ workspace: ViewHandle<Workspace>,
remote_id: ViewId,
state: &mut Option<proto::view::Variant>,
cx: &mut AppContext,
) -> Option<Task<Result<ViewHandle<Self>>>> {
+ let project = workspace.read(cx).project().to_owned();
let Some(proto::view::Variant::Editor(_)) = state else { return None };
let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
@@ -561,7 +562,7 @@ impl Item for Editor {
}
}
- fn tab_content<T: View>(
+ fn tab_content<T: 'static>(
&self,
detail: Option<usize>,
style: &theme::Tab,
@@ -753,7 +754,7 @@ impl Item for Editor {
Some(Box::new(handle.clone()))
}
- fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
+ fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
self.pixel_position_of_newest_cursor
}
@@ -1028,7 +1029,7 @@ impl SearchableItem for Editor {
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
ranges.extend(
query
- .search(excerpt_buffer.as_rope())
+ .search(excerpt_buffer, None)
.await
.into_iter()
.map(|range| {
@@ -1038,17 +1039,22 @@ impl SearchableItem for Editor {
} else {
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
- let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
- ranges.extend(query.search(&rope).await.into_iter().map(|range| {
- let start = excerpt
- .buffer
- .anchor_after(excerpt_range.start + range.start);
- let end = excerpt
- .buffer
- .anchor_before(excerpt_range.start + range.end);
- buffer.anchor_in_excerpt(excerpt.id.clone(), start)
- ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
- }));
+ ranges.extend(
+ query
+ .search(&excerpt.buffer, Some(excerpt_range.clone()))
+ .await
+ .into_iter()
+ .map(|range| {
+ let start = excerpt
+ .buffer
+ .anchor_after(excerpt_range.start + range.start);
+ let end = excerpt
+ .buffer
+ .anchor_before(excerpt_range.start + range.end);
+ buffer.anchor_in_excerpt(excerpt.id.clone(), start)
+ ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
+ }),
+ );
}
}
ranges
@@ -176,14 +176,21 @@ pub fn line_end(
}
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let raw_point = point.to_point(map);
+ let language = map.buffer_snapshot.language_at(raw_point);
+
find_preceding_boundary(map, point, |left, right| {
- (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
+ (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace())
+ || left == '\n'
})
}
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let raw_point = point.to_point(map);
+ let language = map.buffer_snapshot.language_at(raw_point);
find_preceding_boundary(map, point, |left, right| {
- let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
+ let is_word_start =
+ char_kind(language, left) != char_kind(language, right) && !right.is_whitespace();
let is_subword_start =
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start || left == '\n'
@@ -191,14 +198,20 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
}
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let raw_point = point.to_point(map);
+ let language = map.buffer_snapshot.language_at(raw_point);
find_boundary(map, point, |left, right| {
- (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
+ (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace())
+ || right == '\n'
})
}
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let raw_point = point.to_point(map);
+ let language = map.buffer_snapshot.language_at(raw_point);
find_boundary(map, point, |left, right| {
- let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
+ let is_word_end =
+ (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace();
let is_subword_end =
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end || right == '\n'
@@ -385,10 +398,15 @@ pub fn find_boundary_in_line(
}
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
+ let raw_point = point.to_point(map);
+ let language = map.buffer_snapshot.language_at(raw_point);
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
let text = &map.buffer_snapshot;
- let next_char_kind = text.chars_at(ix).next().map(char_kind);
- let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
+ let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c));
+ let prev_char_kind = text
+ .reversed_chars_at(ix)
+ .next()
+ .map(|c| char_kind(language, c));
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
}
@@ -1346,10 +1346,7 @@ impl MultiBuffer {
.map(|state| state.buffer.clone())
}
- pub fn is_completion_trigger<T>(&self, position: T, text: &str, cx: &AppContext) -> bool
- where
- T: ToOffset,
- {
+ pub fn is_completion_trigger(&self, position: Anchor, text: &str, cx: &AppContext) -> bool {
let mut chars = text.chars();
let char = if let Some(char) = chars.next() {
char
@@ -1360,7 +1357,9 @@ impl MultiBuffer {
return false;
}
- if char.is_alphanumeric() || char == '_' {
+ let language = self.language_at(position.clone(), cx);
+
+ if char_kind(language.as_ref(), char) == CharKind::Word {
return true;
}
@@ -1865,13 +1864,16 @@ impl MultiBufferSnapshot {
let mut end = start;
let mut next_chars = self.chars_at(start).peekable();
let mut prev_chars = self.reversed_chars_at(start).peekable();
+
+ let language = self.language_at(start);
+ let kind = |c| char_kind(language, c);
let word_kind = cmp::max(
- prev_chars.peek().copied().map(char_kind),
- next_chars.peek().copied().map(char_kind),
+ prev_chars.peek().copied().map(kind),
+ next_chars.peek().copied().map(kind),
);
for ch in prev_chars {
- if Some(char_kind(ch)) == word_kind && ch != '\n' {
+ if Some(kind(ch)) == word_kind && ch != '\n' {
start -= ch.len_utf8();
} else {
break;
@@ -1879,7 +1881,7 @@ impl MultiBufferSnapshot {
}
for ch in next_chars {
- if Some(char_kind(ch)) == word_kind && ch != '\n' {
+ if Some(kind(ch)) == word_kind && ch != '\n' {
end += ch.len_utf8();
} else {
break;
@@ -6,6 +6,7 @@ use std::{
use anyhow::Result;
+use collections::HashSet;
use futures::Future;
use gpui::{json, ViewContext, ViewHandle};
use indoc::indoc;
@@ -154,10 +155,23 @@ impl<'a> EditorLspTestContext<'a> {
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
+ let mut word_characters: HashSet<char> = Default::default();
+ word_characters.insert('$');
+ word_characters.insert('#');
let language = Language::new(
LanguageConfig {
name: "Typescript".into(),
path_suffixes: vec!["ts".to_string()],
+ brackets: language::BracketPairConfig {
+ pairs: vec![language::BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: true,
+ newline: true,
+ }],
+ disabled_scopes_by_bracket_ix: Default::default(),
+ },
+ word_characters,
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
@@ -169,6 +183,23 @@ impl<'a> EditorLspTestContext<'a> {
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)"#})),
+ indents: Some(Cow::from(indoc! {r#"
+ [
+ (call_expression)
+ (assignment_expression)
+ (member_expression)
+ (lexical_declaration)
+ (variable_declaration)
+ (assignment_expression)
+ (if_statement)
+ (for_statement)
+ ] @indent
+
+ (_ "[" "]" @end) @indent
+ (_ "<" ">" @end) @indent
+ (_ "{" "}" @end) @indent
+ (_ "(" ")" @end) @indent
+ "#})),
..Default::default()
})
.expect("Could not parse queries");
@@ -268,7 +268,7 @@ impl Item for FeedbackEditor {
Some("Send Feedback".into())
}
- fn tab_content<T: View>(
+ fn tab_content<T: 'static>(
&self,
_: Option<usize>,
style: &theme::Tab,
@@ -39,6 +39,7 @@ pathfinder_color = "0.5"
pathfinder_geometry = "0.5"
postage.workspace = true
rand.workspace = true
+refineable.workspace = true
resvg = "0.14"
schemars = "0.8"
seahash = "4.1"
@@ -47,6 +48,7 @@ serde_derive.workspace = true
serde_json.workspace = true
smallvec.workspace = true
smol.workspace = true
+taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] }
time.workspace = true
tiny-skia = "0.5"
usvg = { version = "0.14", features = [] }
@@ -2,7 +2,7 @@ use button_component::Button;
use gpui::{
color::Color,
- elements::{Component, ContainerStyle, Flex, Label, ParentElement},
+ elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent},
fonts::{self, TextStyle},
platform::WindowOptions,
AnyElement, App, Element, Entity, View, ViewContext,
@@ -114,7 +114,7 @@ mod theme {
// Component creation:
mod toggleable_button {
use gpui::{
- elements::{Component, ContainerStyle, LabelStyle},
+ elements::{ContainerStyle, LabelStyle, StatefulComponent},
scene::MouseClick,
EventContext, View,
};
@@ -156,7 +156,7 @@ mod toggleable_button {
}
}
- impl<V: View> Component<V> for ToggleableButton<V> {
+ impl<V: View> StatefulComponent<V> for ToggleableButton<V> {
fn render(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
let button = if let Some(style) = self.style {
self.button.with_style(*style.style_for(self.active))
@@ -171,7 +171,7 @@ mod toggleable_button {
mod button_component {
use gpui::{
- elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler},
+ elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent},
platform::MouseButton,
scene::MouseClick,
AnyElement, Element, EventContext, TypeTag, View, ViewContext,
@@ -212,7 +212,7 @@ mod button_component {
}
}
- impl<V: View> Component<V> for Button<V> {
+ impl<V: View> StatefulComponent<V> for Button<V> {
fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
let click_handler = self.click_handler;
@@ -58,6 +58,7 @@ impl gpui::View for TextView {
font_family_id: family,
underline: Default::default(),
font_properties: Default::default(),
+ soft_wrap: false,
},
)
.with_highlights(vec![(17..26, underline), (34..40, underline)])
@@ -0,0 +1,2919 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
+name = "aho-corasick"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+
+[[package]]
+name = "arrayref"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb"
+dependencies = [
+ "async-lock",
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite",
+ "log",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "socket2",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-net"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f"
+dependencies = [
+ "async-io",
+ "autocfg",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "autocfg",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+ "signal-hook",
+ "windows-sys",
+]
+
+[[package]]
+name = "async-task"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae"
+
+[[package]]
+name = "atomic"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide 0.7.1",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "bindgen"
+version = "0.65.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 2.0.25",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65"
+dependencies = [
+ "async-channel",
+ "async-lock",
+ "async-task",
+ "atomic-waker",
+ "fastrand",
+ "futures-lite",
+ "log",
+]
+
+[[package]]
+name = "bstr"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bytemuck"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+
+[[package]]
+name = "castaway"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clang-sys"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading 0.7.4",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cocoa"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
+dependencies = [
+ "bitflags",
+ "block",
+ "cocoa-foundation",
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6"
+dependencies = [
+ "bitflags",
+ "block",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "collections"
+version = "0.1.0"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-cstr"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+ "uuid 0.5.1",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
+name = "core-graphics"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "core-text"
+version = "19.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25"
+dependencies = [
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.63+curl-8.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "data-url"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "deflate"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
+dependencies = [
+ "adler32",
+ "byteorder",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dlib"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+dependencies = [
+ "libloading 0.8.0",
+]
+
+[[package]]
+name = "dwrote"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "winapi",
+ "wio",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "erased-serde"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "etagere"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644"
+dependencies = [
+ "euclid",
+ "svg_fmt",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide 0.7.1",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e"
+
+[[package]]
+name = "float-ord"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "font-kit"
+version = "0.11.0"
+source = "git+https://github.com/zed-industries/font-kit?rev=b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18#b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18"
+dependencies = [
+ "bitflags",
+ "byteorder",
+ "core-foundation",
+ "core-graphics",
+ "core-text",
+ "dirs-next",
+ "dwrote",
+ "float-ord",
+ "freetype",
+ "lazy_static",
+ "libc",
+ "log",
+ "pathfinder_geometry",
+ "pathfinder_simd",
+ "walkdir",
+ "winapi",
+ "yeslogic-fontconfig-sys",
+]
+
+[[package]]
+name = "fontdb"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e58903f4f8d5b58c7d300908e4ebe5289c1bfdf5587964330f12023b8ff17fd1"
+dependencies = [
+ "log",
+ "memmap2",
+ "ttf-parser 0.12.3",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "freetype"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6"
+dependencies = [
+ "freetype-sys",
+ "libc",
+]
+
+[[package]]
+name = "freetype-sys"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a"
+dependencies = [
+ "cmake",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gif"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "gimli"
+version = "0.27.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "globset"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "gpui"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-task",
+ "bindgen",
+ "block",
+ "cc",
+ "cocoa",
+ "collections",
+ "core-foundation",
+ "core-graphics",
+ "core-text",
+ "ctor",
+ "etagere",
+ "font-kit",
+ "foreign-types",
+ "futures",
+ "gpui_macros",
+ "image",
+ "itertools",
+ "lazy_static",
+ "log",
+ "media",
+ "metal",
+ "num_cpus",
+ "objc",
+ "ordered-float",
+ "parking",
+ "parking_lot 0.11.2",
+ "pathfinder_color",
+ "pathfinder_geometry",
+ "postage",
+ "rand",
+ "resvg",
+ "schemars",
+ "seahash",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smallvec",
+ "smol",
+ "sqlez",
+ "sum_tree",
+ "time",
+ "tiny-skia",
+ "usvg",
+ "util",
+ "uuid 1.4.0",
+ "waker-fn",
+]
+
+[[package]]
+name = "gpui_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+
+[[package]]
+name = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "idna"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "image"
+version = "0.23.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "gif",
+ "jpeg-decoder",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+ "png",
+ "scoped_threadpool",
+ "tiff",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "indoc"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "isahc"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
+dependencies = [
+ "async-channel",
+ "castaway",
+ "crossbeam-utils",
+ "curl",
+ "curl-sys",
+ "encoding_rs",
+ "event-listener",
+ "futures-lite",
+ "http",
+ "log",
+ "mime",
+ "once_cell",
+ "polling",
+ "slab",
+ "sluice",
+ "tracing",
+ "tracing-futures",
+ "url",
+ "waker-fn",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
+
+[[package]]
+name = "jpeg-decoder"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+dependencies = [
+ "rayon",
+]
+
+[[package]]
+name = "kurbo"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
+dependencies = [
+ "arrayvec 0.7.4",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libloading"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
+dependencies = [
+ "cfg-if",
+ "windows-sys",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+dependencies = [
+ "serde",
+ "value-bag",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "media"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bindgen",
+ "block",
+ "bytes",
+ "core-foundation",
+ "foreign-types",
+ "metal",
+ "objc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memmap2"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "metal"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4598d719460ade24c7d91f335daf055bf2a7eec030728ce751814c50cdd6a26c"
+dependencies = [
+ "bitflags",
+ "block",
+ "cocoa-foundation",
+ "foreign-types",
+ "log",
+ "objc",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
+dependencies = [
+ "adler32",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+ "objc_exception",
+]
+
+[[package]]
+name = "objc_exception"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "object"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "ordered-float"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "parking"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.8",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.3.5",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "pathfinder_color"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69bdc0d277d559e35e1b374de56df9262a6b71e091ca04a8831a239f8c7f0c62"
+dependencies = [
+ "pathfinder_simd",
+]
+
+[[package]]
+name = "pathfinder_geometry"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3"
+dependencies = [
+ "log",
+ "pathfinder_simd",
+]
+
+[[package]]
+name = "pathfinder_simd"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff"
+dependencies = [
+ "rustc_version",
+]
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[package]]
+name = "pest"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9"
+dependencies = [
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pico-args"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
+
+[[package]]
+name = "pin-project"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+
+[[package]]
+name = "playground"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+]
+
+[[package]]
+name = "png"
+version = "0.16.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "deflate",
+ "miniz_oxide 0.3.7",
+]
+
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite",
+ "windows-sys",
+]
+
+[[package]]
+name = "pollster"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
+
+[[package]]
+name = "postage"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1"
+dependencies = [
+ "atomic",
+ "crossbeam-queue",
+ "futures",
+ "log",
+ "parking_lot 0.12.1",
+ "pin-project",
+ "pollster",
+ "static_assertions",
+ "thiserror",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92139198957b410250d43fad93e630d956499a625c527eda65175c8680f83387"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rayon"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
+[[package]]
+name = "rctree"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be9e29cb19c8fe84169fcb07f8f11e66bc9e6e0280efd4715c54818296f8a4a8"
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall 0.2.16",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
+
+[[package]]
+name = "resvg"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09697862c5c3f940cbaffef91969c62188b5c8ed385b0aef43a5ff01ddc8000f"
+dependencies = [
+ "jpeg-decoder",
+ "log",
+ "pico-args",
+ "png",
+ "rgb",
+ "svgfilters",
+ "tiny-skia",
+ "usvg",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "roxmltree"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b"
+dependencies = [
+ "xmlparser",
+]
+
+[[package]]
+name = "rust-embed"
+version = "6.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "6.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.25",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "7.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
+dependencies = [
+ "globset",
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc_version"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustybuzz"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ab463a295d00f3692e0974a0bfd83c7a9bcd119e27e07c2beecdb1b44a09d10"
+dependencies = [
+ "bitflags",
+ "bytemuck",
+ "smallvec",
+ "ttf-parser 0.9.0",
+ "unicode-bidi-mirroring",
+ "unicode-ccc",
+ "unicode-general-category",
+ "unicode-script",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
+
+[[package]]
+name = "safe_arch"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "scoped_threadpool"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "serde_fmt"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
+
+[[package]]
+name = "signal-hook"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simplecss"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "sluice"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
+dependencies = [
+ "async-channel",
+ "futures-core",
+ "futures-io",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+
+[[package]]
+name = "smol"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "async-net",
+ "async-process",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "sqlez"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "futures",
+ "indoc",
+ "lazy_static",
+ "libsqlite3-sys",
+ "parking_lot 0.11.2",
+ "smol",
+ "thread_local",
+ "uuid 1.4.0",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "sum_tree"
+version = "0.1.0"
+dependencies = [
+ "arrayvec 0.7.4",
+ "log",
+]
+
+[[package]]
+name = "sval"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1"
+
+[[package]]
+name = "sval_buffer"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028"
+dependencies = [
+ "sval",
+ "sval_ref",
+]
+
+[[package]]
+name = "sval_dynamic"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf"
+dependencies = [
+ "sval",
+]
+
+[[package]]
+name = "sval_fmt"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326"
+dependencies = [
+ "itoa",
+ "ryu",
+ "sval",
+]
+
+[[package]]
+name = "sval_json"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d"
+dependencies = [
+ "itoa",
+ "ryu",
+ "sval",
+]
+
+[[package]]
+name = "sval_ref"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c"
+dependencies = [
+ "sval",
+]
+
+[[package]]
+name = "sval_serde"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046"
+dependencies = [
+ "serde",
+ "sval",
+ "sval_buffer",
+ "sval_fmt",
+]
+
+[[package]]
+name = "svg_fmt"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
+
+[[package]]
+name = "svgfilters"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb0dce2fee79ac40c21dafba48565ff7a5fa275e23ffe9ce047a40c9574ba34e"
+dependencies = [
+ "float-cmp",
+ "rgb",
+]
+
+[[package]]
+name = "svgtypes"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
+dependencies = [
+ "float-cmp",
+ "siphasher",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "take-until"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb"
+
+[[package]]
+name = "thiserror"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tiff"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
+dependencies = [
+ "jpeg-decoder",
+ "miniz_oxide 0.4.4",
+ "weezl",
+]
+
+[[package]]
+name = "time"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
+dependencies = [
+ "itoa",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
+
+[[package]]
+name = "time-macros"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
+name = "tiny-skia"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf81f2900d2e235220e6f31ec9f63ade6a7f59090c556d74fe949bb3b15e9fe"
+dependencies = [
+ "arrayref",
+ "arrayvec 0.5.2",
+ "bytemuck",
+ "cfg-if",
+ "png",
+ "safe_arch",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tracing-futures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
+dependencies = [
+ "pin-project",
+ "tracing",
+]
+
+[[package]]
+name = "ttf-parser"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ddb402ac6c2af6f7a2844243887631c4e94b51585b229fcfddb43958cd55ca"
+
+[[package]]
+name = "ttf-parser"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6"
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
+
+[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694"
+
+[[package]]
+name = "unicode-ccc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1"
+
+[[package]]
+name = "unicode-general-category"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-script"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc"
+
+[[package]]
+name = "unicode-vo"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
+
+[[package]]
+name = "url"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "usvg"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef8352f317d8f9a918ba5154797fb2a93e2730244041cf7d5be35148266adfa5"
+dependencies = [
+ "base64",
+ "data-url",
+ "flate2",
+ "fontdb",
+ "kurbo",
+ "log",
+ "memmap2",
+ "pico-args",
+ "rctree",
+ "roxmltree",
+ "rustybuzz",
+ "simplecss",
+ "siphasher",
+ "svgtypes",
+ "ttf-parser 0.12.3",
+ "unicode-bidi",
+ "unicode-script",
+ "unicode-vo",
+ "xmlwriter",
+]
+
+[[package]]
+name = "util"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "backtrace",
+ "dirs",
+ "futures",
+ "isahc",
+ "lazy_static",
+ "log",
+ "rand",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "smol",
+ "take-until",
+ "url",
+]
+
+[[package]]
+name = "uuid"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
+
+[[package]]
+name = "uuid"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "value-bag"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3"
+dependencies = [
+ "value-bag-serde1",
+ "value-bag-sval2",
+]
+
+[[package]]
+name = "value-bag-serde1"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_fmt",
+]
+
+[[package]]
+name = "value-bag-sval2"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d"
+dependencies = [
+ "sval",
+ "sval_buffer",
+ "sval_dynamic",
+ "sval_fmt",
+ "sval_json",
+ "sval_ref",
+ "sval_serde",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "walkdir"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "weezl"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
+
+[[package]]
+name = "which"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "wio"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "xmlparser"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
+
+[[package]]
+name = "xmlwriter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
+
+[[package]]
+name = "yeslogic-fontconfig-sys"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386"
+dependencies = [
+ "const-cstr",
+ "dlib",
+ "once_cell",
+ "pkg-config",
+]
@@ -0,0 +1,26 @@
+[package]
+name = "playground"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "playground"
+path = "src/playground.rs"
+
+[dependencies]
+anyhow.workspace = true
+derive_more.workspace = true
+gpui = { path = ".." }
+log.workspace = true
+playground_macros = { path = "../playground_macros" }
+parking_lot.workspace = true
+refineable.workspace = true
+serde.workspace = true
+simplelog = "0.9"
+smallvec.workspace = true
+taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] }
+util = { path = "../../util" }
+
+[dev-dependencies]
+gpui = { path = "..", features = ["test-support"] }
@@ -0,0 +1,72 @@
+Much of element styling is now handled by an external engine.
+
+
+How do I make an element hover.
+
+There's a hover style.
+
+Hoverable needs to wrap another element. That element can be styled.
+
+```rs
+struct Hoverable<E: Element> {
+
+}
+
+impl<V> Element<V> for Hoverable {
+
+}
+
+```
+
+
+
+```rs
+#[derive(Styled, Interactive)]
+pub struct Div {
+ declared_style: StyleRefinement,
+ interactions: Interactions
+}
+
+pub trait Styled {
+ fn declared_style(&mut self) -> &mut StyleRefinement;
+ fn compute_style(&mut self) -> Style {
+ Style::default().refine(self.declared_style())
+ }
+
+ // All the tailwind classes, modifying self.declared_style()
+}
+
+impl Style {
+ pub fn paint_background<V>(layout: Layout, cx: &mut PaintContext<V>);
+ pub fn paint_foreground<V>(layout: Layout, cx: &mut PaintContext<V>);
+}
+
+pub trait Interactive<V> {
+ fn interactions(&mut self) -> &mut Interactions<V>;
+
+ fn on_click(self, )
+}
+
+struct Interactions<V> {
+ click: SmallVec<[<Rc<dyn Fn(&mut V, &dyn Any, )>; 1]>,
+}
+
+
+```
+
+
+```rs
+
+
+trait Stylable {
+ type Style;
+
+ fn with_style(self, style: Self::Style) -> Self;
+}
+
+
+
+
+
+
+```
@@ -0,0 +1,78 @@
+use crate::{layout_context::LayoutContext, paint_context::PaintContext};
+use gpui::{geometry::rect::RectF, LayoutEngine, LayoutId};
+use util::ResultExt;
+
+/// Makes a new, playground-style element into a legacy element.
+pub struct AdapterElement<V>(pub(crate) crate::element::AnyElement<V>);
+
+impl<V: 'static> gpui::Element<V> for AdapterElement<V> {
+ type LayoutState = Option<(LayoutEngine, LayoutId)>;
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: gpui::SizeConstraint,
+ view: &mut V,
+ cx: &mut gpui::LayoutContext<V>,
+ ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+ cx.push_layout_engine(LayoutEngine::new());
+
+ let size = constraint.max;
+ let mut cx = LayoutContext::new(cx);
+ let layout_id = self.0.layout(view, &mut cx).log_err();
+ if let Some(layout_id) = layout_id {
+ cx.layout_engine()
+ .unwrap()
+ .compute_layout(layout_id, constraint.max)
+ .log_err();
+ }
+
+ let layout_engine = cx.pop_layout_engine();
+ debug_assert!(layout_engine.is_some(),
+ "unexpected layout stack state. is there an unmatched pop_layout_engine in the called code?"
+ );
+
+ (constraint.max, layout_engine.zip(layout_id))
+ }
+
+ fn paint(
+ &mut self,
+ scene: &mut gpui::SceneBuilder,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout_data: &mut Option<(LayoutEngine, LayoutId)>,
+ view: &mut V,
+ legacy_cx: &mut gpui::PaintContext<V>,
+ ) -> Self::PaintState {
+ let (layout_engine, layout_id) = layout_data.take().unwrap();
+ legacy_cx.push_layout_engine(layout_engine);
+ let mut cx = PaintContext::new(legacy_cx, scene);
+ self.0.paint(view, &mut cx);
+ *layout_data = legacy_cx.pop_layout_engine().zip(Some(layout_id));
+ debug_assert!(layout_data.is_some());
+ }
+
+ fn rect_for_text_range(
+ &self,
+ range_utf16: std::ops::Range<usize>,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout: &Self::LayoutState,
+ paint: &Self::PaintState,
+ view: &V,
+ cx: &gpui::ViewContext<V>,
+ ) -> Option<RectF> {
+ todo!("implement before merging to main")
+ }
+
+ fn debug(
+ &self,
+ bounds: RectF,
+ layout: &Self::LayoutState,
+ paint: &Self::PaintState,
+ view: &V,
+ cx: &gpui::ViewContext<V>,
+ ) -> gpui::serde_json::Value {
+ todo!("implement before merging to main")
+ }
+}
@@ -0,0 +1,276 @@
+#![allow(dead_code)]
+
+use std::{num::ParseIntError, ops::Range};
+
+use smallvec::SmallVec;
+
+pub fn rgb<C: From<Rgba>>(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,
+}
+
+pub trait Lerp {
+ fn lerp(&self, level: f32) -> Hsla;
+}
+
+impl Lerp for Range<Hsla> {
+ fn lerp(&self, level: f32) -> Hsla {
+ let level = level.clamp(0., 1.);
+ Hsla {
+ h: self.start.h + (level * (self.end.h - self.start.h)),
+ s: self.start.s + (level * (self.end.s - self.start.s)),
+ l: self.start.l + (level * (self.end.l - self.start.l)),
+ a: self.start.a + (level * (self.end.a - self.start.a)),
+ }
+ }
+}
+
+impl From<gpui::color::Color> for Rgba {
+ fn from(value: gpui::color::Color) -> Self {
+ Self {
+ r: value.0.r as f32 / 255.0,
+ g: value.0.g as f32 / 255.0,
+ b: value.0.b as f32 / 255.0,
+ a: value.0.a as f32 / 255.0,
+ }
+ }
+}
+
+impl From<Hsla> 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<Self, Self::Error> {
+ 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 })
+ }
+}
+
+impl Into<gpui::color::Color> for Rgba {
+ fn into(self) -> gpui::color::Color {
+ gpui::color::rgba(self.r, self.g, self.b, self.a)
+ }
+}
+
+#[derive(Default, Copy, Clone, Debug, PartialEq)]
+pub struct Hsla {
+ pub h: f32,
+ pub s: f32,
+ pub l: f32,
+ pub a: f32,
+}
+
+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 From<Rgba> 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 Hsla {
+ /// Scales the saturation and lightness by the given values, clamping at 1.0.
+ pub fn scale_sl(mut self, s: f32, l: f32) -> Self {
+ self.s = (self.s * s).clamp(0., 1.);
+ self.l = (self.l * l).clamp(0., 1.);
+ self
+ }
+
+ /// Increases the saturation of the color by a certain amount, with a max
+ /// value of 1.0.
+ pub fn saturate(mut self, amount: f32) -> Self {
+ self.s += amount;
+ self.s = self.s.clamp(0.0, 1.0);
+ self
+ }
+
+ /// Decreases the saturation of the color by a certain amount, with a min
+ /// value of 0.0.
+ pub fn desaturate(mut self, amount: f32) -> Self {
+ self.s -= amount;
+ self.s = self.s.max(0.0);
+ if self.s < 0.0 {
+ self.s = 0.0;
+ }
+ self
+ }
+
+ /// Brightens the color by increasing the lightness by a certain amount,
+ /// with a max value of 1.0.
+ pub fn brighten(mut self, amount: f32) -> Self {
+ self.l += amount;
+ self.l = self.l.clamp(0.0, 1.0);
+ self
+ }
+
+ /// Darkens the color by decreasing the lightness by a certain amount,
+ /// with a max value of 0.0.
+ pub fn darken(mut self, amount: f32) -> Self {
+ self.l -= amount;
+ self.l = self.l.clamp(0.0, 1.0);
+ self
+ }
+}
+
+impl From<gpui::color::Color> for Hsla {
+ fn from(value: gpui::color::Color) -> Self {
+ Rgba::from(value).into()
+ }
+}
+
+impl Into<gpui::color::Color> for Hsla {
+ fn into(self) -> gpui::color::Color {
+ Rgba::from(self).into()
+ }
+}
+
+pub struct ColorScale {
+ colors: SmallVec<[Hsla; 2]>,
+ positions: SmallVec<[f32; 2]>,
+}
+
+pub fn scale<I, C>(colors: I) -> ColorScale
+where
+ I: IntoIterator<Item = C>,
+ C: Into<Hsla>,
+{
+ let mut scale = ColorScale {
+ colors: colors.into_iter().map(Into::into).collect(),
+ positions: SmallVec::new(),
+ };
+ let num_colors: f32 = scale.colors.len() as f32 - 1.0;
+ scale.positions = (0..scale.colors.len())
+ .map(|i| i as f32 / num_colors)
+ .collect();
+ scale
+}
+
+impl ColorScale {
+ fn at(&self, t: f32) -> Hsla {
+ // Ensure that the input is within [0.0, 1.0]
+ debug_assert!(
+ 0.0 <= t && t <= 1.0,
+ "t value {} is out of range. Expected value in range 0.0 to 1.0",
+ t
+ );
+
+ let position = match self
+ .positions
+ .binary_search_by(|a| a.partial_cmp(&t).unwrap())
+ {
+ Ok(index) | Err(index) => index,
+ };
+ let lower_bound = position.saturating_sub(1);
+ let upper_bound = position.min(self.colors.len() - 1);
+ let lower_color = &self.colors[lower_bound];
+ let upper_color = &self.colors[upper_bound];
+
+ match upper_bound.checked_sub(lower_bound) {
+ Some(0) | None => *lower_color,
+ Some(_) => {
+ let interval_t = (t - self.positions[lower_bound])
+ / (self.positions[upper_bound] - self.positions[lower_bound]);
+ let h = lower_color.h + interval_t * (upper_color.h - lower_color.h);
+ let s = lower_color.s + interval_t * (upper_color.s - lower_color.s);
+ let l = lower_color.l + interval_t * (upper_color.l - lower_color.l);
+ let a = lower_color.a + interval_t * (upper_color.a - lower_color.a);
+ Hsla { h, s, l, a }
+ }
+ }
+ }
+}
@@ -0,0 +1,100 @@
+use crate::{
+ div::div,
+ element::{Element, ParentElement},
+ style::StyleHelpers,
+ text::ArcCow,
+ themes::rose_pine,
+};
+use gpui::ViewContext;
+use playground_macros::Element;
+use std::{marker::PhantomData, rc::Rc};
+
+struct ButtonHandlers<V, D> {
+ click: Option<Rc<dyn Fn(&mut V, &D, &mut ViewContext<V>)>>,
+}
+
+impl<V, D> Default for ButtonHandlers<V, D> {
+ fn default() -> Self {
+ Self { click: None }
+ }
+}
+
+use crate as playground;
+#[derive(Element)]
+pub struct Button<V: 'static, D: 'static> {
+ handlers: ButtonHandlers<V, D>,
+ label: Option<ArcCow<'static, str>>,
+ icon: Option<ArcCow<'static, str>>,
+ data: Rc<D>,
+ view_type: PhantomData<V>,
+}
+
+// Impl block for buttons without data.
+// See below for an impl block for any button.
+impl<V: 'static> Button<V, ()> {
+ fn new() -> Self {
+ Self {
+ handlers: ButtonHandlers::default(),
+ label: None,
+ icon: None,
+ data: Rc::new(()),
+ view_type: PhantomData,
+ }
+ }
+
+ pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
+ Button {
+ handlers: ButtonHandlers::default(),
+ label: self.label,
+ icon: self.icon,
+ data: Rc::new(data),
+ view_type: PhantomData,
+ }
+ }
+}
+
+// Impl block for *any* button.
+impl<V: 'static, D: 'static> Button<V, D> {
+ pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
+ self.label = Some(label.into());
+ self
+ }
+
+ pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
+ self.icon = Some(icon.into());
+ self
+ }
+
+ // pub fn click(self, handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static) -> Self {
+ // let data = self.data.clone();
+ // Self::click(self, MouseButton::Left, move |view, _, cx| {
+ // handler(view, data.as_ref(), cx);
+ // })
+ // }
+}
+
+pub fn button<V>() -> Button<V, ()> {
+ Button::new()
+}
+
+impl<V: 'static, D: 'static> Button<V, D> {
+ fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
+ // TODO: Drive theme from the context
+ let button = div()
+ .fill(rose_pine::dawn().error(0.5))
+ .h_4()
+ .children(self.label.clone());
+
+ button
+
+ // TODO: Event handling
+ // if let Some(handler) = self.handlers.click.clone() {
+ // let data = self.data.clone();
+ // // button.mouse_down(MouseButton::Left, move |view, event, cx| {
+ // // handler(view, data.as_ref(), cx)
+ // // })
+ // } else {
+ // button
+ // }
+ }
+}
@@ -0,0 +1,108 @@
+use crate::{
+ element::{AnyElement, Element, Layout, ParentElement},
+ interactive::{InteractionHandlers, Interactive},
+ layout_context::LayoutContext,
+ paint_context::PaintContext,
+ style::{Style, StyleHelpers, StyleRefinement, Styleable},
+};
+use anyhow::Result;
+use gpui::LayoutId;
+use smallvec::SmallVec;
+
+pub struct Div<V: 'static> {
+ style: StyleRefinement,
+ handlers: InteractionHandlers<V>,
+ children: SmallVec<[AnyElement<V>; 2]>,
+}
+
+pub fn div<V>() -> Div<V> {
+ Div {
+ style: Default::default(),
+ handlers: Default::default(),
+ children: Default::default(),
+ }
+}
+
+impl<V: 'static> Element<V> for Div<V> {
+ type Layout = ();
+
+ fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<Layout<V, ()>>
+ where
+ Self: Sized,
+ {
+ let children = self
+ .children
+ .iter_mut()
+ .map(|child| child.layout(view, cx))
+ .collect::<Result<Vec<LayoutId>>>()?;
+
+ cx.add_layout_node(self.style(), (), children)
+ }
+
+ fn paint(&mut self, view: &mut V, layout: &mut Layout<V, ()>, cx: &mut PaintContext<V>)
+ where
+ Self: Sized,
+ {
+ let style = self.style();
+
+ style.paint_background::<V, Self>(layout, cx);
+ for child in &mut self.children {
+ child.paint(view, cx);
+ }
+ }
+}
+
+impl<V> Styleable for Div<V> {
+ type Style = Style;
+
+ fn declared_style(&mut self) -> &mut StyleRefinement {
+ &mut self.style
+ }
+}
+
+impl<V> StyleHelpers for Div<V> {}
+
+impl<V> Interactive<V> for Div<V> {
+ fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
+ &mut self.handlers
+ }
+}
+
+impl<V: 'static> ParentElement<V> for Div<V> {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+ &mut self.children
+ }
+}
+
+#[test]
+fn test() {
+ // let elt = div().w_auto();
+}
+
+// trait Element<V: 'static> {
+// type Style;
+
+// fn layout()
+// }
+
+// trait Stylable<V: 'static>: Element<V> {
+// type Style;
+
+// fn with_style(self, style: Self::Style) -> Self;
+// }
+
+// pub struct HoverStyle<S> {
+// default: S,
+// hovered: S,
+// }
+
+// struct Hover<V: 'static, C: Stylable<V>> {
+// child: C,
+// style: HoverStyle<C::Style>,
+// }
+
+// impl<V: 'static, C: Stylable<V>> Hover<V, C> {
+// fn new(child: C, style: HoverStyle<C::Style>) -> Self {
+// Self { child, style }
+// }
+// }
@@ -0,0 +1,158 @@
+use anyhow::Result;
+use derive_more::{Deref, DerefMut};
+use gpui::{geometry::rect::RectF, EngineLayout};
+use smallvec::SmallVec;
+use std::marker::PhantomData;
+use util::ResultExt;
+
+pub use crate::layout_context::LayoutContext;
+pub use crate::paint_context::PaintContext;
+
+type LayoutId = gpui::LayoutId;
+
+pub trait Element<V: 'static>: 'static {
+ type Layout;
+
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut LayoutContext<V>,
+ ) -> Result<Layout<V, Self::Layout>>
+ where
+ Self: Sized;
+
+ fn paint(
+ &mut self,
+ view: &mut V,
+ layout: &mut Layout<V, Self::Layout>,
+ cx: &mut PaintContext<V>,
+ ) where
+ Self: Sized;
+
+ fn into_any(self) -> AnyElement<V>
+ where
+ Self: 'static + Sized,
+ {
+ AnyElement(Box::new(ElementState {
+ element: self,
+ layout: None,
+ }))
+ }
+}
+
+/// Used to make ElementState<V, E> into a trait object, so we can wrap it in AnyElement<V>.
+trait ElementStateObject<V> {
+ fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId>;
+ fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>);
+}
+
+/// A wrapper around an element that stores its layout state.
+struct ElementState<V: 'static, E: Element<V>> {
+ element: E,
+ layout: Option<Layout<V, E::Layout>>,
+}
+
+/// We blanket-implement the object-safe ElementStateObject interface to make ElementStates into trait objects
+impl<V, E: Element<V>> ElementStateObject<V> for ElementState<V, E> {
+ fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId> {
+ let layout = self.element.layout(view, cx)?;
+ let layout_id = layout.id;
+ self.layout = Some(layout);
+ Ok(layout_id)
+ }
+
+ fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
+ let layout = self.layout.as_mut().expect("paint called before layout");
+ if layout.engine_layout.is_none() {
+ layout.engine_layout = cx.computed_layout(layout.id).log_err()
+ }
+ self.element.paint(view, layout, cx)
+ }
+}
+
+/// A dynamic element.
+pub struct AnyElement<V>(Box<dyn ElementStateObject<V>>);
+
+impl<V> AnyElement<V> {
+ pub fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId> {
+ self.0.layout(view, cx)
+ }
+
+ pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
+ self.0.paint(view, cx)
+ }
+}
+
+#[derive(Deref, DerefMut)]
+pub struct Layout<V, D> {
+ id: LayoutId,
+ engine_layout: Option<EngineLayout>,
+ #[deref]
+ #[deref_mut]
+ element_data: D,
+ view_type: PhantomData<V>,
+}
+
+impl<V: 'static, D> Layout<V, D> {
+ pub fn new(id: LayoutId, element_data: D) -> Self {
+ Self {
+ id,
+ engine_layout: None,
+ element_data: element_data,
+ view_type: PhantomData,
+ }
+ }
+
+ pub fn bounds(&mut self, cx: &mut PaintContext<V>) -> RectF {
+ self.engine_layout(cx).bounds
+ }
+
+ pub fn order(&mut self, cx: &mut PaintContext<V>) -> u32 {
+ self.engine_layout(cx).order
+ }
+
+ fn engine_layout(&mut self, cx: &mut PaintContext<'_, '_, '_, '_, V>) -> &mut EngineLayout {
+ self.engine_layout
+ .get_or_insert_with(|| cx.computed_layout(self.id).log_err().unwrap_or_default())
+ }
+}
+
+impl<V: 'static> Layout<V, Option<AnyElement<V>>> {
+ pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
+ let mut element = self.element_data.take().unwrap();
+ element.paint(view, cx);
+ self.element_data = Some(element);
+ }
+}
+
+pub trait ParentElement<V: 'static> {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
+
+ fn child(mut self, child: impl IntoElement<V>) -> Self
+ where
+ Self: Sized,
+ {
+ self.children_mut().push(child.into_element().into_any());
+ self
+ }
+
+ fn children<I, E>(mut self, children: I) -> Self
+ where
+ I: IntoIterator<Item = E>,
+ E: IntoElement<V>,
+ Self: Sized,
+ {
+ self.children_mut().extend(
+ children
+ .into_iter()
+ .map(|child| child.into_element().into_any()),
+ );
+ self
+ }
+}
+
+pub trait IntoElement<V: 'static> {
+ type Element: Element<V>;
+
+ fn into_element(self) -> Self::Element;
+}
@@ -0,0 +1,76 @@
+use crate::{
+ element::{Element, Layout},
+ layout_context::LayoutContext,
+ paint_context::PaintContext,
+ style::{StyleRefinement, Styleable},
+};
+use anyhow::Result;
+use gpui::platform::MouseMovedEvent;
+use refineable::Refineable;
+use std::{cell::Cell, marker::PhantomData};
+
+pub struct Hoverable<V: 'static, E: Element<V> + Styleable> {
+ hovered: Cell<bool>,
+ child_style: StyleRefinement,
+ hovered_style: StyleRefinement,
+ child: E,
+ view_type: PhantomData<V>,
+}
+
+pub fn hoverable<V, E: Element<V> + Styleable>(mut child: E) -> Hoverable<V, E> {
+ Hoverable {
+ hovered: Cell::new(false),
+ child_style: child.declared_style().clone(),
+ hovered_style: Default::default(),
+ child,
+ view_type: PhantomData,
+ }
+}
+
+impl<V, E: Element<V> + Styleable> Styleable for Hoverable<V, E> {
+ type Style = E::Style;
+
+ fn declared_style(&mut self) -> &mut crate::style::StyleRefinement {
+ self.child.declared_style()
+ }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> Element<V> for Hoverable<V, E> {
+ type Layout = E::Layout;
+
+ fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<Layout<V, Self::Layout>>
+ where
+ Self: Sized,
+ {
+ self.child.layout(view, cx)
+ }
+
+ fn paint(
+ &mut self,
+ view: &mut V,
+ layout: &mut Layout<V, Self::Layout>,
+ cx: &mut PaintContext<V>,
+ ) where
+ Self: Sized,
+ {
+ if self.hovered.get() {
+ // If hovered, refine the child's style with this element's style.
+ self.child.declared_style().refine(&self.hovered_style);
+ } else {
+ // Otherwise, set the child's style back to its original style.
+ *self.child.declared_style() = self.child_style.clone();
+ }
+
+ let bounds = layout.bounds(cx);
+ let order = layout.order(cx);
+ self.hovered.set(bounds.contains_point(cx.mouse_position()));
+ let was_hovered = self.hovered.clone();
+ cx.on_event(order, move |view, event: &MouseMovedEvent, cx| {
+ let is_hovered = bounds.contains_point(event.position);
+ if is_hovered != was_hovered.get() {
+ was_hovered.set(is_hovered);
+ cx.repaint();
+ }
+ });
+ }
+}
@@ -0,0 +1,34 @@
+use gpui::{platform::MouseMovedEvent, EventContext};
+use smallvec::SmallVec;
+use std::rc::Rc;
+
+pub trait Interactive<V: 'static> {
+ fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V>;
+
+ fn on_mouse_move<H>(mut self, handler: H) -> Self
+ where
+ H: 'static + Fn(&mut V, &MouseMovedEvent, bool, &mut EventContext<V>),
+ Self: Sized,
+ {
+ self.interaction_handlers()
+ .mouse_moved
+ .push(Rc::new(move |view, event, hit_test, cx| {
+ handler(view, event, hit_test, cx);
+ cx.bubble
+ }));
+ self
+ }
+}
+
+pub struct InteractionHandlers<V: 'static> {
+ mouse_moved:
+ SmallVec<[Rc<dyn Fn(&mut V, &MouseMovedEvent, bool, &mut EventContext<V>) -> bool>; 2]>,
+}
+
+impl<V> Default for InteractionHandlers<V> {
+ fn default() -> Self {
+ Self {
+ mouse_moved: Default::default(),
+ }
+ }
+}
@@ -0,0 +1,54 @@
+use anyhow::{anyhow, Result};
+use derive_more::{Deref, DerefMut};
+pub use gpui::LayoutContext as LegacyLayoutContext;
+use gpui::{RenderContext, ViewContext};
+pub use taffy::tree::NodeId;
+
+use crate::{element::Layout, style::Style};
+
+#[derive(Deref, DerefMut)]
+pub struct LayoutContext<'a, 'b, 'c, 'd, V> {
+ #[deref]
+ #[deref_mut]
+ pub(crate) legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>,
+}
+
+impl<'a, 'b, V> RenderContext<'a, 'b, V> for LayoutContext<'a, 'b, '_, '_, V> {
+ fn text_style(&self) -> gpui::fonts::TextStyle {
+ self.legacy_cx.text_style()
+ }
+
+ fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
+ self.legacy_cx.push_text_style(style)
+ }
+
+ fn pop_text_style(&mut self) {
+ self.legacy_cx.pop_text_style()
+ }
+
+ fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
+ &mut self.view_context
+ }
+}
+
+impl<'a, 'b, 'c, 'd, V: 'static> LayoutContext<'a, 'b, 'c, 'd, V> {
+ pub fn new(legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>) -> Self {
+ Self { legacy_cx }
+ }
+
+ pub fn add_layout_node<D>(
+ &mut self,
+ style: Style,
+ element_data: D,
+ children: impl IntoIterator<Item = NodeId>,
+ ) -> Result<Layout<V, D>> {
+ let rem_size = self.rem_pixels();
+ let id = self
+ .legacy_cx
+ .layout_engine()
+ .ok_or_else(|| anyhow!("no layout engine"))?
+ .add_node(style.to_taffy(rem_size), children)?;
+
+ Ok(Layout::new(id, element_data))
+ }
+}
@@ -0,0 +1,71 @@
+use anyhow::{anyhow, Result};
+use derive_more::{Deref, DerefMut};
+use gpui::{scene::EventHandler, EngineLayout, EventContext, LayoutId, RenderContext, ViewContext};
+pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext};
+use std::{any::TypeId, rc::Rc};
+pub use taffy::tree::NodeId;
+
+#[derive(Deref, DerefMut)]
+pub struct PaintContext<'a, 'b, 'c, 'd, V> {
+ #[deref]
+ #[deref_mut]
+ pub(crate) legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
+ pub(crate) scene: &'d mut gpui::SceneBuilder,
+}
+
+impl<'a, 'b, V> RenderContext<'a, 'b, V> for PaintContext<'a, 'b, '_, '_, V> {
+ fn text_style(&self) -> gpui::fonts::TextStyle {
+ self.legacy_cx.text_style()
+ }
+
+ fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
+ self.legacy_cx.push_text_style(style)
+ }
+
+ fn pop_text_style(&mut self) {
+ self.legacy_cx.pop_text_style()
+ }
+
+ fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
+ &mut self.view_context
+ }
+}
+
+impl<'a, 'b, 'c, 'd, V: 'static> PaintContext<'a, 'b, 'c, 'd, V> {
+ pub fn new(
+ legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
+ scene: &'d mut gpui::SceneBuilder,
+ ) -> Self {
+ Self { legacy_cx, scene }
+ }
+
+ pub fn on_event<E: 'static>(
+ &mut self,
+ order: u32,
+ handler: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
+ ) {
+ let view = self.weak_handle();
+
+ self.scene.event_handlers.push(EventHandler {
+ order,
+ handler: Rc::new(move |event, window_cx| {
+ if let Some(view) = view.upgrade(window_cx) {
+ view.update(window_cx, |view, view_cx| {
+ let mut event_cx = EventContext::new(view_cx);
+ handler(view, event.downcast_ref().unwrap(), &mut event_cx);
+ event_cx.bubble
+ })
+ } else {
+ true
+ }
+ }),
+ event_type: TypeId::of::<E>(),
+ })
+ }
+
+ pub(crate) fn computed_layout(&mut self, layout_id: LayoutId) -> Result<EngineLayout> {
+ self.layout_engine()
+ .ok_or_else(|| anyhow!("no layout engine present"))?
+ .computed_layout(layout_id)
+ }
+}
@@ -0,0 +1,83 @@
+#![allow(dead_code, unused_variables)]
+use crate::{color::black, style::StyleHelpers};
+use element::Element;
+use gpui::{
+ geometry::{rect::RectF, vector::vec2f},
+ platform::WindowOptions,
+};
+use log::LevelFilter;
+use simplelog::SimpleLogger;
+use themes::{rose_pine, ThemeColors};
+use view::view;
+
+mod adapter;
+mod color;
+mod components;
+mod div;
+mod element;
+mod hoverable;
+mod interactive;
+mod layout_context;
+mod paint_context;
+mod style;
+mod text;
+mod themes;
+mod view;
+
+fn main() {
+ SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+
+ gpui::App::new(()).unwrap().run(|cx| {
+ cx.add_window(
+ WindowOptions {
+ bounds: gpui::platform::WindowBounds::Fixed(RectF::new(
+ vec2f(0., 0.),
+ vec2f(400., 300.),
+ )),
+ center: true,
+ ..Default::default()
+ },
+ |_| view(|_| playground(&rose_pine::moon())),
+ );
+ cx.platform().activate(true);
+ });
+}
+
+fn playground<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+ use div::div;
+
+ div()
+ .text_color(black())
+ .h_full()
+ .w_1_2()
+ .fill(theme.success(0.5))
+ // .hover()
+ // .fill(theme.error(0.5))
+ // .child(button().label("Hello").click(|_, _, _| println!("click!")))
+}
+
+// todo!()
+// // column()
+// // .size(auto())
+// // .fill(theme.base(0.5))
+// // .text_color(theme.text(0.5))
+// // .child(title_bar(theme))
+// // .child(stage(theme))
+// // .child(status_bar(theme))
+// }
+
+// fn title_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+// row()
+// .fill(theme.base(0.2))
+// .justify(0.)
+// .width(auto())
+// .child(text("Zed Playground"))
+// }
+
+// fn stage<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+// row().fill(theme.surface(0.9))
+// }
+
+// fn status_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+// row().fill(theme.surface(0.1))
+// }
@@ -0,0 +1,286 @@
+use crate::{
+ color::Hsla,
+ element::{Element, Layout},
+ paint_context::PaintContext,
+};
+use gpui::{
+ fonts::TextStyleRefinement,
+ geometry::{
+ AbsoluteLength, DefiniteLength, Edges, EdgesRefinement, Length, Point, PointRefinement,
+ Size, SizeRefinement,
+ },
+};
+use playground_macros::styleable_helpers;
+use refineable::Refineable;
+pub use taffy::style::{
+ AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
+ Overflow, Position,
+};
+
+#[derive(Clone, Refineable)]
+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<Overflow>,
+ /// 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<Length>,
+
+ // Size properies
+ /// Sets the initial size of the item
+ #[refineable]
+ pub size: Size<Length>,
+ /// Controls the minimum size of the item
+ #[refineable]
+ pub min_size: Size<Length>,
+ /// Controls the maximum size of the item
+ #[refineable]
+ pub max_size: Size<Length>,
+ /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
+ pub aspect_ratio: Option<f32>,
+
+ // Spacing Properties
+ /// How large should the margin be on each side?
+ #[refineable]
+ pub margin: Edges<Length>,
+ /// How large should the padding be on each side?
+ #[refineable]
+ pub padding: Edges<DefiniteLength>,
+ /// How large should the border be on each side?
+ #[refineable]
+ pub border: Edges<DefiniteLength>,
+
+ // Alignment properties
+ /// How this node's children aligned in the cross/block axis?
+ pub align_items: Option<AlignItems>,
+ /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
+ pub align_self: Option<AlignSelf>,
+ /// How should content contained within this item be aligned in the cross/block axis
+ pub align_content: Option<AlignContent>,
+ /// How should contained within this item be aligned in the main/inline axis
+ pub justify_content: Option<JustifyContent>,
+ /// How large should the gaps between items in a flex container be?
+ #[refineable]
+ pub gap: Size<DefiniteLength>,
+
+ // 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<Fill>,
+ /// 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<Hsla>,
+}
+
+impl Style {
+ pub fn to_taffy(&self, rem_size: f32) -> 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.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).into(),
+ flex_grow: self.flex_grow,
+ flex_shrink: self.flex_shrink,
+ ..Default::default() // Ignore grid properties for now
+ }
+ }
+
+ /// Paints the background of an element styled with this style.
+ /// Return the bounds in which to paint the content.
+ pub fn paint_background<V: 'static, E: Element<V>>(
+ &self,
+ layout: &mut Layout<V, E::Layout>,
+ cx: &mut PaintContext<V>,
+ ) {
+ let bounds = layout.bounds(cx);
+ let rem_size = cx.rem_pixels();
+ if let Some(color) = self.fill.as_ref().and_then(Fill::color) {
+ cx.scene.push_quad(gpui::Quad {
+ bounds,
+ background: Some(color.into()),
+ corner_radii: self.corner_radii.to_gpui(rem_size),
+ border: Default::default(),
+ });
+ }
+ }
+}
+
+impl Default for Style {
+ fn default() -> Self {
+ Style {
+ display: Display::DEFAULT,
+ overflow: Point {
+ x: Overflow::Visible,
+ y: Overflow::Visible,
+ },
+ scrollbar_width: 0.0,
+ position: Position::Relative,
+ inset: Edges::auto(),
+ margin: Edges::<Length>::zero(),
+ padding: Edges::<DefiniteLength>::zero(),
+ border: Edges::<DefiniteLength>::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,
+ text_color: None,
+ corner_radii: CornerRadii::default(),
+ }
+ }
+}
+
+impl StyleRefinement {
+ pub fn text_style(&self) -> Option<TextStyleRefinement> {
+ self.text_color.map(|color| TextStyleRefinement {
+ color: Some(color.into()),
+ ..Default::default()
+ })
+ }
+}
+
+pub struct OptionalTextStyle {
+ color: Option<Hsla>,
+}
+
+impl OptionalTextStyle {
+ pub fn apply(&self, style: &mut gpui::fonts::TextStyle) {
+ if let Some(color) = self.color {
+ style.color = color.into();
+ }
+ }
+}
+
+#[derive(Clone)]
+pub enum Fill {
+ Color(Hsla),
+}
+
+impl Fill {
+ pub fn color(&self) -> Option<Hsla> {
+ match self {
+ Fill::Color(color) => Some(*color),
+ }
+ }
+}
+
+impl Default for Fill {
+ fn default() -> Self {
+ Self::Color(Hsla::default())
+ }
+}
+
+impl From<Hsla> for Fill {
+ fn from(color: Hsla) -> Self {
+ Self::Color(color)
+ }
+}
+
+#[derive(Clone, Refineable, Default)]
+pub struct CornerRadii {
+ top_left: AbsoluteLength,
+ top_right: AbsoluteLength,
+ bottom_left: AbsoluteLength,
+ bottom_right: AbsoluteLength,
+}
+
+impl CornerRadii {
+ pub fn to_gpui(&self, rem_size: f32) -> gpui::scene::CornerRadii {
+ gpui::scene::CornerRadii {
+ top_left: self.top_left.to_pixels(rem_size),
+ top_right: self.top_right.to_pixels(rem_size),
+ bottom_left: self.bottom_left.to_pixels(rem_size),
+ bottom_right: self.bottom_right.to_pixels(rem_size),
+ }
+ }
+}
+
+pub trait Styleable {
+ type Style: refineable::Refineable;
+
+ fn declared_style(&mut self) -> &mut playground::style::StyleRefinement;
+
+ fn style(&mut self) -> playground::style::Style {
+ let mut style = playground::style::Style::default();
+ style.refine(self.declared_style());
+ style
+ }
+}
+
+// Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
+//
+// Example:
+// // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
+// fn p_2(mut self) -> Self where Self: Sized;
+use crate as playground; // Macro invocation references this crate as playground.
+pub trait StyleHelpers: Styleable<Style = Style> {
+ styleable_helpers!();
+
+ fn fill<F>(mut self, fill: F) -> Self
+ where
+ F: Into<Fill>,
+ Self: Sized,
+ {
+ self.declared_style().fill = Some(fill.into());
+ self
+ }
+
+ fn text_color<C>(mut self, color: C) -> Self
+ where
+ C: Into<Hsla>,
+ Self: Sized,
+ {
+ self.declared_style().text_color = Some(color.into());
+ self
+ }
+}
@@ -0,0 +1,151 @@
+use crate::{
+ element::{Element, IntoElement, Layout},
+ layout_context::LayoutContext,
+ paint_context::PaintContext,
+};
+use anyhow::Result;
+use gpui::text_layout::LineLayout;
+use parking_lot::Mutex;
+use std::sync::Arc;
+
+impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
+ type Element = Text;
+
+ fn into_element(self) -> Self::Element {
+ Text { text: self.into() }
+ }
+}
+
+pub struct Text {
+ text: ArcCow<'static, str>,
+}
+
+impl<V: 'static> Element<V> for Text {
+ type Layout = Arc<Mutex<Option<TextLayout>>>;
+
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut LayoutContext<V>,
+ ) -> Result<Layout<V, Self::Layout>> {
+ // let rem_size = cx.rem_pixels();
+ // let fonts = cx.platform().fonts();
+ // let text_style = cx.text_style();
+ // let line_height = cx.font_cache().line_height(text_style.font_size);
+ // let layout_engine = cx.layout_engine().expect("no layout engine present");
+ // let text = self.text.clone();
+ // let layout = Arc::new(Mutex::new(None));
+
+ // let style: Style = Style::default().refined(&self.metadata.style);
+ // let node_id = layout_engine.add_measured_node(style.to_taffy(rem_size), {
+ // let layout = layout.clone();
+ // move |params| {
+ // let line_layout = fonts.layout_line(
+ // text.as_ref(),
+ // text_style.font_size,
+ // &[(text.len(), text_style.to_run())],
+ // );
+
+ // let size = Size {
+ // width: line_layout.width,
+ // height: line_height,
+ // };
+
+ // layout.lock().replace(TextLayout {
+ // line_layout: Arc::new(line_layout),
+ // line_height,
+ // });
+
+ // size
+ // }
+ // })?;
+
+ // Ok((node_id, layout))
+ todo!()
+ }
+
+ fn paint<'a>(
+ &mut self,
+ view: &mut V,
+ layout: &mut Layout<V, Self::Layout>,
+ cx: &mut PaintContext<V>,
+ ) {
+ // ) {
+ // let element_layout_lock = layout.from_element.lock();
+ // let element_layout = element_layout_lock
+ // .as_ref()
+ // .expect("layout has not been performed");
+ // let line_layout = element_layout.line_layout.clone();
+ // let line_height = element_layout.line_height;
+ // drop(element_layout_lock);
+
+ // let text_style = cx.text_style();
+ // let line =
+ // gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
+ // line.paint(
+ // cx.scene,
+ // layout.from_engine.bounds.origin(),
+ // layout.from_engine.bounds,
+ // line_height,
+ // cx.legacy_cx,
+ // );
+ todo!()
+ }
+}
+
+pub struct TextLayout {
+ line_layout: Arc<LineLayout>,
+ line_height: f32,
+}
+
+pub enum ArcCow<'a, T: ?Sized> {
+ Borrowed(&'a T),
+ Owned(Arc<T>),
+}
+
+impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
+ fn clone(&self) -> Self {
+ match self {
+ Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
+ Self::Owned(owned) => Self::Owned(owned.clone()),
+ }
+ }
+}
+
+impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
+ fn from(s: &'a T) -> Self {
+ Self::Borrowed(s)
+ }
+}
+
+impl<T> From<Arc<T>> for ArcCow<'_, T> {
+ fn from(s: Arc<T>) -> Self {
+ Self::Owned(s)
+ }
+}
+
+impl From<String> for ArcCow<'_, str> {
+ fn from(value: String) -> Self {
+ Self::Owned(value.into())
+ }
+}
+
+impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ match self {
+ ArcCow::Borrowed(s) => s,
+ ArcCow::Owned(s) => s.as_ref(),
+ }
+ }
+}
+
+impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
+ fn as_ref(&self) -> &T {
+ match self {
+ ArcCow::Borrowed(borrowed) => borrowed,
+ ArcCow::Owned(owned) => owned.as_ref(),
+ }
+ }
+}
@@ -0,0 +1,84 @@
+use crate::color::{Hsla, Lerp};
+use std::ops::Range;
+
+pub mod rose_pine;
+
+pub struct ThemeColors {
+ pub base: Range<Hsla>,
+ pub surface: Range<Hsla>,
+ pub overlay: Range<Hsla>,
+ pub muted: Range<Hsla>,
+ pub subtle: Range<Hsla>,
+ pub text: Range<Hsla>,
+ pub highlight_low: Range<Hsla>,
+ pub highlight_med: Range<Hsla>,
+ pub highlight_high: Range<Hsla>,
+ pub success: Range<Hsla>,
+ pub warning: Range<Hsla>,
+ pub error: Range<Hsla>,
+ pub inserted: Range<Hsla>,
+ pub deleted: Range<Hsla>,
+ pub modified: Range<Hsla>,
+}
+
+impl ThemeColors {
+ pub fn base(&self, level: f32) -> Hsla {
+ self.base.lerp(level)
+ }
+
+ pub fn surface(&self, level: f32) -> Hsla {
+ self.surface.lerp(level)
+ }
+
+ pub fn overlay(&self, level: f32) -> Hsla {
+ self.overlay.lerp(level)
+ }
+
+ pub fn muted(&self, level: f32) -> Hsla {
+ self.muted.lerp(level)
+ }
+
+ pub fn subtle(&self, level: f32) -> Hsla {
+ self.subtle.lerp(level)
+ }
+
+ pub fn text(&self, level: f32) -> Hsla {
+ self.text.lerp(level)
+ }
+
+ pub fn highlight_low(&self, level: f32) -> Hsla {
+ self.highlight_low.lerp(level)
+ }
+
+ pub fn highlight_med(&self, level: f32) -> Hsla {
+ self.highlight_med.lerp(level)
+ }
+
+ pub fn highlight_high(&self, level: f32) -> Hsla {
+ self.highlight_high.lerp(level)
+ }
+
+ pub fn success(&self, level: f32) -> Hsla {
+ self.success.lerp(level)
+ }
+
+ pub fn warning(&self, level: f32) -> Hsla {
+ self.warning.lerp(level)
+ }
+
+ pub fn error(&self, level: f32) -> Hsla {
+ self.error.lerp(level)
+ }
+
+ pub fn inserted(&self, level: f32) -> Hsla {
+ self.inserted.lerp(level)
+ }
+
+ pub fn deleted(&self, level: f32) -> Hsla {
+ self.deleted.lerp(level)
+ }
+
+ pub fn modified(&self, level: f32) -> Hsla {
+ self.modified.lerp(level)
+ }
+}
@@ -0,0 +1,133 @@
+use std::ops::Range;
+
+use crate::{
+ color::{hsla, rgb, Hsla},
+ ThemeColors,
+};
+
+pub struct RosePineThemes {
+ pub default: RosePinePalette,
+ pub dawn: RosePinePalette,
+ pub moon: RosePinePalette,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct RosePinePalette {
+ pub base: Hsla,
+ pub surface: Hsla,
+ pub overlay: Hsla,
+ pub muted: Hsla,
+ pub subtle: Hsla,
+ pub text: Hsla,
+ pub love: Hsla,
+ pub gold: Hsla,
+ pub rose: Hsla,
+ pub pine: Hsla,
+ pub foam: Hsla,
+ pub iris: Hsla,
+ pub highlight_low: Hsla,
+ pub highlight_med: Hsla,
+ pub highlight_high: Hsla,
+}
+
+impl RosePinePalette {
+ pub fn default() -> RosePinePalette {
+ RosePinePalette {
+ base: rgb(0x191724),
+ surface: rgb(0x1f1d2e),
+ overlay: rgb(0x26233a),
+ muted: rgb(0x6e6a86),
+ subtle: rgb(0x908caa),
+ text: rgb(0xe0def4),
+ love: rgb(0xeb6f92),
+ gold: rgb(0xf6c177),
+ rose: rgb(0xebbcba),
+ pine: rgb(0x31748f),
+ foam: rgb(0x9ccfd8),
+ iris: rgb(0xc4a7e7),
+ highlight_low: rgb(0x21202e),
+ highlight_med: rgb(0x403d52),
+ highlight_high: rgb(0x524f67),
+ }
+ }
+
+ pub fn moon() -> RosePinePalette {
+ RosePinePalette {
+ base: rgb(0x232136),
+ surface: rgb(0x2a273f),
+ overlay: rgb(0x393552),
+ muted: rgb(0x6e6a86),
+ subtle: rgb(0x908caa),
+ text: rgb(0xe0def4),
+ love: rgb(0xeb6f92),
+ gold: rgb(0xf6c177),
+ rose: rgb(0xea9a97),
+ pine: rgb(0x3e8fb0),
+ foam: rgb(0x9ccfd8),
+ iris: rgb(0xc4a7e7),
+ highlight_low: rgb(0x2a283e),
+ highlight_med: rgb(0x44415a),
+ highlight_high: rgb(0x56526e),
+ }
+ }
+
+ pub fn dawn() -> RosePinePalette {
+ RosePinePalette {
+ base: rgb(0xfaf4ed),
+ surface: rgb(0xfffaf3),
+ overlay: rgb(0xf2e9e1),
+ muted: rgb(0x9893a5),
+ subtle: rgb(0x797593),
+ text: rgb(0x575279),
+ love: rgb(0xb4637a),
+ gold: rgb(0xea9d34),
+ rose: rgb(0xd7827e),
+ pine: rgb(0x286983),
+ foam: rgb(0x56949f),
+ iris: rgb(0x907aa9),
+ highlight_low: rgb(0xf4ede8),
+ highlight_med: rgb(0xdfdad9),
+ highlight_high: rgb(0xcecacd),
+ }
+ }
+}
+
+pub fn default() -> ThemeColors {
+ theme_colors(&RosePinePalette::default())
+}
+
+pub fn moon() -> ThemeColors {
+ theme_colors(&RosePinePalette::moon())
+}
+
+pub fn dawn() -> ThemeColors {
+ theme_colors(&RosePinePalette::dawn())
+}
+
+fn theme_colors(p: &RosePinePalette) -> ThemeColors {
+ ThemeColors {
+ base: scale_sl(p.base, (0.8, 0.8), (1.2, 1.2)),
+ surface: scale_sl(p.surface, (0.8, 0.8), (1.2, 1.2)),
+ overlay: scale_sl(p.overlay, (0.8, 0.8), (1.2, 1.2)),
+ muted: scale_sl(p.muted, (0.8, 0.8), (1.2, 1.2)),
+ subtle: scale_sl(p.subtle, (0.8, 0.8), (1.2, 1.2)),
+ text: scale_sl(p.text, (0.8, 0.8), (1.2, 1.2)),
+ highlight_low: scale_sl(p.highlight_low, (0.8, 0.8), (1.2, 1.2)),
+ highlight_med: scale_sl(p.highlight_med, (0.8, 0.8), (1.2, 1.2)),
+ highlight_high: scale_sl(p.highlight_high, (0.8, 0.8), (1.2, 1.2)),
+ success: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
+ warning: scale_sl(p.gold, (0.8, 0.8), (1.2, 1.2)),
+ error: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
+ inserted: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
+ deleted: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
+ modified: scale_sl(p.rose, (0.8, 0.8), (1.2, 1.2)),
+ }
+}
+
+/// Produces a range by multiplying the saturation and lightness of the base color by the given
+/// start and end factors.
+fn scale_sl(base: Hsla, (start_s, start_l): (f32, f32), (end_s, end_l): (f32, f32)) -> Range<Hsla> {
+ let start = hsla(base.h, base.s * start_s, base.l * start_l, base.a);
+ let end = hsla(base.h, base.s * end_s, base.l * end_l, base.a);
+ Range { start, end }
+}
@@ -0,0 +1,26 @@
+use crate::{
+ adapter::AdapterElement,
+ element::{AnyElement, Element},
+};
+use gpui::ViewContext;
+
+pub fn view<F, E>(mut render: F) -> ViewFn
+where
+ F: 'static + FnMut(&mut ViewContext<ViewFn>) -> E,
+ E: Element<ViewFn>,
+{
+ ViewFn(Box::new(move |cx| (render)(cx).into_any()))
+}
+
+pub struct ViewFn(Box<dyn FnMut(&mut ViewContext<ViewFn>) -> AnyElement<ViewFn>>);
+
+impl gpui::Entity for ViewFn {
+ type Event = ();
+}
+
+impl gpui::View for ViewFn {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
+ use gpui::Element as _;
+ AdapterElement((self.0)(cx)).into_any()
+ }
+}
@@ -0,0 +1,14 @@
+[package]
+name = "playground_macros"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/playground_macros.rs"
+proc-macro = true
+
+[dependencies]
+syn = "1.0.72"
+quote = "1.0.9"
+proc-macro2 = "1.0.66"
@@ -0,0 +1,91 @@
+use proc_macro::TokenStream;
+use proc_macro2::Ident;
+use quote::quote;
+use syn::{parse_macro_input, parse_quote, DeriveInput, GenericParam, Generics};
+
+use crate::derive_into_element::impl_into_element;
+
+pub fn derive_element(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as DeriveInput);
+ let type_name = ast.ident;
+ let placeholder_view_generics: Generics = parse_quote! { <V: 'static> };
+
+ let (impl_generics, type_generics, where_clause, view_type_name, lifetimes) =
+ if let Some(first_type_param) = ast.generics.params.iter().find_map(|param| {
+ if let GenericParam::Type(type_param) = param {
+ Some(type_param.ident.clone())
+ } else {
+ None
+ }
+ }) {
+ let mut lifetimes = vec![];
+ for param in ast.generics.params.iter() {
+ if let GenericParam::Lifetime(lifetime_def) = param {
+ lifetimes.push(lifetime_def.lifetime.clone());
+ }
+ }
+ let generics = ast.generics.split_for_impl();
+ (
+ generics.0,
+ Some(generics.1),
+ generics.2,
+ first_type_param,
+ lifetimes,
+ )
+ } else {
+ let generics = placeholder_view_generics.split_for_impl();
+ let placeholder_view_type_name: Ident = parse_quote! { V };
+ (
+ generics.0,
+ None,
+ generics.2,
+ placeholder_view_type_name,
+ vec![],
+ )
+ };
+
+ let lifetimes = if !lifetimes.is_empty() {
+ quote! { <#(#lifetimes),*> }
+ } else {
+ quote! {}
+ };
+
+ let impl_into_element = impl_into_element(
+ &impl_generics,
+ &view_type_name,
+ &type_name,
+ &type_generics,
+ &where_clause,
+ );
+
+ let gen = quote! {
+ impl #impl_generics playground::element::Element<#view_type_name> for #type_name #type_generics
+ #where_clause
+ {
+ type Layout = Option<playground::element::AnyElement<#view_type_name #lifetimes>>;
+
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut playground::element::LayoutContext<V>,
+ ) -> anyhow::Result<playground::element::Layout<V, Self::Layout>> {
+ let mut element = self.render(view, cx).into_any();
+ let layout_id = element.layout(view, cx)?;
+ Ok(playground::element::Layout::new(layout_id, Some(element)))
+ }
+
+ fn paint(
+ &mut self,
+ view: &mut V,
+ layout: &mut playground::element::Layout<V, Self::Layout>,
+ cx: &mut playground::element::PaintContext<V>,
+ ) {
+ layout.paint(view, cx);
+ }
+ }
+
+ #impl_into_element
+ };
+
+ gen.into()
+}
@@ -0,0 +1,69 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{
+ parse_macro_input, parse_quote, DeriveInput, GenericParam, Generics, Ident, WhereClause,
+};
+
+pub fn derive_into_element(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as DeriveInput);
+ let type_name = ast.ident;
+
+ let placeholder_view_generics: Generics = parse_quote! { <V: 'static> };
+ let placeholder_view_type_name: Ident = parse_quote! { V };
+ let view_type_name: Ident;
+ let impl_generics: syn::ImplGenerics<'_>;
+ let type_generics: Option<syn::TypeGenerics<'_>>;
+ let where_clause: Option<&'_ WhereClause>;
+
+ match ast.generics.params.iter().find_map(|param| {
+ if let GenericParam::Type(type_param) = param {
+ Some(type_param.ident.clone())
+ } else {
+ None
+ }
+ }) {
+ Some(type_name) => {
+ view_type_name = type_name;
+ let generics = ast.generics.split_for_impl();
+ impl_generics = generics.0;
+ type_generics = Some(generics.1);
+ where_clause = generics.2;
+ }
+ _ => {
+ view_type_name = placeholder_view_type_name;
+ let generics = placeholder_view_generics.split_for_impl();
+ impl_generics = generics.0;
+ type_generics = None;
+ where_clause = generics.2;
+ }
+ }
+
+ impl_into_element(
+ &impl_generics,
+ &view_type_name,
+ &type_name,
+ &type_generics,
+ &where_clause,
+ )
+ .into()
+}
+
+pub fn impl_into_element(
+ impl_generics: &syn::ImplGenerics<'_>,
+ view_type_name: &Ident,
+ type_name: &Ident,
+ type_generics: &Option<syn::TypeGenerics<'_>>,
+ where_clause: &Option<&WhereClause>,
+) -> proc_macro2::TokenStream {
+ quote! {
+ impl #impl_generics playground::element::IntoElement<#view_type_name> for #type_name #type_generics
+ #where_clause
+ {
+ type Element = Self;
+
+ fn into_element(self) -> Self {
+ self
+ }
+ }
+ }
+}
@@ -0,0 +1,26 @@
+use proc_macro::TokenStream;
+
+mod derive_element;
+mod derive_into_element;
+mod styleable_helpers;
+mod tailwind_lengths;
+
+#[proc_macro]
+pub fn styleable_helpers(args: TokenStream) -> TokenStream {
+ styleable_helpers::styleable_helpers(args)
+}
+
+#[proc_macro_derive(Element, attributes(element_crate))]
+pub fn derive_element(input: TokenStream) -> TokenStream {
+ derive_element::derive_element(input)
+}
+
+#[proc_macro_derive(IntoElement, attributes(element_crate))]
+pub fn derive_into_element(input: TokenStream) -> TokenStream {
+ derive_into_element::derive_into_element(input)
+}
+
+#[proc_macro_attribute]
+pub fn tailwind_lengths(attr: TokenStream, item: TokenStream) -> TokenStream {
+ tailwind_lengths::tailwind_lengths(attr, item)
+}
@@ -0,0 +1,147 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{
+ parse::{Parse, ParseStream, Result},
+ parse_macro_input,
+};
+
+struct StyleableMacroInput;
+
+impl Parse for StyleableMacroInput {
+ fn parse(_input: ParseStream) -> Result<Self> {
+ Ok(StyleableMacroInput)
+ }
+}
+
+pub fn styleable_helpers(input: TokenStream) -> TokenStream {
+ let _ = parse_macro_input!(input as StyleableMacroInput);
+ let methods = generate_methods();
+ let output = quote! {
+ #(#methods)*
+ };
+ output.into()
+}
+
+fn generate_methods() -> Vec<TokenStream2> {
+ let mut methods = Vec::new();
+
+ for (prefix, auto_allowed, fields) in tailwind_prefixes() {
+ for (suffix, length_tokens) in tailwind_lengths() {
+ if !auto_allowed && suffix == "auto" {
+ // Conditional to skip "auto"
+ continue;
+ }
+
+ let method_name = format_ident!("{}_{}", prefix, suffix);
+ let field_assignments = fields
+ .iter()
+ .map(|field_tokens| {
+ quote! {
+ style.#field_tokens = Some(gpui::geometry::#length_tokens);
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let method = quote! {
+ fn #method_name(mut self) -> Self where Self: std::marker::Sized {
+ let mut style = self.declared_style();
+ #(#field_assignments)*
+ self
+ }
+ };
+
+ methods.push(method);
+ }
+ }
+
+ methods
+}
+
+fn tailwind_lengths() -> Vec<(&'static str, TokenStream2)> {
+ vec![
+ ("0", quote! { pixels(0.) }),
+ ("1", quote! { rems(0.25) }),
+ ("2", quote! { rems(0.5) }),
+ ("3", quote! { rems(0.75) }),
+ ("4", quote! { rems(1.) }),
+ ("5", quote! { rems(1.25) }),
+ ("6", quote! { rems(1.5) }),
+ ("8", quote! { rems(2.0) }),
+ ("10", quote! { rems(2.5) }),
+ ("12", quote! { rems(3.) }),
+ ("16", quote! { rems(4.) }),
+ ("20", quote! { rems(5.) }),
+ ("24", quote! { rems(6.) }),
+ ("32", quote! { rems(8.) }),
+ ("40", quote! { rems(10.) }),
+ ("48", quote! { rems(12.) }),
+ ("56", quote! { rems(14.) }),
+ ("64", quote! { rems(16.) }),
+ ("72", quote! { rems(18.) }),
+ ("80", quote! { rems(20.) }),
+ ("96", quote! { rems(24.) }),
+ ("auto", quote! { auto() }),
+ ("px", quote! { pixels(1.) }),
+ ("full", quote! { relative(1.) }),
+ ("1_2", quote! { relative(0.5) }),
+ ("1_3", quote! { relative(1./3.) }),
+ ("2_3", quote! { relative(2./3.) }),
+ ("1_4", quote! { relative(0.25) }),
+ ("2_4", quote! { relative(0.5) }),
+ ("3_4", quote! { relative(0.75) }),
+ ("1_5", quote! { relative(0.2) }),
+ ("2_5", quote! { relative(0.4) }),
+ ("3_5", quote! { relative(0.6) }),
+ ("4_5", quote! { relative(0.8) }),
+ ("1_6", quote! { relative(1./6.) }),
+ ("5_6", quote! { relative(5./6.) }),
+ ("1_12", quote! { relative(1./12.) }),
+ // ("screen_50", quote! { DefiniteLength::Vh(50.0) }),
+ // ("screen_75", quote! { DefiniteLength::Vh(75.0) }),
+ // ("screen", quote! { DefiniteLength::Vh(100.0) }),
+ ]
+}
+
+fn tailwind_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
+ vec![
+ ("w", true, vec![quote! { size.width }]),
+ ("h", true, vec![quote! { size.height }]),
+ ("min_w", false, vec![quote! { min_size.width }]),
+ ("min_h", false, vec![quote! { min_size.height }]),
+ ("max_w", false, vec![quote! { max_size.width }]),
+ ("max_h", false, vec![quote! { max_size.height }]),
+ (
+ "m",
+ true,
+ vec![quote! { margin.top }, quote! { margin.bottom }],
+ ),
+ ("mt", true, vec![quote! { margin.top }]),
+ ("mb", true, vec![quote! { margin.bottom }]),
+ (
+ "mx",
+ true,
+ vec![quote! { margin.left }, quote! { margin.right }],
+ ),
+ ("ml", true, vec![quote! { margin.left }]),
+ ("mr", true, vec![quote! { margin.right }]),
+ (
+ "p",
+ false,
+ vec![quote! { padding.top }, quote! { padding.bottom }],
+ ),
+ ("pt", false, vec![quote! { padding.top }]),
+ ("pb", false, vec![quote! { padding.bottom }]),
+ (
+ "px",
+ false,
+ vec![quote! { padding.left }, quote! { padding.right }],
+ ),
+ ("pl", false, vec![quote! { padding.left }]),
+ ("pr", false, vec![quote! { padding.right }]),
+ ("top", true, vec![quote! { inset.top }]),
+ ("bottom", true, vec![quote! { inset.bottom }]),
+ ("left", true, vec![quote! { inset.left }]),
+ ("right", true, vec![quote! { inset.right }]),
+ ]
+}
@@ -0,0 +1,99 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{parse_macro_input, FnArg, ItemFn, PatType};
+
+pub fn tailwind_lengths(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ let input_function = parse_macro_input!(item as ItemFn);
+
+ let visibility = &input_function.vis;
+ let function_signature = input_function.sig.clone();
+ let function_body = input_function.block;
+ let where_clause = &function_signature.generics.where_clause;
+
+ let argument_name = match function_signature.inputs.iter().nth(1) {
+ Some(FnArg::Typed(PatType { pat, .. })) => pat,
+ _ => panic!("Couldn't find the second argument in the function signature"),
+ };
+
+ let mut output_functions = TokenStream2::new();
+
+ for (length, value) in fixed_lengths() {
+ let function_name = format_ident!("{}{}", function_signature.ident, length);
+ output_functions.extend(quote! {
+ #visibility fn #function_name(mut self) -> Self #where_clause {
+ let #argument_name = #value.into();
+ #function_body
+ }
+ });
+ }
+
+ output_functions.into()
+}
+
+fn fixed_lengths() -> Vec<(&'static str, TokenStream2)> {
+ vec![
+ ("0", quote! { DefinedLength::Pixels(0.) }),
+ ("px", quote! { DefinedLength::Pixels(1.) }),
+ ("0_5", quote! { DefinedLength::Rems(0.125) }),
+ ("1", quote! { DefinedLength::Rems(0.25) }),
+ ("1_5", quote! { DefinedLength::Rems(0.375) }),
+ ("2", quote! { DefinedLength::Rems(0.5) }),
+ ("2_5", quote! { DefinedLength::Rems(0.625) }),
+ ("3", quote! { DefinedLength::Rems(0.75) }),
+ ("3_5", quote! { DefinedLength::Rems(0.875) }),
+ ("4", quote! { DefinedLength::Rems(1.) }),
+ ("5", quote! { DefinedLength::Rems(1.25) }),
+ ("6", quote! { DefinedLength::Rems(1.5) }),
+ ("7", quote! { DefinedLength::Rems(1.75) }),
+ ("8", quote! { DefinedLength::Rems(2.) }),
+ ("9", quote! { DefinedLength::Rems(2.25) }),
+ ("10", quote! { DefinedLength::Rems(2.5) }),
+ ("11", quote! { DefinedLength::Rems(2.75) }),
+ ("12", quote! { DefinedLength::Rems(3.) }),
+ ("14", quote! { DefinedLength::Rems(3.5) }),
+ ("16", quote! { DefinedLength::Rems(4.) }),
+ ("20", quote! { DefinedLength::Rems(5.) }),
+ ("24", quote! { DefinedLength::Rems(6.) }),
+ ("28", quote! { DefinedLength::Rems(7.) }),
+ ("32", quote! { DefinedLength::Rems(8.) }),
+ ("36", quote! { DefinedLength::Rems(9.) }),
+ ("40", quote! { DefinedLength::Rems(10.) }),
+ ("44", quote! { DefinedLength::Rems(11.) }),
+ ("48", quote! { DefinedLength::Rems(12.) }),
+ ("52", quote! { DefinedLength::Rems(13.) }),
+ ("56", quote! { DefinedLength::Rems(14.) }),
+ ("60", quote! { DefinedLength::Rems(15.) }),
+ ("64", quote! { DefinedLength::Rems(16.) }),
+ ("72", quote! { DefinedLength::Rems(18.) }),
+ ("80", quote! { DefinedLength::Rems(20.) }),
+ ("96", quote! { DefinedLength::Rems(24.) }),
+ ("half", quote! { DefinedLength::Percent(50.) }),
+ ("1_3rd", quote! { DefinedLength::Percent(33.333333) }),
+ ("2_3rd", quote! { DefinedLength::Percent(66.666667) }),
+ ("1_4th", quote! { DefinedLength::Percent(25.) }),
+ ("2_4th", quote! { DefinedLength::Percent(50.) }),
+ ("3_4th", quote! { DefinedLength::Percent(75.) }),
+ ("1_5th", quote! { DefinedLength::Percent(20.) }),
+ ("2_5th", quote! { DefinedLength::Percent(40.) }),
+ ("3_5th", quote! { DefinedLength::Percent(60.) }),
+ ("4_5th", quote! { DefinedLength::Percent(80.) }),
+ ("1_6th", quote! { DefinedLength::Percent(16.666667) }),
+ ("2_6th", quote! { DefinedLength::Percent(33.333333) }),
+ ("3_6th", quote! { DefinedLength::Percent(50.) }),
+ ("4_6th", quote! { DefinedLength::Percent(66.666667) }),
+ ("5_6th", quote! { DefinedLength::Percent(83.333333) }),
+ ("1_12th", quote! { DefinedLength::Percent(8.333333) }),
+ ("2_12th", quote! { DefinedLength::Percent(16.666667) }),
+ ("3_12th", quote! { DefinedLength::Percent(25.) }),
+ ("4_12th", quote! { DefinedLength::Percent(33.333333) }),
+ ("5_12th", quote! { DefinedLength::Percent(41.666667) }),
+ ("6_12th", quote! { DefinedLength::Percent(50.) }),
+ ("7_12th", quote! { DefinedLength::Percent(58.333333) }),
+ ("8_12th", quote! { DefinedLength::Percent(66.666667) }),
+ ("9_12th", quote! { DefinedLength::Percent(75.) }),
+ ("10_12th", quote! { DefinedLength::Percent(83.333333) }),
+ ("11_12th", quote! { DefinedLength::Percent(91.666667) }),
+ ("full", quote! { DefinedLength::Percent(100.) }),
+ ]
+}
@@ -7,6 +7,34 @@ pub mod test_app_context;
pub(crate) mod window;
mod window_input_handler;
+use crate::{
+ elements::{AnyElement, AnyRootElement, RootElement},
+ executor::{self, Task},
+ fonts::TextStyle,
+ json,
+ keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
+ platform::{
+ self, FontSystem, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton,
+ PathPromptOptions, Platform, PromptLevel, WindowBounds, WindowOptions,
+ },
+ util::post_inc,
+ window::{Window, WindowContext},
+ AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId,
+};
+pub use action::*;
+use anyhow::{anyhow, Context, Result};
+use callback_collection::CallbackCollection;
+use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
+use derive_more::Deref;
+pub use menu::*;
+use parking_lot::Mutex;
+use platform::Event;
+use postage::oneshot;
+#[cfg(any(test, feature = "test-support"))]
+use ref_counts::LeakDetector;
+use ref_counts::RefCounts;
+use smallvec::SmallVec;
+use smol::prelude::*;
use std::{
any::{type_name, Any, TypeId},
cell::RefCell,
@@ -21,45 +49,12 @@ use std::{
sync::{Arc, Weak},
time::Duration,
};
-
-use anyhow::{anyhow, Context, Result};
-
-use derive_more::Deref;
-use parking_lot::Mutex;
-use postage::oneshot;
-use smallvec::SmallVec;
-use smol::prelude::*;
-use util::ResultExt;
-use uuid::Uuid;
-
-pub use action::*;
-use callback_collection::CallbackCollection;
-use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
-pub use menu::*;
-use platform::Event;
-#[cfg(any(test, feature = "test-support"))]
-use ref_counts::LeakDetector;
#[cfg(any(test, feature = "test-support"))]
pub use test_app_context::{ContextHandle, TestAppContext};
+use util::ResultExt;
+use uuid::Uuid;
use window_input_handler::WindowInputHandler;
-use crate::{
- elements::{AnyElement, AnyRootElement, RootElement},
- executor::{self, Task},
- fonts::TextStyle,
- json,
- keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
- platform::{
- self, FontSystem, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton,
- PathPromptOptions, Platform, PromptLevel, WindowBounds, WindowOptions,
- },
- util::post_inc,
- window::{Window, WindowContext},
- AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId,
-};
-
-use self::ref_counts::RefCounts;
-
pub trait Entity: 'static {
type Event;
@@ -73,10 +68,12 @@ pub trait Entity: 'static {
}
pub trait View: Entity + Sized {
- fn ui_name() -> &'static str;
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self>;
fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
+ fn ui_name() -> &'static str {
+ type_name::<Self>()
+ }
fn key_down(&mut self, _: &KeyDownEvent, _: &mut ViewContext<Self>) -> bool {
false
}
@@ -640,7 +637,7 @@ impl AppContext {
pub fn add_action<A, V, F, R>(&mut self, handler: F)
where
A: Action,
- V: View,
+ V: 'static,
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
{
self.add_action_internal(handler, false)
@@ -649,7 +646,7 @@ impl AppContext {
pub fn capture_action<A, V, F>(&mut self, handler: F)
where
A: Action,
- V: View,
+ V: 'static,
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
{
self.add_action_internal(handler, true)
@@ -658,7 +655,7 @@ impl AppContext {
fn add_action_internal<A, V, F, R>(&mut self, mut handler: F, capture: bool)
where
A: Action,
- V: View,
+ V: 'static,
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
{
let handler = Box::new(
@@ -699,7 +696,7 @@ impl AppContext {
pub fn add_async_action<A, V, F>(&mut self, mut handler: F)
where
A: Action,
- V: View,
+ V: 'static,
F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> Option<Task<Result<()>>>,
{
self.add_action(move |view, action, cx| {
@@ -898,8 +895,8 @@ impl AppContext {
fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
where
+ V: 'static,
F: 'static + FnMut(ViewHandle<V>, bool, &mut WindowContext) -> bool,
- V: View,
{
let subscription_id = post_inc(&mut self.next_subscription_id);
let observed = handle.downgrade();
@@ -1382,15 +1379,15 @@ impl AppContext {
self.windows.keys().copied()
}
- pub fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
+ pub fn read_view<V: 'static>(&self, handle: &ViewHandle<V>) -> &V {
if let Some(view) = self.views.get(&(handle.window, handle.view_id)) {
view.as_any().downcast_ref().expect("downcast is type safe")
} else {
- panic!("circular view reference for type {}", type_name::<T>());
+ panic!("circular view reference for type {}", type_name::<V>());
}
}
- fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
+ fn upgrade_view_handle<V: 'static>(&self, handle: &WeakViewHandle<V>) -> Option<ViewHandle<V>> {
if self.ref_counts.lock().is_entity_alive(handle.view_id) {
Some(ViewHandle::new(
handle.window,
@@ -1659,6 +1656,9 @@ impl AppContext {
subscription_id,
callback,
),
+ Effect::RepaintWindow { window } => {
+ self.handle_repaint_window_effect(window)
+ }
}
self.pending_notifications.clear();
} else {
@@ -1896,6 +1896,15 @@ impl AppContext {
});
}
+ fn handle_repaint_window_effect(&mut self, window: AnyWindowHandle) {
+ self.update_window(window, |cx| {
+ cx.layout(false).log_err();
+ if let Some(scene) = cx.paint().log_err() {
+ cx.window.platform_window.present_scene(scene);
+ }
+ });
+ }
+
fn handle_window_activation_effect(&mut self, window: AnyWindowHandle, active: bool) -> bool {
self.update_window(window, |cx| {
if cx.window.is_active == active {
@@ -2151,7 +2160,7 @@ struct ViewMetadata {
keymap_context: KeymapContext,
}
-#[derive(Default, Clone)]
+#[derive(Default, Clone, Debug)]
pub struct WindowInvalidation {
pub updated: HashSet<usize>,
pub removed: Vec<usize>,
@@ -2255,6 +2264,9 @@ pub enum Effect {
window: AnyWindowHandle,
is_active: bool,
},
+ RepaintWindow {
+ window: AnyWindowHandle,
+ },
WindowActivationObservation {
window: AnyWindowHandle,
subscription_id: usize,
@@ -2448,6 +2460,10 @@ impl Debug for Effect {
.debug_struct("Effect::ActiveLabeledTasksObservation")
.field("subscription_id", subscription_id)
.finish(),
+ Effect::RepaintWindow { window } => f
+ .debug_struct("Effect::RepaintWindow")
+ .field("window_id", &window.id())
+ .finish(),
}
}
}
@@ -2543,10 +2559,7 @@ pub trait AnyView {
}
}
-impl<V> AnyView for V
-where
- V: View,
-{
+impl<V: View> AnyView for V {
fn as_any(&self) -> &dyn Any {
self
}
@@ -2878,7 +2891,7 @@ pub struct ViewContext<'a, 'b, T: ?Sized> {
view_type: PhantomData<T>,
}
-impl<'a, 'b, T: View> Deref for ViewContext<'a, 'b, T> {
+impl<'a, 'b, V> Deref for ViewContext<'a, 'b, V> {
type Target = WindowContext<'a>;
fn deref(&self) -> &Self::Target {
@@ -2886,14 +2899,14 @@ impl<'a, 'b, T: View> Deref for ViewContext<'a, 'b, T> {
}
}
-impl<T: View> DerefMut for ViewContext<'_, '_, T> {
+impl<'a, 'b, V> DerefMut for ViewContext<'a, 'b, V> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.window_context
}
}
-impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
- pub(crate) fn mutable(window_context: &'b mut WindowContext<'a>, view_id: usize) -> Self {
+impl<'a, 'b, V: 'static> ViewContext<'a, 'b, V> {
+ pub fn mutable(window_context: &'b mut WindowContext<'a>, view_id: usize) -> Self {
Self {
window_context: Reference::Mutable(window_context),
view_id,
@@ -2901,7 +2914,7 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
}
}
- pub(crate) fn immutable(window_context: &'b WindowContext<'a>, view_id: usize) -> Self {
+ pub fn immutable(window_context: &'b WindowContext<'a>, view_id: usize) -> Self {
Self {
window_context: Reference::Immutable(window_context),
view_id,
@@ -2913,6 +2926,12 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
&mut self.window_context
}
+ pub fn notify(&mut self) {
+ let window = self.window_handle;
+ let view_id = self.view_id;
+ self.window_context.notify_view(window, view_id);
+ }
+
pub fn handle(&self) -> ViewHandle<V> {
ViewHandle::new(
self.window_handle,
@@ -3226,21 +3245,6 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
})
}
- pub fn emit(&mut self, payload: V::Event) {
- self.window_context
- .pending_effects
- .push_back(Effect::Event {
- entity_id: self.view_id,
- payload: Box::new(payload),
- });
- }
-
- pub fn notify(&mut self) {
- let window = self.window_handle;
- let view_id = self.view_id;
- self.window_context.notify_view(window, view_id);
- }
-
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext<V>)) {
let handle = self.handle();
self.window_context
@@ -3295,15 +3299,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
let region_id = MouseRegionId::new(tag, self.view_id, region_id);
MouseState {
hovered: self.window.hovered_region_ids.contains(®ion_id),
- clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
- if region_id == clicked_region_id {
- Some(button)
- } else {
- None
- }
- } else {
- None
- },
+ mouse_down: !self.window.clicked_region_ids.is_empty(),
+ clicked: self
+ .window
+ .clicked_region_ids
+ .iter()
+ .find(|click_region_id| **click_region_id == region_id)
+ // If we've gotten here, there should always be a clicked region.
+ // But let's be defensive and return None if there isn't.
+ .and_then(|_| self.window.clicked_region.map(|(_, button)| button)),
accessed_hovered: false,
accessed_clicked: false,
}
@@ -3341,6 +3345,10 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
self.element_state::<Tag, T>(element_id, T::default())
}
+ pub fn rem_pixels(&self) -> f32 {
+ 16.
+ }
+
pub fn default_element_state_dynamic<T: 'static + Default>(
&mut self,
tag: TypeTag,
@@ -3350,6 +3358,17 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
}
}
+impl<V: View> ViewContext<'_, '_, V> {
+ pub fn emit(&mut self, event: V::Event) {
+ self.window_context
+ .pending_effects
+ .push_back(Effect::Event {
+ entity_id: self.view_id,
+ payload: Box::new(event),
+ });
+ }
+}
+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TypeTag {
tag: TypeId,
@@ -3428,15 +3447,27 @@ impl<V> BorrowWindowContext for ViewContext<'_, '_, V> {
}
}
-pub struct LayoutContext<'a, 'b, 'c, V: View> {
- view_context: &'c mut ViewContext<'a, 'b, V>,
+/// Methods shared by both LayoutContext and PaintContext
+///
+/// It's that PaintContext should be implemented in terms of layout context and
+/// deref to it, in which case we wouldn't need this.
+pub trait RenderContext<'a, 'b, V> {
+ fn text_style(&self) -> TextStyle;
+ fn push_text_style(&mut self, style: TextStyle);
+ fn pop_text_style(&mut self);
+ fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V>;
+}
+
+pub struct LayoutContext<'a, 'b, 'c, V> {
+ // Nathan: Making this is public while I work on playground.
+ pub view_context: &'c mut ViewContext<'a, 'b, V>,
new_parents: &'c mut HashMap<usize, usize>,
views_to_notify_if_ancestors_change: &'c mut HashMap<usize, SmallVec<[usize; 2]>>,
- text_style_stack: Vec<Arc<TextStyle>>,
+ text_style_stack: Vec<TextStyle>,
pub refreshing: bool,
}
-impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> LayoutContext<'a, 'b, 'c, V> {
pub fn new(
view_context: &'c mut ViewContext<'a, 'b, V>,
new_parents: &'c mut HashMap<usize, usize>,
@@ -3500,26 +3531,39 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
.push(self_view_id);
}
- pub fn text_style(&self) -> Arc<TextStyle> {
+ pub fn with_text_style<F, T>(&mut self, style: TextStyle, f: F) -> T
+ where
+ F: FnOnce(&mut Self) -> T,
+ {
+ self.push_text_style(style);
+ let result = f(self);
+ self.pop_text_style();
+ result
+ }
+}
+
+impl<'a, 'b, 'c, V> RenderContext<'a, 'b, V> for LayoutContext<'a, 'b, 'c, V> {
+ fn text_style(&self) -> TextStyle {
self.text_style_stack
.last()
.cloned()
- .unwrap_or(Default::default())
+ .unwrap_or(TextStyle::default(&self.font_cache))
}
- pub fn with_text_style<S, F, T>(&mut self, style: S, f: F) -> T
- where
- S: Into<Arc<TextStyle>>,
- F: FnOnce(&mut Self) -> T,
- {
- self.text_style_stack.push(style.into());
- let result = f(self);
+ fn push_text_style(&mut self, style: TextStyle) {
+ self.text_style_stack.push(style);
+ }
+
+ fn pop_text_style(&mut self) {
self.text_style_stack.pop();
- result
+ }
+
+ fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
+ &mut self.view_context
}
}
-impl<'a, 'b, 'c, V: View> Deref for LayoutContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> Deref for LayoutContext<'a, 'b, 'c, V> {
type Target = ViewContext<'a, 'b, V>;
fn deref(&self) -> &Self::Target {
@@ -3527,13 +3571,13 @@ impl<'a, 'b, 'c, V: View> Deref for LayoutContext<'a, 'b, 'c, V> {
}
}
-impl<V: View> DerefMut for LayoutContext<'_, '_, '_, V> {
+impl<V> DerefMut for LayoutContext<'_, '_, '_, V> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.view_context
}
}
-impl<V: View> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
+impl<V> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
BorrowAppContext::read_with(&*self.view_context, f)
}
@@ -3543,7 +3587,7 @@ impl<V: View> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
}
}
-impl<V: View> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
+impl<V> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
type Result<T> = T;
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
@@ -3573,39 +3617,42 @@ impl<V: View> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
}
}
-pub struct PaintContext<'a, 'b, 'c, V: View> {
- view_context: &'c mut ViewContext<'a, 'b, V>,
- text_style_stack: Vec<Arc<TextStyle>>,
+pub struct PaintContext<'a, 'b, 'c, V> {
+ pub view_context: &'c mut ViewContext<'a, 'b, V>,
+ text_style_stack: Vec<TextStyle>,
}
-impl<'a, 'b, 'c, V: View> PaintContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> PaintContext<'a, 'b, 'c, V> {
pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
Self {
view_context,
text_style_stack: Vec::new(),
}
}
+}
- pub fn text_style(&self) -> Arc<TextStyle> {
+impl<'a, 'b, 'c, V> RenderContext<'a, 'b, V> for PaintContext<'a, 'b, 'c, V> {
+ fn text_style(&self) -> TextStyle {
self.text_style_stack
.last()
.cloned()
- .unwrap_or(Default::default())
+ .unwrap_or(TextStyle::default(&self.font_cache))
}
- pub fn with_text_style<S, F, T>(&mut self, style: S, f: F) -> T
- where
- S: Into<Arc<TextStyle>>,
- F: FnOnce(&mut Self) -> T,
- {
- self.text_style_stack.push(style.into());
- let result = f(self);
+ fn push_text_style(&mut self, style: TextStyle) {
+ self.text_style_stack.push(style);
+ }
+
+ fn pop_text_style(&mut self) {
self.text_style_stack.pop();
- result
+ }
+
+ fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
+ &mut self.view_context
}
}
-impl<'a, 'b, 'c, V: View> Deref for PaintContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> Deref for PaintContext<'a, 'b, 'c, V> {
type Target = ViewContext<'a, 'b, V>;
fn deref(&self) -> &Self::Target {
@@ -3613,13 +3660,13 @@ impl<'a, 'b, 'c, V: View> Deref for PaintContext<'a, 'b, 'c, V> {
}
}
-impl<V: View> DerefMut for PaintContext<'_, '_, '_, V> {
+impl<V> DerefMut for PaintContext<'_, '_, '_, V> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.view_context
}
}
-impl<V: View> BorrowAppContext for PaintContext<'_, '_, '_, V> {
+impl<V> BorrowAppContext for PaintContext<'_, '_, '_, V> {
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
BorrowAppContext::read_with(&*self.view_context, f)
}
@@ -3629,7 +3676,7 @@ impl<V: View> BorrowAppContext for PaintContext<'_, '_, '_, V> {
}
}
-impl<V: View> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
+impl<V> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
type Result<T> = T;
fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
@@ -3661,25 +3708,37 @@ impl<V: View> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
}
}
-pub struct EventContext<'a, 'b, 'c, V: View> {
+pub struct EventContext<'a, 'b, 'c, V> {
view_context: &'c mut ViewContext<'a, 'b, V>,
pub(crate) handled: bool,
+ // I would like to replace handled with this.
+ // Being additive for now.
+ pub bubble: bool,
}
-impl<'a, 'b, 'c, V: View> EventContext<'a, 'b, 'c, V> {
- pub(crate) fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
+impl<'a, 'b, 'c, V: 'static> EventContext<'a, 'b, 'c, V> {
+ pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
EventContext {
view_context,
handled: true,
+ bubble: false,
}
}
pub fn propagate_event(&mut self) {
self.handled = false;
}
+
+ pub fn bubble_event(&mut self) {
+ self.bubble = true;
+ }
+
+ pub fn event_bubbled(&self) -> bool {
+ self.bubble
+ }
}
-impl<'a, 'b, 'c, V: View> Deref for EventContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> Deref for EventContext<'a, 'b, 'c, V> {
type Target = ViewContext<'a, 'b, V>;
fn deref(&self) -> &Self::Target {
@@ -3687,13 +3746,13 @@ impl<'a, 'b, 'c, V: View> Deref for EventContext<'a, 'b, 'c, V> {
}
}
-impl<V: View> DerefMut for EventContext<'_, '_, '_, V> {
+impl<V> DerefMut for EventContext<'_, '_, '_, V> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.view_context
}
}
-impl<V: View> BorrowAppContext for EventContext<'_, '_, '_, V> {
+impl<V> BorrowAppContext for EventContext<'_, '_, '_, V> {
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
BorrowAppContext::read_with(&*self.view_context, f)
}
@@ -3703,7 +3762,7 @@ impl<V: View> BorrowAppContext for EventContext<'_, '_, '_, V> {
}
}
-impl<V: View> BorrowWindowContext for EventContext<'_, '_, '_, V> {
+impl<V> BorrowWindowContext for EventContext<'_, '_, '_, V> {
type Result<T> = T;
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
@@ -3764,14 +3823,20 @@ impl<'a, T> DerefMut for Reference<'a, T> {
pub struct MouseState {
pub(crate) hovered: bool,
pub(crate) clicked: Option<MouseButton>,
+ pub(crate) mouse_down: bool,
pub(crate) accessed_hovered: bool,
pub(crate) accessed_clicked: bool,
}
impl MouseState {
+ pub fn dragging(&mut self) -> bool {
+ self.accessed_hovered = true;
+ self.hovered && self.mouse_down
+ }
+
pub fn hovered(&mut self) -> bool {
self.accessed_hovered = true;
- self.hovered
+ self.hovered && (!self.mouse_down || self.clicked.is_some())
}
pub fn clicked(&mut self) -> Option<MouseButton> {
@@ -4031,7 +4096,7 @@ impl<V> Clone for WindowHandle<V> {
impl<V> Copy for WindowHandle<V> {}
-impl<V: View> WindowHandle<V> {
+impl<V: 'static> WindowHandle<V> {
fn new(window_id: usize) -> Self {
WindowHandle {
any_handle: AnyWindowHandle::new(window_id, TypeId::of::<V>()),
@@ -4069,7 +4134,9 @@ impl<V: View> WindowHandle<V> {
.update(cx, update)
})
}
+}
+impl<V: View> WindowHandle<V> {
pub fn replace_root<C, F>(&self, cx: &mut C, build_root: F) -> C::Result<ViewHandle<V>>
where
C: BorrowWindowContext,
@@ -4149,7 +4216,7 @@ impl AnyWindowHandle {
self.update(cx, |cx| cx.add_view(build_view))
}
- pub fn downcast<V: View>(self) -> Option<WindowHandle<V>> {
+ pub fn downcast<V: 'static>(self) -> Option<WindowHandle<V>> {
if self.root_view_type == TypeId::of::<V>() {
Some(WindowHandle {
any_handle: self,
@@ -4160,7 +4227,7 @@ impl AnyWindowHandle {
}
}
- pub fn root_is<V: View>(&self) -> bool {
+ pub fn root_is<V: 'static>(&self) -> bool {
self.root_view_type == TypeId::of::<V>()
}
@@ -4238,9 +4305,9 @@ impl AnyWindowHandle {
}
#[repr(transparent)]
-pub struct ViewHandle<T> {
+pub struct ViewHandle<V> {
any_handle: AnyViewHandle,
- view_type: PhantomData<T>,
+ view_type: PhantomData<V>,
}
impl<T> Deref for ViewHandle<T> {
@@ -4251,15 +4318,15 @@ impl<T> Deref for ViewHandle<T> {
}
}
-impl<T: View> ViewHandle<T> {
+impl<V: 'static> ViewHandle<V> {
fn new(window: AnyWindowHandle, view_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
Self {
- any_handle: AnyViewHandle::new(window, view_id, TypeId::of::<T>(), ref_counts.clone()),
+ any_handle: AnyViewHandle::new(window, view_id, TypeId::of::<V>(), ref_counts.clone()),
view_type: PhantomData,
}
}
- pub fn downgrade(&self) -> WeakViewHandle<T> {
+ pub fn downgrade(&self) -> WeakViewHandle<V> {
WeakViewHandle::new(self.window, self.view_id)
}
@@ -4275,14 +4342,14 @@ impl<T: View> ViewHandle<T> {
self.view_id
}
- pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
+ pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
cx.read_view(self)
}
pub fn read_with<C, F, S>(&self, cx: &C, read: F) -> C::Result<S>
where
C: BorrowWindowContext,
- F: FnOnce(&T, &ViewContext<T>) -> S,
+ F: FnOnce(&V, &ViewContext<V>) -> S,
{
cx.read_window(self.window, |cx| {
let cx = ViewContext::immutable(cx, self.view_id);
@@ -4293,7 +4360,7 @@ impl<T: View> ViewHandle<T> {
pub fn update<C, F, S>(&self, cx: &mut C, update: F) -> C::Result<S>
where
C: BorrowWindowContext,
- F: FnOnce(&mut T, &mut ViewContext<T>) -> S,
+ F: FnOnce(&mut V, &mut ViewContext<V>) -> S,
{
let mut update = Some(update);
@@ -4429,8 +4496,8 @@ impl AnyViewHandle {
TypeId::of::<T>() == self.view_type
}
- pub fn downcast<T: View>(self) -> Option<ViewHandle<T>> {
- if self.is::<T>() {
+ pub fn downcast<V: 'static>(self) -> Option<ViewHandle<V>> {
+ if self.is::<V>() {
Some(ViewHandle {
any_handle: self,
view_type: PhantomData,
@@ -4440,8 +4507,8 @@ impl AnyViewHandle {
}
}
- pub fn downcast_ref<T: View>(&self) -> Option<&ViewHandle<T>> {
- if self.is::<T>() {
+ pub fn downcast_ref<V: 'static>(&self) -> Option<&ViewHandle<V>> {
+ if self.is::<V>() {
Some(unsafe { mem::transmute(self) })
} else {
None
@@ -4620,12 +4687,13 @@ impl AnyWeakModelHandle {
}
}
-#[derive(Copy)]
pub struct WeakViewHandle<T> {
any_handle: AnyWeakViewHandle,
view_type: PhantomData<T>,
}
+impl<T> Copy for WeakViewHandle<T> {}
+
impl<T> Debug for WeakViewHandle<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(&format!("WeakViewHandle<{}>", type_name::<T>()))
@@ -4640,7 +4708,7 @@ impl<T> WeakHandle for WeakViewHandle<T> {
}
}
-impl<V: View> WeakViewHandle<V> {
+impl<V: 'static> WeakViewHandle<V> {
fn new(window: AnyWindowHandle, view_id: usize) -> Self {
Self {
any_handle: AnyWeakViewHandle {
@@ -4680,28 +4748,47 @@ impl<V: View> WeakViewHandle<V> {
cx.read(|cx| {
let handle = cx
.upgrade_view_handle(self)
- .ok_or_else(|| anyhow!("view {} was dropped", V::ui_name()))?;
+ .ok_or_else(|| anyhow!("view was dropped"))?;
cx.read_window(self.window, |cx| handle.read_with(cx, read))
.ok_or_else(|| anyhow!("window was removed"))
})
}
- pub fn update<T>(
+ pub fn update<T, B>(
&self,
- cx: &mut AsyncAppContext,
+ cx: &mut B,
update: impl FnOnce(&mut V, &mut ViewContext<V>) -> T,
- ) -> Result<T> {
- cx.update(|cx| {
- let handle = cx
- .upgrade_view_handle(self)
- .ok_or_else(|| anyhow!("view {} was dropped", V::ui_name()))?;
- cx.update_window(self.window, |cx| handle.update(cx, update))
- .ok_or_else(|| anyhow!("window was removed"))
+ ) -> Result<T>
+ where
+ B: BorrowWindowContext,
+ B::Result<Option<T>>: Flatten<T>,
+ {
+ cx.update_window(self.window(), |cx| {
+ cx.upgrade_view_handle(self)
+ .map(|handle| handle.update(cx, update))
})
+ .flatten()
+ .ok_or_else(|| anyhow!("window was removed"))
+ }
+}
+
+pub trait Flatten<T> {
+ fn flatten(self) -> Option<T>;
+}
+
+impl<T> Flatten<T> for Option<Option<T>> {
+ fn flatten(self) -> Option<T> {
+ self.flatten()
+ }
+}
+
+impl<T> Flatten<T> for Option<T> {
+ fn flatten(self) -> Option<T> {
+ self
}
}
-impl<T> Deref for WeakViewHandle<T> {
+impl<V> Deref for WeakViewHandle<V> {
type Target = AnyWeakViewHandle;
fn deref(&self) -> &Self::Target {
@@ -4709,7 +4796,7 @@ impl<T> Deref for WeakViewHandle<T> {
}
}
-impl<T> Clone for WeakViewHandle<T> {
+impl<V> Clone for WeakViewHandle<V> {
fn clone(&self) -> Self {
Self {
any_handle: self.any_handle.clone(),
@@ -5263,6 +5350,7 @@ mod tests {
button: MouseButton::Left,
modifiers: Default::default(),
click_count: 1,
+ is_down: true,
}),
false,
);
@@ -1,6 +1,6 @@
use crate::{
elements::AnyRootElement,
- geometry::rect::RectF,
+ geometry::{rect::RectF, Size},
json::ToJson,
keymap_matcher::{Binding, KeymapContext, Keystroke, MatchResult},
platform::{
@@ -8,8 +8,9 @@ use crate::{
MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
},
scene::{
- CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent,
- MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
+ CursorRegion, EventHandler, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
+ MouseEvent, MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
+ Scene,
},
text_layout::TextLayoutCache,
util::post_inc,
@@ -31,7 +32,11 @@ use sqlez::{
use std::{
any::TypeId,
mem,
- ops::{Deref, DerefMut, Range},
+ ops::{Deref, DerefMut, Range, Sub},
+};
+use taffy::{
+ tree::{Measurable, MeasureFunc},
+ Taffy,
};
use util::ResultExt;
use uuid::Uuid;
@@ -39,6 +44,7 @@ use uuid::Uuid;
use super::{Reference, ViewMetadata};
pub struct Window {
+ layout_engines: Vec<LayoutEngine>,
pub(crate) root_view: Option<AnyViewHandle>,
pub(crate) focused_view_id: Option<usize>,
pub(crate) parents: HashMap<usize, usize>,
@@ -51,6 +57,7 @@ pub struct Window {
appearance: Appearance,
cursor_regions: Vec<CursorRegion>,
mouse_regions: Vec<(MouseRegion, usize)>,
+ event_handlers: Vec<EventHandler>,
last_mouse_moved_event: Option<Event>,
pub(crate) hovered_region_ids: Vec<MouseRegionId>,
pub(crate) clicked_region_ids: Vec<MouseRegionId>,
@@ -67,12 +74,13 @@ impl Window {
build_view: F,
) -> Self
where
- F: FnOnce(&mut ViewContext<V>) -> V,
V: View,
+ F: FnOnce(&mut ViewContext<V>) -> V,
{
let titlebar_height = platform_window.titlebar_height();
let appearance = platform_window.appearance();
let mut window = Self {
+ layout_engines: Vec::new(),
root_view: None,
focused_view_id: None,
parents: Default::default(),
@@ -83,6 +91,7 @@ impl Window {
rendered_views: Default::default(),
cursor_regions: Default::default(),
mouse_regions: Default::default(),
+ event_handlers: Default::default(),
text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
last_mouse_moved_event: None,
hovered_region_ids: Default::default(),
@@ -109,6 +118,10 @@ impl Window {
.as_ref()
.expect("root_view called during window construction")
}
+
+ pub fn take_event_handlers(&mut self) -> Vec<EventHandler> {
+ mem::take(&mut self.event_handlers)
+ }
}
pub struct WindowContext<'a> {
@@ -207,6 +220,24 @@ impl<'a> WindowContext<'a> {
}
}
+ pub fn repaint(&mut self) {
+ let window = self.window();
+ self.pending_effects
+ .push_back(Effect::RepaintWindow { window });
+ }
+
+ pub fn layout_engine(&mut self) -> Option<&mut LayoutEngine> {
+ self.window.layout_engines.last_mut()
+ }
+
+ pub fn push_layout_engine(&mut self, engine: LayoutEngine) {
+ self.window.layout_engines.push(engine);
+ }
+
+ pub fn pop_layout_engine(&mut self) -> Option<LayoutEngine> {
+ self.window.layout_engines.pop()
+ }
+
pub fn remove_window(&mut self) {
self.removed = true;
}
@@ -227,6 +258,10 @@ impl<'a> WindowContext<'a> {
self.window.platform_window.content_size()
}
+ pub fn mouse_position(&self) -> Vector2F {
+ self.window.mouse_position
+ }
+
pub fn text_layout_cache(&self) -> &TextLayoutCache {
&self.window.text_layout_cache
}
@@ -242,14 +277,11 @@ impl<'a> WindowContext<'a> {
Some(result)
}
- pub(crate) fn update_view<T, S>(
+ pub(crate) fn update_view<V: 'static, S>(
&mut self,
- handle: &ViewHandle<T>,
- update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
- ) -> S
- where
- T: View,
- {
+ handle: &ViewHandle<V>,
+ update: &mut dyn FnMut(&mut V, &mut ViewContext<V>) -> S,
+ ) -> S {
self.update_any_view(handle.view_id, |view, cx| {
let mut cx = ViewContext::mutable(cx, handle.view_id);
update(
@@ -475,6 +507,8 @@ impl<'a> WindowContext<'a> {
}
pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
+ self.dispatch_to_new_event_handlers(&event);
+
let mut mouse_events = SmallVec::<[_; 2]>::new();
let mut notified_views: HashSet<usize> = Default::default();
let handle = self.window_handle;
@@ -583,10 +617,11 @@ impl<'a> WindowContext<'a> {
}
}
- if self
- .window
- .platform_window
- .is_topmost_for_position(*position)
+ if pressed_button.is_none()
+ && self
+ .window
+ .platform_window
+ .is_topmost_for_position(*position)
{
self.platform().set_cursor_style(style_to_assign);
}
@@ -753,6 +788,11 @@ impl<'a> WindowContext<'a> {
.contains_point(self.window.mouse_position)
{
valid_regions.push(mouse_region.clone());
+ } else {
+ // Let the view know that it hasn't been clicked anymore
+ if mouse_region.notify_on_click {
+ notified_views.insert(mouse_region.id().view_id());
+ }
}
}
}
@@ -852,6 +892,18 @@ impl<'a> WindowContext<'a> {
any_event_handled
}
+ fn dispatch_to_new_event_handlers(&mut self, event: &Event) {
+ if let Some(mouse_event) = event.mouse_event() {
+ let event_handlers = self.window.take_event_handlers();
+ for event_handler in event_handlers.iter().rev() {
+ if event_handler.event_type == mouse_event.type_id() {
+ (event_handler.handler)(mouse_event, self);
+ }
+ }
+ self.window.event_handlers = event_handlers;
+ }
+ }
+
pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
let handle = self.window_handle;
if let Some(focused_view_id) = self.window.focused_view_id {
@@ -942,14 +994,16 @@ impl<'a> WindowContext<'a> {
Ok(element)
}
- pub(crate) fn layout(&mut self, refreshing: bool) -> Result<HashMap<usize, usize>> {
+ pub fn layout(&mut self, refreshing: bool) -> Result<HashMap<usize, usize>> {
let window_size = self.window.platform_window.content_size();
let root_view_id = self.window.root_view().id();
+
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
+
let mut new_parents = HashMap::default();
let mut views_to_notify_if_ancestors_change = HashMap::default();
rendered_root.layout(
- SizeConstraint::strict(window_size),
+ SizeConstraint::new(window_size, window_size),
&mut new_parents,
&mut views_to_notify_if_ancestors_change,
refreshing,
@@ -982,7 +1036,7 @@ impl<'a> WindowContext<'a> {
Ok(old_parents)
}
- pub(crate) fn paint(&mut self) -> Result<Scene> {
+ pub fn paint(&mut self) -> Result<Scene> {
let window_size = self.window.platform_window.content_size();
let scale_factor = self.window.platform_window.scale_factor();
@@ -1001,9 +1055,10 @@ impl<'a> WindowContext<'a> {
.insert(root_view_id, rendered_root);
self.window.text_layout_cache.finish_frame();
- let scene = scene_builder.build();
+ let mut scene = scene_builder.build();
self.window.cursor_regions = scene.cursor_regions();
self.window.mouse_regions = scene.mouse_regions();
+ self.window.event_handlers = scene.take_event_handlers();
if self.window_is_active() {
if let Some(event) = self.window.last_mouse_moved_event.clone() {
@@ -1014,6 +1069,11 @@ impl<'a> WindowContext<'a> {
Ok(scene)
}
+ pub fn root_element(&self) -> &Box<dyn AnyRootElement> {
+ let view_id = self.window.root_view().id();
+ self.window.rendered_views.get(&view_id).unwrap()
+ }
+
pub fn rect_for_text_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
let focused_view_id = self.window.focused_view_id?;
self.window
@@ -1216,6 +1276,119 @@ impl<'a> WindowContext<'a> {
}
}
+#[derive(Default)]
+pub struct LayoutEngine(Taffy);
+pub use taffy::style::Style as LayoutStyle;
+
+impl LayoutEngine {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub fn add_node<C>(&mut self, style: LayoutStyle, children: C) -> Result<LayoutId>
+ where
+ C: IntoIterator<Item = LayoutId>,
+ {
+ Ok(self
+ .0
+ .new_with_children(style, &children.into_iter().collect::<Vec<_>>())?)
+ }
+
+ pub fn add_measured_node<F>(&mut self, style: LayoutStyle, measure: F) -> Result<LayoutId>
+ where
+ F: Fn(MeasureParams) -> Size<f32> + Sync + Send + 'static,
+ {
+ Ok(self
+ .0
+ .new_leaf_with_measure(style, MeasureFunc::Boxed(Box::new(MeasureFn(measure))))?)
+ }
+
+ pub fn compute_layout(&mut self, root: LayoutId, available_space: Vector2F) -> Result<()> {
+ self.0.compute_layout(
+ root,
+ taffy::geometry::Size {
+ width: available_space.x().into(),
+ height: available_space.y().into(),
+ },
+ )?;
+ Ok(())
+ }
+
+ pub fn computed_layout(&mut self, node: LayoutId) -> Result<EngineLayout> {
+ Ok(self.0.layout(node)?.into())
+ }
+}
+
+pub struct MeasureFn<F>(F);
+
+impl<F: Send + Sync> Measurable for MeasureFn<F>
+where
+ F: Fn(MeasureParams) -> Size<f32>,
+{
+ fn measure(
+ &self,
+ known_dimensions: taffy::prelude::Size<Option<f32>>,
+ available_space: taffy::prelude::Size<taffy::style::AvailableSpace>,
+ ) -> taffy::prelude::Size<f32> {
+ (self.0)(MeasureParams {
+ known_dimensions: known_dimensions.into(),
+ available_space: available_space.into(),
+ })
+ .into()
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct EngineLayout {
+ pub bounds: RectF,
+ pub order: u32,
+}
+
+pub struct MeasureParams {
+ pub known_dimensions: Size<Option<f32>>,
+ pub available_space: Size<AvailableSpace>,
+}
+
+#[derive(Clone)]
+pub enum AvailableSpace {
+ /// The amount of space available is the specified number of pixels
+ Pixels(f32),
+ /// The amount of space available is indefinite and the node should be laid out under a min-content constraint
+ MinContent,
+ /// The amount of space available is indefinite and the node should be laid out under a max-content constraint
+ MaxContent,
+}
+
+impl Default for AvailableSpace {
+ fn default() -> Self {
+ Self::Pixels(0.)
+ }
+}
+
+impl From<taffy::prelude::AvailableSpace> for AvailableSpace {
+ fn from(value: taffy::prelude::AvailableSpace) -> Self {
+ match value {
+ taffy::prelude::AvailableSpace::Definite(pixels) => Self::Pixels(pixels),
+ taffy::prelude::AvailableSpace::MinContent => Self::MinContent,
+ taffy::prelude::AvailableSpace::MaxContent => Self::MaxContent,
+ }
+ }
+}
+
+impl From<&taffy::tree::Layout> for EngineLayout {
+ fn from(value: &taffy::tree::Layout) -> Self {
+ Self {
+ bounds: RectF::new(
+ vec2f(value.location.x, value.location.y),
+ vec2f(value.size.width, value.size.height),
+ ),
+ order: value.order,
+ }
+ }
+}
+
+pub type LayoutId = taffy::prelude::NodeId;
+
pub struct RenderParams {
pub view_id: usize,
pub titlebar_height: f32,
@@ -1324,6 +1497,12 @@ impl SizeConstraint {
max: size,
}
}
+ pub fn loose(max: Vector2F) -> Self {
+ Self {
+ min: Vector2F::zero(),
+ max,
+ }
+ }
pub fn strict_along(axis: Axis, max: f32) -> Self {
match axis {
@@ -1360,6 +1539,17 @@ impl SizeConstraint {
}
}
+impl Sub<Vector2F> for SizeConstraint {
+ type Output = SizeConstraint;
+
+ fn sub(self, rhs: Vector2F) -> SizeConstraint {
+ SizeConstraint {
+ min: self.min - rhs,
+ max: self.max - rhs,
+ }
+ }
+}
+
impl Default for SizeConstraint {
fn default() -> Self {
SizeConstraint {
@@ -1378,6 +1568,7 @@ impl ToJson for SizeConstraint {
}
}
+#[derive(Clone)]
pub struct ChildView {
view_id: usize,
view_name: &'static str,
@@ -1393,7 +1584,7 @@ impl ChildView {
}
}
-impl<V: View> Element<V> for ChildView {
+impl<V: 'static> Element<V> for ChildView {
type LayoutState = ();
type PaintState = ();
@@ -15,35 +15,75 @@ use serde_json::json;
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
#[repr(transparent)]
-pub struct Color(#[schemars(with = "String")] ColorU);
+pub struct Color(#[schemars(with = "String")] pub ColorU);
+
+pub fn color(rgba: u32) -> Color {
+ Color::from_u32(rgba)
+}
+
+pub fn rgb(r: f32, g: f32, b: f32) -> Color {
+ Color(ColorF::new(r, g, b, 1.).to_u8())
+}
+
+pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
+ Color(ColorF::new(r, g, b, a).to_u8())
+}
+
+pub fn transparent_black() -> Color {
+ Color(ColorU::transparent_black())
+}
+
+pub fn black() -> Color {
+ Color(ColorU::black())
+}
+
+pub fn white() -> Color {
+ Color(ColorU::white())
+}
+
+pub fn red() -> Color {
+ color(0xff0000ff)
+}
+
+pub fn green() -> Color {
+ color(0x00ff00ff)
+}
+
+pub fn blue() -> Color {
+ color(0x0000ffff)
+}
+
+pub fn yellow() -> Color {
+ color(0xffff00ff)
+}
impl Color {
pub fn transparent_black() -> Self {
- Self(ColorU::transparent_black())
+ transparent_black()
}
pub fn black() -> Self {
- Self(ColorU::black())
+ black()
}
pub fn white() -> Self {
- Self(ColorU::white())
+ white()
}
pub fn red() -> Self {
- Self(ColorU::from_u32(0xff0000ff))
+ Color::from_u32(0xff0000ff)
}
pub fn green() -> Self {
- Self(ColorU::from_u32(0x00ff00ff))
+ Color::from_u32(0x00ff00ff)
}
pub fn blue() -> Self {
- Self(ColorU::from_u32(0x0000ffff))
+ Color::from_u32(0x0000ffff)
}
pub fn yellow() -> Self {
- Self(ColorU::from_u32(0xffff00ff))
+ Color::from_u32(0xffff00ff)
}
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
@@ -101,6 +141,12 @@ impl<'de> Deserialize<'de> for Color {
}
}
+impl From<u32> for Color {
+ fn from(value: u32) -> Self {
+ Self(ColorU::from_u32(value))
+ }
+}
+
impl ToJson for Color {
fn to_json(&self) -> serde_json::Value {
json!(format!(
@@ -34,7 +34,7 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
- json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
+ json, Action, Entity, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
ViewContext, WeakViewHandle, WindowContext,
};
use anyhow::{anyhow, Result};
@@ -42,14 +42,19 @@ use collections::HashMap;
use core::panic;
use json::ToJson;
use smallvec::SmallVec;
-use std::{any::Any, borrow::Cow, mem, ops::Range};
+use std::{
+ any::{type_name, Any},
+ borrow::Cow,
+ mem,
+ ops::Range,
+};
-pub trait Element<V: View>: 'static {
+pub trait Element<V: 'static>: 'static {
type LayoutState;
type PaintState;
fn view_name(&self) -> &'static str {
- V::ui_name()
+ type_name::<V>()
}
fn layout(
@@ -229,13 +234,30 @@ pub trait Element<V: View>: 'static {
{
MouseEventHandler::for_child::<Tag>(self.into_any(), region_id)
}
-}
-pub trait RenderElement {
- fn render<V: View>(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
+ fn component(self) -> StatelessElementAdapter
+ where
+ Self: Sized,
+ {
+ StatelessElementAdapter::new(self.into_any())
+ }
+
+ fn stateful_component(self) -> StatefulElementAdapter<V>
+ where
+ Self: Sized,
+ {
+ StatefulElementAdapter::new(self.into_any())
+ }
+
+ fn styleable_component(self) -> StylableAdapter<StatelessElementAdapter>
+ where
+ Self: Sized,
+ {
+ StatelessElementAdapter::new(self.into_any()).stylable()
+ }
}
-trait AnyElementState<V: View> {
+trait AnyElementState<V> {
fn layout(
&mut self,
constraint: SizeConstraint,
@@ -249,7 +271,7 @@ trait AnyElementState<V: View> {
origin: Vector2F,
visible_bounds: RectF,
view: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut PaintContext<V>,
);
fn rect_for_text_range(
@@ -266,7 +288,7 @@ trait AnyElementState<V: View> {
fn metadata(&self) -> Option<&dyn Any>;
}
-enum ElementState<V: View, E: Element<V>> {
+enum ElementState<V: 'static, E: Element<V>> {
Empty,
Init {
element: E,
@@ -287,7 +309,7 @@ enum ElementState<V: View, E: Element<V>> {
},
}
-impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
+impl<V, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
fn layout(
&mut self,
constraint: SizeConstraint,
@@ -330,7 +352,7 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
origin: Vector2F,
visible_bounds: RectF,
view: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut PaintContext<V>,
) {
*self = match mem::take(self) {
ElementState::PostLayout {
@@ -469,18 +491,18 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
}
}
-impl<V: View, E: Element<V>> Default for ElementState<V, E> {
+impl<V, E: Element<V>> Default for ElementState<V, E> {
fn default() -> Self {
Self::Empty
}
}
-pub struct AnyElement<V: View> {
+pub struct AnyElement<V> {
state: Box<dyn AnyElementState<V>>,
name: Option<Cow<'static, str>>,
}
-impl<V: View> AnyElement<V> {
+impl<V> AnyElement<V> {
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
@@ -506,7 +528,7 @@ impl<V: View> AnyElement<V> {
origin: Vector2F,
visible_bounds: RectF,
view: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut PaintContext<V>,
) {
self.state.paint(scene, origin, visible_bounds, view, cx);
}
@@ -548,7 +570,7 @@ impl<V: View> AnyElement<V> {
}
}
-impl<V: View> Element<V> for AnyElement<V> {
+impl<V: 'static> Element<V> for AnyElement<V> {
type LayoutState = ();
type PaintState = ();
@@ -606,12 +628,18 @@ impl<V: View> Element<V> for AnyElement<V> {
}
}
-pub struct RootElement<V: View> {
+impl Entity for AnyElement<()> {
+ type Event = ();
+}
+
+// impl View for AnyElement<()> {}
+
+pub struct RootElement<V> {
element: AnyElement<V>,
view: WeakViewHandle<V>,
}
-impl<V: View> RootElement<V> {
+impl<V> RootElement<V> {
pub fn new(element: AnyElement<V>, view: WeakViewHandle<V>) -> Self {
Self { element, view }
}
@@ -679,7 +707,9 @@ impl<V: View> AnyRootElement for RootElement<V> {
.ok_or_else(|| anyhow!("paint called on a root element for a dropped view"))?;
view.update(cx, |view, cx| {
- self.element.paint(scene, origin, visible_bounds, view, cx);
+ let mut cx = PaintContext::new(cx);
+ self.element
+ .paint(scene, origin, visible_bounds, view, &mut cx);
Ok(())
})
}
@@ -719,7 +749,7 @@ impl<V: View> AnyRootElement for RootElement<V> {
}
}
-pub trait ParentElement<'a, V: View>: Extend<AnyElement<V>> + Sized {
+pub trait ParentElement<'a, V: 'static>: Extend<AnyElement<V>> + Sized {
fn add_children<E: Element<V>>(&mut self, children: impl IntoIterator<Item = E>) {
self.extend(children.into_iter().map(|child| child.into_any()));
}
@@ -739,7 +769,12 @@ pub trait ParentElement<'a, V: View>: Extend<AnyElement<V>> + Sized {
}
}
-impl<'a, V: View, T> ParentElement<'a, V> for T where T: Extend<AnyElement<V>> {}
+impl<'a, V, T> ParentElement<'a, V> for T
+where
+ V: 'static,
+ T: Extend<AnyElement<V>>,
+{
+}
pub fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
if max_size.x().is_infinite() && max_size.y().is_infinite() {
@@ -1,18 +1,18 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
- json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+ json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
ViewContext,
};
use json::ToJson;
use serde_json::json;
-pub struct Align<V: View> {
+pub struct Align<V> {
child: AnyElement<V>,
alignment: Vector2F,
}
-impl<V: View> Align<V> {
+impl<V> Align<V> {
pub fn new(child: AnyElement<V>) -> Self {
Self {
child,
@@ -41,7 +41,7 @@ impl<V: View> Align<V> {
}
}
-impl<V: View> Element<V> for Align<V> {
+impl<V: 'static> Element<V> for Align<V> {
type LayoutState = ();
type PaintState = ();
@@ -3,7 +3,7 @@ use std::marker::PhantomData;
use super::Element;
use crate::{
json::{self, json},
- PaintContext, SceneBuilder, View, ViewContext,
+ PaintContext, SceneBuilder, ViewContext,
};
use json::ToJson;
use pathfinder_geometry::{
@@ -15,7 +15,6 @@ pub struct Canvas<V, F>(F, PhantomData<V>);
impl<V, F> Canvas<V, F>
where
- V: View,
F: FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
{
pub fn new(f: F) -> Self {
@@ -23,7 +22,7 @@ where
}
}
-impl<V: View, F> Element<V> for Canvas<V, F>
+impl<V: 'static, F> Element<V> for Canvas<V, F>
where
F: 'static + FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
{
@@ -4,21 +4,21 @@ use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use serde_json::json;
use crate::{
- json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+ json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
ViewContext,
};
-pub struct Clipped<V: View> {
+pub struct Clipped<V> {
child: AnyElement<V>,
}
-impl<V: View> Clipped<V> {
+impl<V> Clipped<V> {
pub fn new(child: AnyElement<V>) -> Self {
Self { child }
}
}
-impl<V: View> Element<V> for Clipped<V> {
+impl<V: 'static> Element<V> for Clipped<V> {
type LayoutState = ();
type PaintState = ();
@@ -1,47 +1,96 @@
-use std::marker::PhantomData;
+use std::{any::Any, marker::PhantomData};
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use crate::{
- AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
- ViewContext,
+ AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
};
use super::Empty;
-pub trait GeneralComponent {
- fn render<V: View>(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
- fn element<V: View>(self) -> ComponentAdapter<V, Self>
+/// The core stateless component trait, simply rendering an element tree
+pub trait Component {
+ fn render<V: 'static>(self, cx: &mut ViewContext<V>) -> AnyElement<V>;
+
+ fn element<V: 'static>(self) -> ComponentAdapter<V, Self>
where
Self: Sized,
{
ComponentAdapter::new(self)
}
+
+ fn stylable(self) -> StylableAdapter<Self>
+ where
+ Self: Sized,
+ {
+ StylableAdapter::new(self)
+ }
+
+ fn stateful<V: 'static>(self) -> StatefulAdapter<Self, V>
+ where
+ Self: Sized,
+ {
+ StatefulAdapter::new(self)
+ }
}
-pub trait StyleableComponent {
+/// Allows a a component's styles to be rebound in a simple way.
+pub trait Stylable: Component {
type Style: Clone;
- type Output: GeneralComponent;
+
+ fn with_style(self, style: Self::Style) -> Self;
+}
+
+/// This trait models the typestate pattern for a component's style,
+/// enforcing at compile time that a component is only usable after
+/// it has been styled while still allowing for late binding of the
+/// styling information
+pub trait SafeStylable {
+ type Style: Clone;
+ type Output: Component;
fn with_style(self, style: Self::Style) -> Self::Output;
}
-impl GeneralComponent for () {
- fn render<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
- Empty::new().into_any()
+/// All stylable components can trivially implement SafeStylable
+impl<C: Stylable> SafeStylable for C {
+ type Style = C::Style;
+
+ type Output = C;
+
+ fn with_style(self, style: Self::Style) -> Self::Output {
+ self.with_style(style)
+ }
+}
+
+/// Allows converting an unstylable component into a stylable one
+/// by using `()` as the style type
+pub struct StylableAdapter<C: Component> {
+ component: C,
+}
+
+impl<C: Component> StylableAdapter<C> {
+ pub fn new(component: C) -> Self {
+ Self { component }
}
}
-impl StyleableComponent for () {
+impl<C: Component> SafeStylable for StylableAdapter<C> {
type Style = ();
- type Output = ();
+
+ type Output = C;
fn with_style(self, _: Self::Style) -> Self::Output {
- ()
+ self.component
}
}
-pub trait Component<V: View> {
+/// This is a secondary trait for components that can be styled
+/// which rely on their view's state. This is useful for components that, for example,
+/// want to take click handler callbacks Unfortunately, the generic bound on the
+/// Component trait makes it incompatible with the stateless components above.
+// So let's just replicate them for now
+pub trait StatefulComponent<V: 'static> {
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
fn element(self) -> ComponentAdapter<V, Self>
@@ -50,21 +99,63 @@ pub trait Component<V: View> {
{
ComponentAdapter::new(self)
}
+
+ fn styleable(self) -> StatefulStylableAdapter<Self, V>
+ where
+ Self: Sized,
+ {
+ StatefulStylableAdapter::new(self)
+ }
+
+ fn stateless(self) -> StatelessElementAdapter
+ where
+ Self: Sized + 'static,
+ {
+ StatelessElementAdapter::new(self.element().into_any())
+ }
}
-impl<V: View, C: GeneralComponent> Component<V> for C {
- fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
- self.render(v, cx)
+/// It is trivial to convert stateless components to stateful components, so lets
+/// do so en masse. Note that the reverse is impossible without a helper.
+impl<V: 'static, C: Component> StatefulComponent<V> for C {
+ fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
+ self.render(cx)
}
}
-// StylableComponent -> GeneralComponent
-pub struct StylableComponentAdapter<C: Component<V>, V: View> {
+/// Same as stylable, but generic over a view type
+pub trait StatefulStylable<V: 'static>: StatefulComponent<V> {
+ type Style: Clone;
+
+ fn with_style(self, style: Self::Style) -> Self;
+}
+
+/// Same as SafeStylable, but generic over a view type
+pub trait StatefulSafeStylable<V: 'static> {
+ type Style: Clone;
+ type Output: StatefulComponent<V>;
+
+ fn with_style(self, style: Self::Style) -> Self::Output;
+}
+
+/// Converting from stateless to stateful
+impl<V: 'static, C: SafeStylable> StatefulSafeStylable<V> for C {
+ type Style = C::Style;
+
+ type Output = C::Output;
+
+ fn with_style(self, style: Self::Style) -> Self::Output {
+ self.with_style(style)
+ }
+}
+
+// A helper for converting stateless components into stateful ones
+pub struct StatefulAdapter<C, V> {
component: C,
phantom: std::marker::PhantomData<V>,
}
-impl<C: Component<V>, V: View> StylableComponentAdapter<C, V> {
+impl<C: Component, V: 'static> StatefulAdapter<C, V> {
pub fn new(component: C) -> Self {
Self {
component,
@@ -73,7 +164,31 @@ impl<C: Component<V>, V: View> StylableComponentAdapter<C, V> {
}
}
-impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapter<C, V> {
+impl<C: Component, V: 'static> StatefulComponent<V> for StatefulAdapter<C, V> {
+ fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
+ self.component.render(cx)
+ }
+}
+
+// A helper for converting stateful but style-less components into stylable ones
+// by using `()` as the style type
+pub struct StatefulStylableAdapter<C: StatefulComponent<V>, V: 'static> {
+ component: C,
+ phantom: std::marker::PhantomData<V>,
+}
+
+impl<C: StatefulComponent<V>, V: 'static> StatefulStylableAdapter<C, V> {
+ pub fn new(component: C) -> Self {
+ Self {
+ component,
+ phantom: std::marker::PhantomData,
+ }
+ }
+}
+
+impl<C: StatefulComponent<V>, V: 'static> StatefulSafeStylable<V>
+ for StatefulStylableAdapter<C, V>
+{
type Style = ();
type Output = C;
@@ -83,13 +198,37 @@ impl<C: GeneralComponent, V: View> StyleableComponent for StylableComponentAdapt
}
}
-// Element -> Component
-pub struct ElementAdapter<V: View> {
+/// A way of erasing the view generic from an element, useful
+/// for wrapping up an explicit element tree into stateless
+/// components
+pub struct StatelessElementAdapter {
+ element: Box<dyn Any>,
+}
+
+impl StatelessElementAdapter {
+ pub fn new<V: 'static>(element: AnyElement<V>) -> Self {
+ StatelessElementAdapter {
+ element: Box::new(element) as Box<dyn Any>,
+ }
+ }
+}
+
+impl Component for StatelessElementAdapter {
+ fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
+ *self
+ .element
+ .downcast::<AnyElement<V>>()
+ .expect("Don't move elements out of their view :(")
+ }
+}
+
+// For converting elements into stateful components
+pub struct StatefulElementAdapter<V: 'static> {
element: AnyElement<V>,
_phantom: std::marker::PhantomData<V>,
}
-impl<V: View> ElementAdapter<V> {
+impl<V: 'static> StatefulElementAdapter<V> {
pub fn new(element: AnyElement<V>) -> Self {
Self {
element,
@@ -98,20 +237,35 @@ impl<V: View> ElementAdapter<V> {
}
}
-impl<V: View> Component<V> for ElementAdapter<V> {
+impl<V: 'static> StatefulComponent<V> for StatefulElementAdapter<V> {
fn render(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
self.element
}
}
-// Component -> Element
-pub struct ComponentAdapter<V: View, E> {
+/// A convenient shorthand for creating an empty component.
+impl Component for () {
+ fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
+ Empty::new().into_any()
+ }
+}
+
+impl Stylable for () {
+ type Style = ();
+
+ fn with_style(self, _: Self::Style) -> Self {
+ ()
+ }
+}
+
+// For converting components back into Elements
+pub struct ComponentAdapter<V: 'static, E> {
component: Option<E>,
element: Option<AnyElement<V>>,
phantom: PhantomData<V>,
}
-impl<E, V: View> ComponentAdapter<V, E> {
+impl<E, V: 'static> ComponentAdapter<V, E> {
pub fn new(e: E) -> Self {
Self {
component: Some(e),
@@ -121,7 +275,7 @@ impl<E, V: View> ComponentAdapter<V, E> {
}
}
-impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
+impl<V: 'static, C: StatefulComponent<V> + 'static> Element<V> for ComponentAdapter<V, C> {
type LayoutState = ();
type PaintState = ();
@@ -184,6 +338,7 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
) -> serde_json::Value {
serde_json::json!({
"type": "ComponentAdapter",
+ "component": std::any::type_name::<C>(),
"child": self.element.as_ref().map(|el| el.debug(view, cx)),
})
}
@@ -5,21 +5,21 @@ use serde_json::json;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
- json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+ json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
ViewContext,
};
-pub struct ConstrainedBox<V: View> {
+pub struct ConstrainedBox<V> {
child: AnyElement<V>,
constraint: Constraint<V>,
}
-pub enum Constraint<V: View> {
+pub enum Constraint<V> {
Static(SizeConstraint),
Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint>),
}
-impl<V: View> ToJson for Constraint<V> {
+impl<V> ToJson for Constraint<V> {
fn to_json(&self) -> serde_json::Value {
match self {
Constraint::Static(constraint) => constraint.to_json(),
@@ -28,7 +28,7 @@ impl<V: View> ToJson for Constraint<V> {
}
}
-impl<V: View> ConstrainedBox<V> {
+impl<V: 'static> ConstrainedBox<V> {
pub fn new(child: impl Element<V>) -> Self {
Self {
child: child.into_any(),
@@ -132,7 +132,7 @@ impl<V: View> ConstrainedBox<V> {
}
}
-impl<V: View> Element<V> for ConstrainedBox<V> {
+impl<V: 'static> Element<V> for ConstrainedBox<V> {
type LayoutState = ();
type PaintState = ();
@@ -10,8 +10,7 @@ use crate::{
json::ToJson,
platform::CursorStyle,
scene::{self, Border, CornerRadii, CursorRegion, Quad},
- AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
- ViewContext,
+ AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
@@ -45,14 +44,22 @@ impl ContainerStyle {
..Default::default()
}
}
+
+ pub fn additional_length(&self) -> f32 {
+ self.padding.left
+ + self.padding.right
+ + self.border.width * 2.
+ + self.margin.left
+ + self.margin.right
+ }
}
-pub struct Container<V: View> {
+pub struct Container<V> {
child: AnyElement<V>,
style: ContainerStyle,
}
-impl<V: View> Container<V> {
+impl<V> Container<V> {
pub fn new(child: AnyElement<V>) -> Self {
Self {
child,
@@ -199,7 +206,7 @@ impl<V: View> Container<V> {
}
}
-impl<V: View> Element<V> for Container<V> {
+impl<V: 'static> Element<V> for Container<V> {
type LayoutState = ();
type PaintState = ();
@@ -350,8 +357,8 @@ impl ToJson for ContainerStyle {
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
pub struct Margin {
pub top: f32,
- pub left: f32,
pub bottom: f32,
+ pub left: f32,
pub right: f32,
}
@@ -6,7 +6,7 @@ use crate::{
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
- LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
+ LayoutContext, PaintContext, SceneBuilder, ViewContext,
};
use crate::{Element, SizeConstraint};
@@ -26,7 +26,7 @@ impl Empty {
}
}
-impl<V: View> Element<V> for Empty {
+impl<V: 'static> Element<V> for Empty {
type LayoutState = ();
type PaintState = ();
@@ -2,18 +2,18 @@ use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
- json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+ json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
ViewContext,
};
use serde_json::json;
-pub struct Expanded<V: View> {
+pub struct Expanded<V> {
child: AnyElement<V>,
full_width: bool,
full_height: bool,
}
-impl<V: View> Expanded<V> {
+impl<V: 'static> Expanded<V> {
pub fn new(child: impl Element<V>) -> Self {
Self {
child: child.into_any(),
@@ -35,7 +35,7 @@ impl<V: View> Expanded<V> {
}
}
-impl<V: View> Element<V> for Expanded<V> {
+impl<V: 'static> Element<V> for Expanded<V> {
type LayoutState = ();
type PaintState = ();
@@ -3,7 +3,7 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
use crate::{
json::{self, ToJson, Value},
AnyElement, Axis, Element, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder,
- SizeConstraint, Vector2FExt, View, ViewContext,
+ SizeConstraint, Vector2FExt, ViewContext,
};
use pathfinder_geometry::{
rect::RectF,
@@ -17,20 +17,22 @@ struct ScrollState {
scroll_position: Cell<f32>,
}
-pub struct Flex<V: View> {
+pub struct Flex<V> {
axis: Axis,
children: Vec<AnyElement<V>>,
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
child_alignment: f32,
+ spacing: f32,
}
-impl<V: View> Flex<V> {
+impl<V: 'static> Flex<V> {
pub fn new(axis: Axis) -> Self {
Self {
axis,
children: Default::default(),
scroll_state: None,
child_alignment: -1.,
+ spacing: 0.,
}
}
@@ -51,6 +53,11 @@ impl<V: View> Flex<V> {
self
}
+ pub fn with_spacing(mut self, spacing: f32) -> Self {
+ self.spacing = spacing;
+ self
+ }
+
pub fn scrollable<Tag>(
mut self,
element_id: usize,
@@ -81,7 +88,8 @@ impl<V: View> Flex<V> {
cx: &mut LayoutContext<V>,
) {
let cross_axis = self.axis.invert();
- for child in &mut self.children {
+ let last = self.children.len() - 1;
+ for (ix, child) in &mut self.children.iter_mut().enumerate() {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if let Some((flex, expanded)) = metadata.flex {
if expanded != layout_expanded {
@@ -93,6 +101,10 @@ impl<V: View> Flex<V> {
} else {
let space_per_flex = *remaining_space / *remaining_flex;
space_per_flex * flex
+ } - if ix == 0 || ix == last {
+ self.spacing / 2.
+ } else {
+ self.spacing
};
let child_min = if expanded { child_max } else { 0. };
let child_constraint = match self.axis {
@@ -115,13 +127,13 @@ impl<V: View> Flex<V> {
}
}
-impl<V: View> Extend<AnyElement<V>> for Flex<V> {
+impl<V> Extend<AnyElement<V>> for Flex<V> {
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
self.children.extend(children);
}
}
-impl<V: View> Element<V> for Flex<V> {
+impl<V: 'static> Element<V> for Flex<V> {
type LayoutState = f32;
type PaintState = ();
@@ -137,7 +149,8 @@ impl<V: View> Element<V> for Flex<V> {
let cross_axis = self.axis.invert();
let mut cross_axis_max: f32 = 0.0;
- for child in &mut self.children {
+ let last = self.children.len().saturating_sub(1);
+ for (ix, child) in &mut self.children.iter_mut().enumerate() {
let metadata = child.metadata::<FlexParentData>();
contains_float |= metadata.map_or(false, |metadata| metadata.float);
@@ -155,7 +168,12 @@ impl<V: View> Element<V> for Flex<V> {
),
};
let size = child.layout(child_constraint, view, cx);
- fixed_space += size.along(self.axis);
+ fixed_space += size.along(self.axis)
+ + if ix == 0 || ix == last {
+ self.spacing / 2.
+ } else {
+ self.spacing
+ };
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
}
}
@@ -315,7 +333,8 @@ impl<V: View> Element<V> for Flex<V> {
}
}
- for child in &mut self.children {
+ let last = self.children.len().saturating_sub(1);
+ for (ix, child) in &mut self.children.iter_mut().enumerate() {
if remaining_space > 0. {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if metadata.float {
@@ -353,9 +372,11 @@ impl<V: View> Element<V> for Flex<V> {
child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
+ let spacing = if ix == last { 0. } else { self.spacing };
+
match self.axis {
- Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
- Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
+ Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0),
+ Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing),
}
}
@@ -401,12 +422,12 @@ struct FlexParentData {
float: bool,
}
-pub struct FlexItem<V: View> {
+pub struct FlexItem<V> {
metadata: FlexParentData,
child: AnyElement<V>,
}
-impl<V: View> FlexItem<V> {
+impl<V: 'static> FlexItem<V> {
pub fn new(child: impl Element<V>) -> Self {
FlexItem {
metadata: FlexParentData {
@@ -428,7 +449,7 @@ impl<V: View> FlexItem<V> {
}
}
-impl<V: View> Element<V> for FlexItem<V> {
+impl<V: 'static> Element<V> for FlexItem<V> {
type LayoutState = ();
type PaintState = ();
@@ -3,16 +3,15 @@ use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::json,
- AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
- ViewContext,
+ AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
};
-pub struct Hook<V: View> {
+pub struct Hook<V> {
child: AnyElement<V>,
after_layout: Option<Box<dyn FnMut(Vector2F, &mut ViewContext<V>)>>,
}
-impl<V: View> Hook<V> {
+impl<V: 'static> Hook<V> {
pub fn new(child: impl Element<V>) -> Self {
Self {
child: child.into_any(),
@@ -29,7 +28,7 @@ impl<V: View> Hook<V> {
}
}
-impl<V: View> Element<V> for Hook<V> {
+impl<V: 'static> Element<V> for Hook<V> {
type LayoutState = ();
type PaintState = ();
@@ -6,7 +6,7 @@ use crate::{
},
json::{json, ToJson},
scene, Border, Element, ImageData, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
- View, ViewContext,
+ ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
@@ -57,7 +57,7 @@ impl Image {
}
}
-impl<V: View> Element<V> for Image {
+impl<V: 'static> Element<V> for Image {
type LayoutState = Option<Arc<ImageData>>;
type PaintState = ();
@@ -31,7 +31,7 @@ impl KeystrokeLabel {
}
}
-impl<V: View> Element<V> for KeystrokeLabel {
+impl<V: 'static> Element<V> for KeystrokeLabel {
type LayoutState = AnyElement<V>;
type PaintState = ();
@@ -8,7 +8,7 @@ use crate::{
},
json::{ToJson, Value},
text_layout::{Line, RunStyle},
- Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
+ Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
@@ -128,7 +128,7 @@ impl Label {
}
}
-impl<V: View> Element<V> for Label {
+impl<V: 'static> Element<V> for Label {
type LayoutState = Line;
type PaintState = ();
@@ -5,16 +5,16 @@ use crate::{
},
json::json,
AnyElement, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, SizeConstraint,
- View, ViewContext,
+ ViewContext,
};
use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc};
use sum_tree::{Bias, SumTree};
-pub struct List<V: View> {
+pub struct List<V> {
state: ListState<V>,
}
-pub struct ListState<V: View>(Rc<RefCell<StateInner<V>>>);
+pub struct ListState<V>(Rc<RefCell<StateInner<V>>>);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Orientation {
@@ -22,7 +22,7 @@ pub enum Orientation {
Bottom,
}
-struct StateInner<V: View> {
+struct StateInner<V> {
last_layout_width: Option<f32>,
render_item: Box<dyn FnMut(&mut V, usize, &mut ViewContext<V>) -> AnyElement<V>>,
rendered_range: Range<usize>,
@@ -40,13 +40,13 @@ pub struct ListOffset {
pub offset_in_item: f32,
}
-enum ListItem<V: View> {
+enum ListItem<V> {
Unrendered,
Rendered(Rc<RefCell<AnyElement<V>>>),
Removed(f32),
}
-impl<V: View> Clone for ListItem<V> {
+impl<V> Clone for ListItem<V> {
fn clone(&self) -> Self {
match self {
Self::Unrendered => Self::Unrendered,
@@ -56,7 +56,7 @@ impl<V: View> Clone for ListItem<V> {
}
}
-impl<V: View> Debug for ListItem<V> {
+impl<V> Debug for ListItem<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unrendered => write!(f, "Unrendered"),
@@ -86,13 +86,13 @@ struct UnrenderedCount(usize);
#[derive(Clone, Debug, Default)]
struct Height(f32);
-impl<V: View> List<V> {
+impl<V> List<V> {
pub fn new(state: ListState<V>) -> Self {
Self { state }
}
}
-impl<V: View> Element<V> for List<V> {
+impl<V: 'static> Element<V> for List<V> {
type LayoutState = ListOffset;
type PaintState = ();
@@ -347,7 +347,7 @@ impl<V: View> Element<V> for List<V> {
}
}
-impl<V: View> ListState<V> {
+impl<V: 'static> ListState<V> {
pub fn new<D, F>(
element_count: usize,
orientation: Orientation,
@@ -440,13 +440,13 @@ impl<V: View> ListState<V> {
}
}
-impl<V: View> Clone for ListState<V> {
+impl<V> Clone for ListState<V> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
-impl<V: View> StateInner<V> {
+impl<V: 'static> StateInner<V> {
fn render_item(
&mut self,
ix: usize,
@@ -560,7 +560,7 @@ impl<V: View> StateInner<V> {
}
}
-impl<V: View> ListItem<V> {
+impl<V> ListItem<V> {
fn remove(&self) -> Self {
match self {
ListItem::Unrendered => ListItem::Unrendered,
@@ -570,7 +570,7 @@ impl<V: View> ListItem<V> {
}
}
-impl<V: View> sum_tree::Item for ListItem<V> {
+impl<V> sum_tree::Item for ListItem<V> {
type Summary = ListItemSummary;
fn summary(&self) -> Self::Summary {
@@ -944,7 +944,7 @@ mod tests {
type Event = ();
}
- impl View for TestView {
+ impl crate::View for TestView {
fn ui_name() -> &'static str {
"TestView"
}
@@ -968,7 +968,7 @@ mod tests {
}
}
- impl<V: View> Element<V> for TestElement {
+ impl<V: 'static> Element<V> for TestElement {
type LayoutState = ();
type PaintState = ();
@@ -11,12 +11,12 @@ use crate::{
MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
},
AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext,
- SceneBuilder, SizeConstraint, TypeTag, View, ViewContext,
+ SceneBuilder, SizeConstraint, TypeTag, ViewContext,
};
use serde_json::json;
use std::ops::Range;
-pub struct MouseEventHandler<V: View> {
+pub struct MouseEventHandler<V: 'static> {
child: AnyElement<V>,
region_id: usize,
cursor_style: Option<CursorStyle>,
@@ -31,7 +31,7 @@ pub struct MouseEventHandler<V: View> {
/// Element which provides a render_child callback with a MouseState and paints a mouse
/// region under (or above) it for easy mouse event handling.
-impl<V: View> MouseEventHandler<V> {
+impl<V: 'static> MouseEventHandler<V> {
pub fn for_child<Tag: 'static>(child: impl Element<V>, region_id: usize) -> Self {
Self {
child: child.into_any(),
@@ -267,7 +267,7 @@ impl<V: View> MouseEventHandler<V> {
}
}
-impl<V: View> Element<V> for MouseEventHandler<V> {
+impl<V: 'static> Element<V> for MouseEventHandler<V> {
type LayoutState = ();
type PaintState = ();
@@ -4,11 +4,11 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::ToJson,
AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
- SizeConstraint, View, ViewContext,
+ SizeConstraint, ViewContext,
};
use serde_json::json;
-pub struct Overlay<V: View> {
+pub struct Overlay<V> {
child: AnyElement<V>,
anchor_position: Option<Vector2F>,
anchor_corner: AnchorCorner,
@@ -73,7 +73,7 @@ impl AnchorCorner {
}
}
-impl<V: View> Overlay<V> {
+impl<V: 'static> Overlay<V> {
pub fn new(child: impl Element<V>) -> Self {
Self {
child: child.into_any(),
@@ -117,7 +117,7 @@ impl<V: View> Overlay<V> {
}
}
-impl<V: View> Element<V> for Overlay<V> {
+impl<V: 'static> Element<V> for Overlay<V> {
type LayoutState = Vector2F;
type PaintState = ();
@@ -59,7 +59,7 @@ where
.and_then(|map| map.0.get(&tag))
}
-pub struct Resizable<V: View> {
+pub struct Resizable<V: 'static> {
child: AnyElement<V>,
tag: TypeTag,
handle_side: HandleSide,
@@ -69,7 +69,7 @@ pub struct Resizable<V: View> {
const DEFAULT_HANDLE_SIZE: f32 = 4.0;
-impl<V: View> Resizable<V> {
+impl<V: 'static> Resizable<V> {
pub fn new<Tag: 'static>(
child: AnyElement<V>,
handle_side: HandleSide,
@@ -97,7 +97,7 @@ impl<V: View> Resizable<V> {
}
}
-impl<V: View> Element<V> for Resizable<V> {
+impl<V: 'static> Element<V> for Resizable<V> {
type LayoutState = SizeConstraint;
type PaintState = ();
@@ -219,12 +219,12 @@ impl<V: View> Element<V> for Resizable<V> {
#[derive(Debug, Default)]
struct ProviderMap(HashMap<TypeTag, (RectF, RectF)>);
-pub struct BoundsProvider<V: View, P> {
+pub struct BoundsProvider<V: 'static, P> {
child: AnyElement<V>,
phantom: std::marker::PhantomData<P>,
}
-impl<V: View, P: 'static> BoundsProvider<V, P> {
+impl<V: 'static, P: 'static> BoundsProvider<V, P> {
pub fn new(child: AnyElement<V>) -> Self {
Self {
child,
@@ -3,17 +3,16 @@ use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::{self, json, ToJson},
- AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
- ViewContext,
+ AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
};
/// Element which renders it's children in a stack on top of each other.
/// The first child determines the size of the others.
-pub struct Stack<V: View> {
+pub struct Stack<V> {
children: Vec<AnyElement<V>>,
}
-impl<V: View> Default for Stack<V> {
+impl<V> Default for Stack<V> {
fn default() -> Self {
Self {
children: Vec::new(),
@@ -21,13 +20,13 @@ impl<V: View> Default for Stack<V> {
}
}
-impl<V: View> Stack<V> {
+impl<V> Stack<V> {
pub fn new() -> Self {
Self::default()
}
}
-impl<V: View> Element<V> for Stack<V> {
+impl<V: 'static> Element<V> for Stack<V> {
type LayoutState = ();
type PaintState = ();
@@ -99,7 +98,7 @@ impl<V: View> Element<V> for Stack<V> {
}
}
-impl<V: View> Extend<AnyElement<V>> for Stack<V> {
+impl<V> Extend<AnyElement<V>> for Stack<V> {
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
self.children.extend(children)
}
@@ -7,7 +7,7 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
- scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+ scene, Element, LayoutContext, SceneBuilder, SizeConstraint, ViewContext,
};
use schemars::JsonSchema;
use serde_derive::Deserialize;
@@ -27,7 +27,7 @@ impl Svg {
}
}
- pub fn for_style<V: View>(style: SvgStyle) -> impl Element<V> {
+ pub fn for_style<V: 'static>(style: SvgStyle) -> impl Element<V> {
Self::new(style.asset)
.with_color(style.color)
.constrained()
@@ -41,7 +41,7 @@ impl Svg {
}
}
-impl<V: View> Element<V> for Svg {
+impl<V: 'static> Element<V> for Svg {
type LayoutState = Option<usvg::Tree>;
type PaintState = ();
@@ -8,7 +8,7 @@ use crate::{
json::{ToJson, Value},
text_layout::{Line, RunStyle, ShapedBoundary},
AppContext, Element, FontCache, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
- TextLayoutCache, View, ViewContext,
+ TextLayoutCache, ViewContext,
};
use log::warn;
use serde_json::json;
@@ -70,7 +70,7 @@ impl Text {
}
}
-impl<V: View> Element<V> for Text {
+impl<V: 'static> Element<V> for Text {
type LayoutState = LayoutState;
type PaintState = ();
@@ -338,7 +338,7 @@ impl<V: View> Element<V> for Text {
}
/// Perform text layout on a series of highlighted chunks of text.
-fn layout_highlighted_chunks<'a>(
+pub fn layout_highlighted_chunks<'a>(
chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
text_style: &TextStyle,
text_layout_cache: &TextLayoutCache,
@@ -7,7 +7,7 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::json,
Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
- Task, TypeTag, View, ViewContext,
+ Task, TypeTag, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
@@ -22,7 +22,7 @@ use util::ResultExt;
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
-pub struct Tooltip<V: View> {
+pub struct Tooltip<V> {
child: AnyElement<V>,
tooltip: Option<AnyElement<V>>,
_state: ElementStateHandle<Rc<TooltipState>>,
@@ -52,7 +52,7 @@ pub struct KeystrokeStyle {
text: TextStyle,
}
-impl<V: View> Tooltip<V> {
+impl<V: 'static> Tooltip<V> {
pub fn new<Tag: 'static>(
id: usize,
text: impl Into<Cow<'static, str>>,
@@ -181,7 +181,7 @@ impl<V: View> Tooltip<V> {
}
}
-impl<V: View> Element<V> for Tooltip<V> {
+impl<V: 'static> Element<V> for Tooltip<V> {
type LayoutState = ();
type PaintState = ();
@@ -6,7 +6,7 @@ use crate::{
},
json::{self, json},
platform::ScrollWheelEvent,
- AnyElement, LayoutContext, MouseRegion, PaintContext, SceneBuilder, View, ViewContext,
+ AnyElement, LayoutContext, MouseRegion, PaintContext, SceneBuilder, ViewContext,
};
use json::ToJson;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -36,13 +36,13 @@ struct StateInner {
scroll_to: Option<ScrollTarget>,
}
-pub struct UniformListLayoutState<V: View> {
+pub struct UniformListLayoutState<V> {
scroll_max: f32,
item_height: f32,
items: Vec<AnyElement<V>>,
}
-pub struct UniformList<V: View> {
+pub struct UniformList<V> {
state: UniformListState,
item_count: usize,
#[allow(clippy::type_complexity)]
@@ -53,7 +53,7 @@ pub struct UniformList<V: View> {
view_id: usize,
}
-impl<V: View> UniformList<V> {
+impl<V: 'static> UniformList<V> {
pub fn new<F>(
state: UniformListState,
item_count: usize,
@@ -61,7 +61,6 @@ impl<V: View> UniformList<V> {
append_items: F,
) -> Self
where
- V: View,
F: 'static + Fn(&mut V, Range<usize>, &mut Vec<AnyElement<V>>, &mut ViewContext<V>),
{
Self {
@@ -151,7 +150,7 @@ impl<V: View> UniformList<V> {
}
}
-impl<V: View> Element<V> for UniformList<V> {
+impl<V: 'static> Element<V> for UniformList<V> {
type LayoutState = UniformListLayoutState<V>;
type PaintState = ();
@@ -11,6 +11,7 @@ pub use font_kit::{
properties::{Properties, Stretch, Style, Weight},
};
use ordered_float::OrderedFloat;
+use refineable::Refineable;
use schemars::JsonSchema;
use serde::{de, Deserialize, Serialize};
use serde_json::Value;
@@ -59,7 +60,7 @@ pub struct Features {
pub zero: Option<bool>,
}
-#[derive(Clone, Debug, JsonSchema)]
+#[derive(Clone, Debug, JsonSchema, Refineable)]
pub struct TextStyle {
pub color: Color,
pub font_family_name: Arc<str>,
@@ -69,6 +70,7 @@ pub struct TextStyle {
#[schemars(with = "PropertiesDef")]
pub font_properties: Properties,
pub underline: Underline,
+ pub soft_wrap: bool,
}
impl TextStyle {
@@ -90,20 +92,11 @@ impl TextStyle {
font_size: refinement.font_size.unwrap_or(self.font_size),
font_properties: refinement.font_properties.unwrap_or(self.font_properties),
underline: refinement.underline.unwrap_or(self.underline),
+ soft_wrap: refinement.soft_wrap.unwrap_or(self.soft_wrap),
}
}
}
-pub struct TextStyleRefinement {
- pub color: Option<Color>,
- pub font_family_name: Option<Arc<str>>,
- pub font_family_id: Option<FamilyId>,
- pub font_id: Option<FontId>,
- pub font_size: Option<f32>,
- pub font_properties: Option<Properties>,
- pub underline: Option<Underline>,
-}
-
#[derive(JsonSchema)]
#[serde(remote = "Properties")]
pub struct PropertiesDef {
@@ -222,9 +215,31 @@ impl TextStyle {
font_size,
font_properties,
underline,
+ soft_wrap: false,
})
}
+ pub fn default(font_cache: &FontCache) -> Self {
+ let font_family_id = font_cache.known_existing_family();
+ let font_id = font_cache
+ .select_font(font_family_id, &Default::default())
+ .expect("did not have any font in system-provided family");
+ let font_family_name = font_cache
+ .family_name(font_family_id)
+ .expect("we loaded this family from the font cache, so this should work");
+
+ Self {
+ color: Color::default(),
+ font_family_name,
+ font_family_id,
+ font_id,
+ font_size: 14.,
+ font_properties: Default::default(),
+ underline: Default::default(),
+ soft_wrap: true,
+ }
+ }
+
pub fn with_font_size(mut self, font_size: f32) -> Self {
self.font_size = font_size;
self
@@ -352,24 +367,7 @@ impl Default for TextStyle {
let font_cache = font_cache
.as_ref()
.expect("TextStyle::default can only be called within a call to with_font_cache");
-
- let font_family_id = font_cache.known_existing_family();
- let font_id = font_cache
- .select_font(font_family_id, &Default::default())
- .expect("did not have any font in system-provided family");
- let font_family_name = font_cache
- .family_name(font_family_id)
- .expect("we loaded this family from the font cache, so this should work");
-
- Self {
- color: Default::default(),
- font_family_name,
- font_family_id,
- font_id,
- font_size: 14.,
- font_properties: Default::default(),
- underline: Default::default(),
- }
+ Self::default(font_cache)
})
}
}
@@ -2,6 +2,7 @@ use super::scene::{Path, PathVertex};
use crate::{color::Color, json::ToJson};
pub use pathfinder_geometry::*;
use rect::RectF;
+use refineable::Refineable;
use serde::{Deserialize, Deserializer};
use serde_json::json;
use vector::{vec2f, Vector2F};
@@ -131,3 +132,258 @@ impl ToJson for RectF {
json!({"origin": self.origin().to_json(), "size": self.size().to_json()})
}
}
+
+#[derive(Refineable)]
+pub struct Point<T: Clone + Default> {
+ pub x: T,
+ pub y: T,
+}
+
+impl<T: Clone + Default> Clone for Point<T> {
+ fn clone(&self) -> Self {
+ Self {
+ x: self.x.clone(),
+ y: self.y.clone(),
+ }
+ }
+}
+
+impl<T: Clone + Default> Into<taffy::geometry::Point<T>> for Point<T> {
+ fn into(self) -> taffy::geometry::Point<T> {
+ taffy::geometry::Point {
+ x: self.x,
+ y: self.y,
+ }
+ }
+}
+
+#[derive(Clone, Refineable)]
+pub struct Size<T: Clone + Default> {
+ pub width: T,
+ pub height: T,
+}
+
+impl<S, T: Clone + Default> From<taffy::geometry::Size<S>> for Size<T>
+where
+ S: Into<T>,
+{
+ fn from(value: taffy::geometry::Size<S>) -> Self {
+ Self {
+ width: value.width.into(),
+ height: value.height.into(),
+ }
+ }
+}
+
+impl<S, T: Clone + Default> Into<taffy::geometry::Size<S>> for Size<T>
+where
+ T: Into<S>,
+{
+ fn into(self) -> taffy::geometry::Size<S> {
+ taffy::geometry::Size {
+ width: self.width.into(),
+ height: self.height.into(),
+ }
+ }
+}
+
+impl Size<DefiniteLength> {
+ pub fn zero() -> Self {
+ Self {
+ width: pixels(0.),
+ height: pixels(0.),
+ }
+ }
+
+ pub fn to_taffy(&self, rem_size: f32) -> taffy::geometry::Size<taffy::style::LengthPercentage> {
+ taffy::geometry::Size {
+ width: self.width.to_taffy(rem_size),
+ height: self.height.to_taffy(rem_size),
+ }
+ }
+}
+
+impl Size<Length> {
+ pub fn auto() -> Self {
+ Self {
+ width: Length::Auto,
+ height: Length::Auto,
+ }
+ }
+
+ pub fn to_taffy<T: From<taffy::prelude::LengthPercentageAuto>>(
+ &self,
+ rem_size: f32,
+ ) -> taffy::geometry::Size<T> {
+ taffy::geometry::Size {
+ width: self.width.to_taffy(rem_size).into(),
+ height: self.height.to_taffy(rem_size).into(),
+ }
+ }
+}
+
+#[derive(Clone, Default, Refineable)]
+pub struct Edges<T: Clone + Default> {
+ pub top: T,
+ pub right: T,
+ pub bottom: T,
+ pub left: T,
+}
+
+impl Edges<DefiniteLength> {
+ pub fn zero() -> Self {
+ Self {
+ top: pixels(0.),
+ right: pixels(0.),
+ bottom: pixels(0.),
+ left: pixels(0.),
+ }
+ }
+
+ pub fn to_taffy(&self, rem_size: f32) -> taffy::geometry::Rect<taffy::style::LengthPercentage> {
+ taffy::geometry::Rect {
+ top: self.top.to_taffy(rem_size),
+ right: self.right.to_taffy(rem_size),
+ bottom: self.bottom.to_taffy(rem_size),
+ left: self.left.to_taffy(rem_size),
+ }
+ }
+}
+
+impl Edges<Length> {
+ pub fn auto() -> Self {
+ Self {
+ top: Length::Auto,
+ right: Length::Auto,
+ bottom: Length::Auto,
+ left: Length::Auto,
+ }
+ }
+
+ pub fn zero() -> Self {
+ Self {
+ top: pixels(0.),
+ right: pixels(0.),
+ bottom: pixels(0.),
+ left: pixels(0.),
+ }
+ }
+
+ pub fn to_taffy(
+ &self,
+ rem_size: f32,
+ ) -> taffy::geometry::Rect<taffy::style::LengthPercentageAuto> {
+ taffy::geometry::Rect {
+ top: self.top.to_taffy(rem_size),
+ right: self.right.to_taffy(rem_size),
+ bottom: self.bottom.to_taffy(rem_size),
+ left: self.left.to_taffy(rem_size),
+ }
+ }
+}
+
+#[derive(Clone, Copy)]
+pub enum AbsoluteLength {
+ Pixels(f32),
+ Rems(f32),
+}
+
+impl AbsoluteLength {
+ pub fn to_pixels(&self, rem_size: f32) -> f32 {
+ match self {
+ AbsoluteLength::Pixels(pixels) => *pixels,
+ AbsoluteLength::Rems(rems) => rems * rem_size,
+ }
+ }
+}
+
+impl Default for AbsoluteLength {
+ fn default() -> Self {
+ Self::Pixels(0.0)
+ }
+}
+
+/// A non-auto length that can be defined in pixels, rems, or percent of parent.
+#[derive(Clone, Copy)]
+pub enum DefiniteLength {
+ Absolute(AbsoluteLength),
+ Relative(f32), // Percent, from 0 to 100.
+}
+
+impl DefiniteLength {
+ fn to_taffy(&self, rem_size: f32) -> taffy::style::LengthPercentage {
+ match self {
+ DefiniteLength::Absolute(length) => match length {
+ AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(*pixels),
+ AbsoluteLength::Rems(rems) => {
+ taffy::style::LengthPercentage::Length(rems * rem_size)
+ }
+ },
+ DefiniteLength::Relative(fraction) => {
+ taffy::style::LengthPercentage::Percent(*fraction)
+ }
+ }
+ }
+}
+
+impl From<AbsoluteLength> 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,
+}
+
+pub fn relative<T: From<DefiniteLength>>(fraction: f32) -> T {
+ DefiniteLength::Relative(fraction).into()
+}
+
+pub fn rems<T: From<AbsoluteLength>>(rems: f32) -> T {
+ AbsoluteLength::Rems(rems).into()
+}
+
+pub fn pixels<T: From<AbsoluteLength>>(pixels: f32) -> T {
+ AbsoluteLength::Pixels(pixels).into()
+}
+
+pub fn auto() -> Length {
+ Length::Auto
+}
+
+impl Length {
+ pub fn to_taffy(&self, rem_size: f32) -> taffy::prelude::LengthPercentageAuto {
+ match self {
+ Length::Definite(length) => length.to_taffy(rem_size).into(),
+ Length::Auto => taffy::prelude::LengthPercentageAuto::Auto,
+ }
+ }
+}
+
+impl From<DefiniteLength> for Length {
+ fn from(length: DefiniteLength) -> Self {
+ Self::Definite(length)
+ }
+}
+
+impl From<AbsoluteLength> for Length {
+ fn from(length: AbsoluteLength) -> Self {
+ Self::Definite(length.into())
+ }
+}
+
+impl Default for Length {
+ fn default() -> Self {
+ Self::Definite(DefiniteLength::default())
+ }
+}
@@ -27,7 +27,10 @@ pub mod json;
pub mod keymap_matcher;
pub mod platform;
pub use gpui_macros::{test, Element};
-pub use window::{Axis, RectFExt, SizeConstraint, Vector2FExt, WindowContext};
+pub use window::{
+ Axis, EngineLayout, LayoutEngine, LayoutId, RectFExt, SizeConstraint, Vector2FExt,
+ WindowContext,
+};
pub use anyhow;
pub use serde_json;
@@ -192,7 +192,7 @@ impl<'a> WindowOptions<'a> {
}
}
-#[derive(Debug)]
+#[derive(Debug, Default)]
pub struct TitlebarOptions<'a> {
pub title: Option<&'a str>,
pub appears_transparent: bool,
@@ -1,4 +1,4 @@
-use std::ops::Deref;
+use std::{any::Any, ops::Deref};
use pathfinder_geometry::vector::vec2f;
@@ -142,6 +142,7 @@ pub struct MouseButtonEvent {
pub position: Vector2F,
pub modifiers: Modifiers,
pub click_count: usize,
+ pub is_down: bool,
}
impl Deref for MouseButtonEvent {
@@ -174,6 +175,7 @@ impl MouseMovedEvent {
button: self.pressed_button.unwrap_or(button),
modifiers: self.modifiers,
click_count: 0,
+ is_down: self.pressed_button.is_some(),
}
}
}
@@ -211,10 +213,24 @@ impl Event {
Event::KeyDown { .. } => None,
Event::KeyUp { .. } => None,
Event::ModifiersChanged { .. } => None,
- Event::MouseDown(event) | Event::MouseUp(event) => Some(event.position),
+ Event::MouseDown(event) => Some(event.position),
+ Event::MouseUp(event) => Some(event.position),
Event::MouseMoved(event) => Some(event.position),
Event::MouseExited(event) => Some(event.position),
Event::ScrollWheel(event) => Some(event.position),
}
}
+
+ pub fn mouse_event<'a>(&'a self) -> Option<&'a dyn Any> {
+ match self {
+ Event::KeyDown { .. } => None,
+ Event::KeyUp { .. } => None,
+ Event::ModifiersChanged { .. } => None,
+ Event::MouseDown(event) => Some(event),
+ Event::MouseUp(event) => Some(event),
+ Event::MouseMoved(event) => Some(event),
+ Event::MouseExited(event) => Some(event),
+ Event::ScrollWheel(event) => Some(event),
+ }
+ }
}
@@ -132,6 +132,7 @@ impl Event {
),
modifiers: read_modifiers(native_event),
click_count: native_event.clickCount() as usize,
+ is_down: true,
})
})
}
@@ -158,6 +159,7 @@ impl Event {
),
modifiers: read_modifiers(native_event),
click_count: native_event.clickCount() as usize,
+ is_down: false,
})
})
}
@@ -1,5 +1,6 @@
mod mouse_event;
mod mouse_region;
+mod region;
#[cfg(debug_assertions)]
use collections::HashSet;
@@ -8,7 +9,12 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde_derive::Serialize;
use serde_json::json;
-use std::{borrow::Cow, sync::Arc};
+use std::{
+ any::{Any, TypeId},
+ borrow::Cow,
+ rc::Rc,
+ sync::Arc,
+};
use crate::{
color::Color,
@@ -16,7 +22,7 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::ToJson,
platform::{current::Surface, CursorStyle},
- ImageData,
+ ImageData, WindowContext,
};
pub use mouse_event::*;
pub use mouse_region::*;
@@ -25,6 +31,8 @@ pub struct SceneBuilder {
scale_factor: f32,
stacking_contexts: Vec<StackingContext>,
active_stacking_context_stack: Vec<usize>,
+ /// Used by the playground crate.
+ pub event_handlers: Vec<EventHandler>,
#[cfg(debug_assertions)]
mouse_region_ids: HashSet<MouseRegionId>,
}
@@ -32,6 +40,7 @@ pub struct SceneBuilder {
pub struct Scene {
scale_factor: f32,
stacking_contexts: Vec<StackingContext>,
+ event_handlers: Vec<EventHandler>,
}
struct StackingContext {
@@ -272,6 +281,12 @@ impl Scene {
})
.collect()
}
+
+ pub fn take_event_handlers(&mut self) -> Vec<EventHandler> {
+ self.event_handlers
+ .sort_by(|a, b| a.order.cmp(&b.order).reverse());
+ std::mem::take(&mut self.event_handlers)
+ }
}
impl SceneBuilder {
@@ -283,6 +298,7 @@ impl SceneBuilder {
active_stacking_context_stack: vec![0],
#[cfg(debug_assertions)]
mouse_region_ids: Default::default(),
+ event_handlers: Vec::new(),
}
}
@@ -292,6 +308,7 @@ impl SceneBuilder {
Scene {
scale_factor: self.scale_factor,
stacking_contexts: self.stacking_contexts,
+ event_handlers: self.event_handlers,
}
}
@@ -688,6 +705,13 @@ impl MouseRegion {
}
}
+pub struct EventHandler {
+ pub order: u32,
+ // The &dyn Any parameter below expects an event.
+ pub handler: Rc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>,
+ pub event_type: TypeId,
+}
+
fn can_draw(bounds: RectF) -> bool {
let size = bounds.size();
size.x() > 0. && size.y() > 0.
@@ -1,6 +1,4 @@
-use crate::{
- platform::MouseButton, window::WindowContext, EventContext, TypeTag, View, ViewContext,
-};
+use crate::{platform::MouseButton, window::WindowContext, EventContext, TypeTag, ViewContext};
use collections::HashMap;
use pathfinder_geometry::rect::RectF;
use smallvec::SmallVec;
@@ -72,7 +70,7 @@ impl MouseRegion {
pub fn on_down<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseDown, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_down(button, handler);
@@ -81,7 +79,7 @@ impl MouseRegion {
pub fn on_up<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseUp, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_up(button, handler);
@@ -90,7 +88,7 @@ impl MouseRegion {
pub fn on_click<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_click(button, handler);
@@ -99,7 +97,7 @@ impl MouseRegion {
pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_click_out(button, handler);
@@ -108,7 +106,7 @@ impl MouseRegion {
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseDownOut, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_down_out(button, handler);
@@ -117,7 +115,7 @@ impl MouseRegion {
pub fn on_up_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseUpOut, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_up_out(button, handler);
@@ -126,7 +124,7 @@ impl MouseRegion {
pub fn on_drag<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseDrag, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_drag(button, handler);
@@ -135,7 +133,7 @@ impl MouseRegion {
pub fn on_hover<V, F>(mut self, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseHover, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_hover(handler);
@@ -144,7 +142,7 @@ impl MouseRegion {
pub fn on_move<V, F>(mut self, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseMove, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_move(handler);
@@ -153,7 +151,7 @@ impl MouseRegion {
pub fn on_move_out<V, F>(mut self, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseMoveOut, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_move_out(handler);
@@ -162,7 +160,7 @@ impl MouseRegion {
pub fn on_scroll<V, F>(mut self, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseScrollWheel, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_scroll(handler);
@@ -314,7 +312,7 @@ impl HandlerSet {
pub fn on_move<V, F>(mut self, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseMove, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::move_disc(), None,
@@ -336,7 +334,7 @@ impl HandlerSet {
pub fn on_move_out<V, F>(mut self, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseMoveOut, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::move_out_disc(), None,
@@ -358,7 +356,7 @@ impl HandlerSet {
pub fn on_down<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseDown, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::down_disc(), Some(button),
@@ -380,7 +378,7 @@ impl HandlerSet {
pub fn on_up<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseUp, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::up_disc(), Some(button),
@@ -402,7 +400,7 @@ impl HandlerSet {
pub fn on_click<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::click_disc(), Some(button),
@@ -424,7 +422,7 @@ impl HandlerSet {
pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::click_out_disc(), Some(button),
@@ -446,7 +444,7 @@ impl HandlerSet {
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseDownOut, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::down_out_disc(), Some(button),
@@ -468,7 +466,7 @@ impl HandlerSet {
pub fn on_up_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseUpOut, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::up_out_disc(), Some(button),
@@ -490,7 +488,7 @@ impl HandlerSet {
pub fn on_drag<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseDrag, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::drag_disc(), Some(button),
@@ -512,7 +510,7 @@ impl HandlerSet {
pub fn on_hover<V, F>(mut self, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseHover, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::hover_disc(), None,
@@ -534,7 +532,7 @@ impl HandlerSet {
pub fn on_scroll<V, F>(mut self, handler: F) -> Self
where
- V: View,
+ V: 'static,
F: Fn(MouseScrollWheel, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::scroll_wheel_disc(), None,
@@ -0,0 +1,7 @@
+// use crate::geometry::rect::RectF;
+// use crate::WindowContext;
+
+// struct Region {
+// pub bounds: RectF,
+// pub click_handler: Option<Rc<dyn Fn(&dyn Any, MouseEvent, &mut WindowContext)>>,
+// }
@@ -212,7 +212,7 @@ pub struct Glyph {
}
impl Line {
- fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
+ pub fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
let mut style_runs = SmallVec::new();
for (len, style) in runs {
style_runs.push(StyleRun {
@@ -1,14 +1,14 @@
-use gpui::{elements::RenderElement, View, ViewContext};
-use gpui_macros::Element;
+use gpui::{elements::Empty, Element, ViewContext};
+// use gpui_macros::Element;
#[test]
fn test_derive_render_element() {
#[derive(Element)]
struct TestElement {}
- impl RenderElement for TestElement {
- fn render<V: View>(&mut self, _: &mut V, _: &mut ViewContext<V>) -> gpui::AnyElement<V> {
- unimplemented!()
+ impl TestElement {
+ fn render<V: 'static>(&mut self, _: &mut V, _: &mut ViewContext<V>) -> impl Element<V> {
+ Empty::new()
}
}
}
@@ -10,6 +10,7 @@ proc-macro = true
doctest = false
[dependencies]
+lazy_static.workspace = true
+proc-macro2 = "1.0"
syn = "1.0"
quote = "1.0"
-proc-macro2 = "1.0"
@@ -4,7 +4,7 @@ use quote::{format_ident, quote};
use std::mem;
use syn::{
parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, DeriveInput, FnArg,
- ItemFn, Lit, Meta, NestedMeta, Type,
+ GenericParam, Generics, ItemFn, Lit, Meta, NestedMeta, Type, WhereClause,
};
#[proc_macro_attribute]
@@ -278,18 +278,44 @@ fn parse_bool(literal: &Lit) -> Result<bool, TokenStream> {
#[proc_macro_derive(Element)]
pub fn element_derive(input: TokenStream) -> TokenStream {
- // Parse the input tokens into a syntax tree
- let input = parse_macro_input!(input as DeriveInput);
+ let ast = parse_macro_input!(input as DeriveInput);
+ let type_name = ast.ident;
- // The name of the struct/enum
- let name = input.ident;
- let must_implement = format_ident!("{}MustImplementRenderElement", name);
+ let placeholder_view_generics: Generics = parse_quote! { <V: 'static> };
+ let placeholder_view_type_name: Ident = parse_quote! { V };
+ let view_type_name: Ident;
+ let impl_generics: syn::ImplGenerics<'_>;
+ let type_generics: Option<syn::TypeGenerics<'_>>;
+ let where_clause: Option<&'_ WhereClause>;
- let expanded = quote! {
- trait #must_implement : gpui::elements::RenderElement {}
- impl #must_implement for #name {}
+ match ast.generics.params.iter().find_map(|param| {
+ if let GenericParam::Type(type_param) = param {
+ Some(type_param.ident.clone())
+ } else {
+ None
+ }
+ }) {
+ Some(type_name) => {
+ view_type_name = type_name;
+ let generics = ast.generics.split_for_impl();
+ impl_generics = generics.0;
+ type_generics = Some(generics.1);
+ where_clause = generics.2;
+ }
+ _ => {
+ view_type_name = placeholder_view_type_name;
+ let generics = placeholder_view_generics.split_for_impl();
+ impl_generics = generics.0;
+ type_generics = None;
+ where_clause = generics.2;
+ }
+ }
+
+ let gen = quote! {
+ impl #impl_generics Element<#view_type_name> for #type_name #type_generics
+ #where_clause
+ {
- impl<V: gpui::View> gpui::elements::Element<V> for #name {
type LayoutState = gpui::elements::AnyElement<V>;
type PaintState = ();
@@ -299,7 +325,7 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
view: &mut V,
cx: &mut gpui::LayoutContext<V>,
) -> (gpui::geometry::vector::Vector2F, gpui::elements::AnyElement<V>) {
- let mut element = self.render(view, cx);
+ let mut element = self.render(view, cx).into_any();
let size = element.layout(constraint, view, cx);
(size, element)
}
@@ -336,11 +362,11 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
_: &(),
view: &V,
cx: &gpui::ViewContext<V>,
- ) -> gpui::serde_json::Value {
+ ) -> gpui::json::Value {
element.debug(view, cx)
}
}
};
- // Return generated code
- TokenStream::from(expanded)
+
+ gen.into()
}
@@ -359,6 +359,14 @@ impl Buffer {
)
}
+ pub fn remote(remote_id: u64, replica_id: ReplicaId, base_text: String) -> Self {
+ Self::build(
+ TextBuffer::new(replica_id, remote_id, base_text),
+ None,
+ None,
+ )
+ }
+
pub fn from_proto(
replica_id: ReplicaId,
message: proto::BufferState,
@@ -2192,13 +2200,16 @@ impl BufferSnapshot {
let mut end = start;
let mut next_chars = self.chars_at(start).peekable();
let mut prev_chars = self.reversed_chars_at(start).peekable();
+
+ let language = self.language_at(start);
+ let kind = |c| char_kind(language, c);
let word_kind = cmp::max(
- prev_chars.peek().copied().map(char_kind),
- next_chars.peek().copied().map(char_kind),
+ prev_chars.peek().copied().map(kind),
+ next_chars.peek().copied().map(kind),
);
for ch in prev_chars {
- if Some(char_kind(ch)) == word_kind && ch != '\n' {
+ if Some(kind(ch)) == word_kind && ch != '\n' {
start -= ch.len_utf8();
} else {
break;
@@ -2206,7 +2217,7 @@ impl BufferSnapshot {
}
for ch in next_chars {
- if Some(char_kind(ch)) == word_kind && ch != '\n' {
+ if Some(kind(ch)) == word_kind && ch != '\n' {
end += ch.len_utf8();
} else {
break;
@@ -3003,14 +3014,18 @@ pub fn contiguous_ranges(
})
}
-pub fn char_kind(c: char) -> CharKind {
+pub fn char_kind(language: Option<&Arc<Language>>, c: char) -> CharKind {
if c.is_whitespace() {
- CharKind::Whitespace
+ return CharKind::Whitespace;
} else if c.is_alphanumeric() || c == '_' {
- CharKind::Word
- } else {
- CharKind::Punctuation
+ return CharKind::Word;
+ }
+ if let Some(language) = language {
+ if language.config.word_characters.contains(&c) {
+ return CharKind::Word;
+ }
}
+ CharKind::Punctuation
}
/// Find all of the ranges of whitespace that occur at the ends of lines
@@ -11,7 +11,7 @@ mod buffer_tests;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
use futures::{
channel::oneshot,
future::{BoxFuture, Shared},
@@ -344,6 +344,8 @@ pub struct LanguageConfig {
pub block_comment: Option<(Arc<str>, Arc<str>)>,
#[serde(default)]
pub overrides: HashMap<String, LanguageConfigOverride>,
+ #[serde(default)]
+ pub word_characters: HashSet<char>,
}
#[derive(Debug, Default)]
@@ -411,6 +413,7 @@ impl Default for LanguageConfig {
block_comment: Default::default(),
overrides: Default::default(),
collapsed_placeholder: Default::default(),
+ word_characters: Default::default(),
}
}
}
@@ -207,6 +207,7 @@ pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
}
}
+// This behavior is currently copied in the collab database, for snapshotting channel notes
pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operation> {
Ok(
match message
@@ -450,7 +450,7 @@ impl View for LspLogView {
}
impl Item for LspLogView {
- fn tab_content<V: View>(
+ fn tab_content<V: 'static>(
&self,
_: Option<usize>,
style: &theme::Tab,
@@ -373,6 +373,7 @@ impl View for SyntaxTreeView {
font_size,
font_properties: Default::default(),
underline: Default::default(),
+ soft_wrap: false,
};
let line_height = cx.font_cache().line_height(font_size);
@@ -451,7 +452,7 @@ impl View for SyntaxTreeView {
}
impl Item for SyntaxTreeView {
- fn tab_content<V: View>(
+ fn tab_content<V: 'static>(
&self,
_: Option<usize>,
style: &theme::Tab,
@@ -11,7 +11,7 @@ mod project_tests;
mod worktree_tests;
use anyhow::{anyhow, Context, Result};
-use client::{proto, Client, TypedEnvelope, UserStore};
+use client::{proto, Client, TypedEnvelope, UserId, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet};
use copilot::Copilot;
@@ -250,6 +250,7 @@ enum ProjectClientState {
pub struct Collaborator {
pub peer_id: proto::PeerId,
pub replica_id: ReplicaId,
+ pub user_id: UserId,
}
#[derive(Clone, Debug, PartialEq)]
@@ -281,6 +282,7 @@ pub enum Event {
old_peer_id: proto::PeerId,
new_peer_id: proto::PeerId,
},
+ CollaboratorJoined(proto::PeerId),
CollaboratorLeft(proto::PeerId),
RefreshInlayHints,
}
@@ -5180,7 +5182,7 @@ impl Project {
snapshot.file().map(|file| file.path().as_ref()),
) {
query
- .search(snapshot.as_rope())
+ .search(&snapshot, None)
.await
.iter()
.map(|range| {
@@ -5930,6 +5932,7 @@ impl Project {
let collaborator = Collaborator::from_proto(collaborator)?;
this.update(&mut cx, |this, cx| {
this.shared_buffers.remove(&collaborator.peer_id);
+ cx.emit(Event::CollaboratorJoined(collaborator.peer_id));
this.collaborators
.insert(collaborator.peer_id, collaborator);
cx.notify();
@@ -7756,6 +7759,7 @@ impl Collaborator {
Ok(Self {
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
replica_id: message.replica_id as ReplicaId,
+ user_id: message.user_id as UserId,
})
}
}
@@ -3,7 +3,7 @@ use anyhow::{Context, Result};
use client::proto;
use globset::{Glob, GlobMatcher};
use itertools::Itertools;
-use language::{char_kind, Rope};
+use language::{char_kind, BufferSnapshot};
use regex::{Regex, RegexBuilder};
use smol::future::yield_now;
use std::{
@@ -39,6 +39,7 @@ pub enum SearchQuery {
case_sensitive: bool,
inner: SearchInputs,
},
+
Regex {
regex: Regex,
@@ -214,12 +215,24 @@ impl SearchQuery {
}
}
- pub async fn search(&self, rope: &Rope) -> Vec<Range<usize>> {
+ pub async fn search(
+ &self,
+ buffer: &BufferSnapshot,
+ subrange: Option<Range<usize>>,
+ ) -> Vec<Range<usize>> {
const YIELD_INTERVAL: usize = 20000;
if self.as_str().is_empty() {
return Default::default();
}
+ let language = buffer.language_at(0);
+ let rope = if let Some(range) = subrange {
+ buffer.as_rope().slice(range)
+ } else {
+ buffer.as_rope().clone()
+ };
+
+ let kind = |c| char_kind(language, c);
let mut matches = Vec::new();
match self {
@@ -236,10 +249,10 @@ impl SearchQuery {
let mat = mat.unwrap();
if *whole_word {
- let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind);
- let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap());
- let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap());
- let next_kind = rope.chars_at(mat.end()).next().map(char_kind);
+ let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind);
+ let start_kind = kind(rope.chars_at(mat.start()).next().unwrap());
+ let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap());
+ let next_kind = rope.chars_at(mat.end()).next().map(kind);
if Some(start_kind) == prev_kind || Some(end_kind) == next_kind {
continue;
}
@@ -247,6 +260,7 @@ impl SearchQuery {
matches.push(mat.start()..mat.end())
}
}
+
Self::Regex {
regex, multiline, ..
} => {
@@ -284,6 +298,7 @@ impl SearchQuery {
}
}
}
+
matches
}
@@ -1320,7 +1320,7 @@ impl ProjectPanel {
}
}
- fn render_entry_visual_element<V: View>(
+ fn render_entry_visual_element<V: 'static>(
details: &EntryDetails,
editor: Option<&ViewHandle<Editor>>,
padding: f32,
@@ -3,7 +3,7 @@ use std::path::Path;
use fuzzy::StringMatch;
use gpui::{
elements::{Label, LabelStyle},
- AnyElement, Element, View,
+ AnyElement, Element,
};
use util::paths::PathExt;
use workspace::WorkspaceLocation;
@@ -43,7 +43,7 @@ impl HighlightedText {
}
}
- pub fn render<V: View>(self, style: impl Into<LabelStyle>) -> AnyElement<V> {
+ pub fn render<V: 'static>(self, style: impl Into<LabelStyle>) -> AnyElement<V> {
Label::new(self.text, style)
.with_highlights(self.highlight_positions)
.into_any()
@@ -0,0 +1,15 @@
+[package]
+name = "refineable"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/refineable.rs"
+doctest = false
+
+[dependencies]
+syn = "1.0.72"
+quote = "1.0.9"
+proc-macro2 = "1.0.66"
+derive_refineable = { path = "./derive_refineable" }
@@ -0,0 +1,15 @@
+[package]
+name = "derive_refineable"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/derive_refineable.rs"
+proc-macro = true
+doctest = false
+
+[dependencies]
+syn = "1.0.72"
+quote = "1.0.9"
+proc-macro2 = "1.0.66"
@@ -0,0 +1,188 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{
+ parse_macro_input, parse_quote, DeriveInput, Field, FieldsNamed, PredicateType, TraitBound,
+ Type, TypeParamBound, WhereClause, WherePredicate,
+};
+
+#[proc_macro_derive(Refineable, attributes(refineable))]
+pub fn derive_refineable(input: TokenStream) -> TokenStream {
+ let DeriveInput {
+ ident,
+ data,
+ generics,
+ ..
+ } = parse_macro_input!(input);
+
+ let refinement_ident = format_ident!("{}Refinement", ident);
+ let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+
+ let fields = match data {
+ syn::Data::Struct(syn::DataStruct {
+ fields: syn::Fields::Named(FieldsNamed { named, .. }),
+ ..
+ }) => named.into_iter().collect::<Vec<Field>>(),
+ _ => panic!("This derive macro only supports structs with named fields"),
+ };
+
+ let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect();
+ let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect();
+ let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect();
+
+ // Create trait bound that each wrapped type must implement Clone & Default
+ let type_param_bounds: Vec<_> = wrapped_types
+ .iter()
+ .map(|ty| {
+ WherePredicate::Type(PredicateType {
+ lifetimes: None,
+ bounded_ty: ty.clone(),
+ colon_token: Default::default(),
+ bounds: {
+ let mut punctuated = syn::punctuated::Punctuated::new();
+ punctuated.push_value(TypeParamBound::Trait(TraitBound {
+ paren_token: None,
+ modifier: syn::TraitBoundModifier::None,
+ lifetimes: None,
+ path: parse_quote!(Clone),
+ }));
+ punctuated.push_punct(syn::token::Add::default());
+ punctuated.push_value(TypeParamBound::Trait(TraitBound {
+ paren_token: None,
+ modifier: syn::TraitBoundModifier::None,
+ lifetimes: None,
+ path: parse_quote!(Default),
+ }));
+ punctuated
+ },
+ })
+ })
+ .collect();
+
+ // Append to where_clause or create a new one if it doesn't exist
+ let where_clause = match where_clause.cloned() {
+ Some(mut where_clause) => {
+ where_clause
+ .predicates
+ .extend(type_param_bounds.into_iter());
+ where_clause.clone()
+ }
+ None => WhereClause {
+ where_token: Default::default(),
+ predicates: type_param_bounds.into_iter().collect(),
+ },
+ };
+
+ let field_assignments: Vec<TokenStream2> = fields
+ .iter()
+ .map(|field| {
+ let name = &field.ident;
+ let is_refineable = is_refineable_field(field);
+ let is_optional = is_optional_field(field);
+
+ if is_refineable {
+ quote! {
+ self.#name.refine(&refinement.#name);
+ }
+ } else if is_optional {
+ quote! {
+ if let Some(ref value) = &refinement.#name {
+ self.#name = Some(value.clone());
+ }
+ }
+ } else {
+ quote! {
+ if let Some(ref value) = &refinement.#name {
+ self.#name = value.clone();
+ }
+ }
+ }
+ })
+ .collect();
+
+ let refinement_field_assignments: Vec<TokenStream2> = fields
+ .iter()
+ .map(|field| {
+ let name = &field.ident;
+ let is_refineable = is_refineable_field(field);
+
+ if is_refineable {
+ quote! {
+ self.#name.refine(&refinement.#name);
+ }
+ } else {
+ quote! {
+ if let Some(ref value) = &refinement.#name {
+ self.#name = Some(value.clone());
+ }
+ }
+ }
+ })
+ .collect();
+
+ let gen = quote! {
+ #[derive(Default, Clone)]
+ pub struct #refinement_ident #impl_generics {
+ #( #field_visibilities #field_names: #wrapped_types ),*
+ }
+
+ impl #impl_generics Refineable for #ident #ty_generics
+ #where_clause
+ {
+ type Refinement = #refinement_ident #ty_generics;
+
+ fn refine(&mut self, refinement: &Self::Refinement) {
+ #( #field_assignments )*
+ }
+ }
+
+ impl #impl_generics Refineable for #refinement_ident #ty_generics
+ #where_clause
+ {
+ type Refinement = #refinement_ident #ty_generics;
+
+ fn refine(&mut self, refinement: &Self::Refinement) {
+ #( #refinement_field_assignments )*
+ }
+ }
+ };
+
+ gen.into()
+}
+
+fn is_refineable_field(f: &Field) -> bool {
+ f.attrs.iter().any(|attr| attr.path.is_ident("refineable"))
+}
+
+fn is_optional_field(f: &Field) -> bool {
+ if let Type::Path(typepath) = &f.ty {
+ if typepath.qself.is_none() {
+ let segments = &typepath.path.segments;
+ if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") {
+ return true;
+ }
+ }
+ }
+ false
+}
+
+fn get_wrapper_type(field: &Field, ty: &Type) -> syn::Type {
+ if is_refineable_field(field) {
+ let struct_name = if let Type::Path(tp) = ty {
+ tp.path.segments.last().unwrap().ident.clone()
+ } else {
+ panic!("Expected struct type for a refineable field");
+ };
+ let refinement_struct_name = format_ident!("{}Refinement", struct_name);
+ let generics = if let Type::Path(tp) = ty {
+ &tp.path.segments.last().unwrap().arguments
+ } else {
+ &syn::PathArguments::None
+ };
+ parse_quote!(#refinement_struct_name #generics)
+ } else if is_optional_field(field) {
+ ty.clone()
+ } else {
+ parse_quote!(Option<#ty>)
+ }
+}
@@ -0,0 +1,14 @@
+pub use derive_refineable::Refineable;
+
+pub trait Refineable {
+ type Refinement: Default;
+
+ fn refine(&mut self, refinement: &Self::Refinement);
+ fn refined(mut self, refinement: &Self::Refinement) -> Self
+ where
+ Self: Sized,
+ {
+ self.refine(refinement);
+ self
+ }
+}
@@ -23,7 +23,7 @@ async-tungstenite = "0.16"
base64 = "0.13"
futures.workspace = true
parking_lot.workspace = true
-prost = "0.8"
+prost.workspace = true
rand.workspace = true
rsa = "0.4"
serde.workspace = true
@@ -142,6 +142,13 @@ message Envelope {
GetChannelMembersResponse get_channel_members_response = 128;
SetChannelMemberAdmin set_channel_member_admin = 129;
RenameChannel rename_channel = 130;
+
+ JoinChannelBuffer join_channel_buffer = 131;
+ JoinChannelBufferResponse join_channel_buffer_response = 132;
+ UpdateChannelBuffer update_channel_buffer = 133;
+ LeaveChannelBuffer leave_channel_buffer = 134;
+ AddChannelBufferCollaborator add_channel_buffer_collaborator = 135;
+ RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
}
}
@@ -411,6 +418,16 @@ message RemoveProjectCollaborator {
PeerId peer_id = 2;
}
+message AddChannelBufferCollaborator {
+ uint64 channel_id = 1;
+ Collaborator collaborator = 2;
+}
+
+message RemoveChannelBufferCollaborator {
+ uint64 channel_id = 1;
+ PeerId peer_id = 2;
+}
+
message GetDefinition {
uint64 project_id = 1;
uint64 buffer_id = 2;
@@ -540,6 +557,11 @@ message UpdateBuffer {
repeated Operation operations = 3;
}
+message UpdateChannelBuffer {
+ uint64 channel_id = 1;
+ repeated Operation operations = 2;
+}
+
message UpdateBufferFile {
uint64 project_id = 1;
uint64 buffer_id = 2;
@@ -948,6 +970,22 @@ message RenameChannel {
string name = 2;
}
+message JoinChannelBuffer {
+ uint64 channel_id = 1;
+}
+
+message JoinChannelBufferResponse {
+ uint64 buffer_id = 1;
+ uint32 replica_id = 2;
+ string base_text = 3;
+ repeated Operation operations = 4;
+ repeated Collaborator collaborators = 5;
+}
+
+message LeaveChannelBuffer {
+ uint64 channel_id = 1;
+}
+
message RespondToChannelInvite {
uint64 channel_id = 1;
bool accept = 2;
@@ -1082,6 +1120,7 @@ message View {
oneof variant {
Editor editor = 3;
+ ChannelView channel_view = 4;
}
message Editor {
@@ -1094,6 +1133,11 @@ message View {
float scroll_x = 7;
float scroll_y = 8;
}
+
+ message ChannelView {
+ uint64 channel_id = 1;
+ Editor editor = 2;
+ }
}
message Collaborator {
@@ -1144,7 +1188,6 @@ enum GitStatus {
Conflict = 2;
}
-
message BufferState {
uint64 id = 1;
optional File file = 2;
@@ -248,7 +248,13 @@ messages!(
(GetPrivateUserInfo, Foreground),
(GetPrivateUserInfoResponse, Foreground),
(GetChannelMembers, Foreground),
- (GetChannelMembersResponse, Foreground)
+ (GetChannelMembersResponse, Foreground),
+ (JoinChannelBuffer, Foreground),
+ (JoinChannelBufferResponse, Foreground),
+ (LeaveChannelBuffer, Background),
+ (UpdateChannelBuffer, Foreground),
+ (RemoveChannelBufferCollaborator, Foreground),
+ (AddChannelBufferCollaborator, Foreground),
);
request_messages!(
@@ -315,6 +321,8 @@ request_messages!(
(UpdateParticipantLocation, Ack),
(UpdateProject, Ack),
(UpdateWorktree, Ack),
+ (JoinChannelBuffer, JoinChannelBufferResponse),
+ (LeaveChannelBuffer, Ack)
);
entity_messages!(
@@ -370,6 +378,13 @@ entity_messages!(
UpdateDiffBase
);
+entity_messages!(
+ channel_id,
+ UpdateChannelBuffer,
+ RemoveChannelBufferCollaborator,
+ AddChannelBufferCollaborator
+);
+
const KIB: usize = 1024;
const MIB: usize = KIB * 1024;
const MAX_BUFFER_LEN: usize = MIB;
@@ -482,7 +482,7 @@ impl Item for ProjectSearchView {
.update(cx, |editor, cx| editor.deactivated(cx));
}
- fn tab_content<T: View>(
+ fn tab_content<T: 'static>(
&self,
_detail: Option<usize>,
tab_theme: &theme::Tab,
@@ -2,13 +2,13 @@ use bitflags::bitflags;
pub use buffer_search::BufferSearchBar;
use gpui::{
actions,
- elements::{Component, StyleableComponent, TooltipStyle},
+ elements::{Component, SafeStylable, TooltipStyle},
Action, AnyElement, AppContext, Element, View,
};
pub use mode::SearchMode;
use project::search::SearchQuery;
pub use project_search::{ProjectSearchBar, ProjectSearchView};
-use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle};
+use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle};
pub mod buffer_search;
mod history;
@@ -89,15 +89,12 @@ impl SearchOptions {
tooltip_style: TooltipStyle,
button_style: ToggleIconButtonStyle,
) -> AnyElement<V> {
- ActionButton::new_dynamic(
- self.to_toggle_action(),
- format!("Toggle {}", self.label()),
- tooltip_style,
- )
- .with_contents(theme::components::svg::Svg::new(self.icon()))
- .toggleable(active)
- .with_style(button_style)
- .element()
- .into_any()
+ Button::dynamic_action(self.to_toggle_action())
+ .with_tooltip(format!("Toggle {}", self.label()), tooltip_style)
+ .with_contents(Svg::new(self.icon()))
+ .toggleable(active)
+ .with_style(button_style)
+ .element()
+ .into_any()
}
}
@@ -2,7 +2,7 @@ use std::{cmp::Ordering, fmt::Debug};
use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary};
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, PartialEq, Eq)]
pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
where
K: Clone + Debug + Default + Ord,
@@ -162,6 +162,16 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
}
}
+impl<K: Debug, V: Debug> Debug for TreeMap<K, V>
+where
+ K: Clone + Debug + Default + Ord,
+ V: Clone + Debug,
+{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_map().entries(self.iter()).finish()
+ }
+}
+
#[derive(Debug)]
struct MapSeekTargetAdaptor<'a, T>(&'a T);
@@ -567,6 +567,7 @@ impl Element<TerminalView> for TerminalElement {
font_size,
font_properties: Default::default(),
underline: Default::default(),
+ soft_wrap: false,
};
let selection_color = settings.theme.editor.selection.selection;
let match_color = settings.theme.search.match_background;
@@ -661,7 +661,7 @@ impl Item for TerminalView {
Some(self.terminal().read(cx).title().into())
}
- fn tab_content<T: View>(
+ fn tab_content<T: 'static>(
&self,
_detail: Option<usize>,
tab_theme: &theme::Tab,
@@ -12,7 +12,7 @@ mod undo_map;
pub use anchor::*;
use anyhow::{anyhow, Result};
-use clock::ReplicaId;
+pub use clock::ReplicaId;
use collections::{HashMap, HashSet};
use fs::LineEnding;
use locator::Locator;
@@ -1,23 +1,143 @@
-use gpui::elements::StyleableComponent;
+use gpui::{elements::SafeStylable, Action};
use crate::{Interactive, Toggleable};
-use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle};
+use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle};
-pub type ToggleIconButtonStyle = Toggleable<Interactive<ButtonStyle<SvgStyle>>>;
+pub type IconButtonStyle = Interactive<ButtonStyle<SvgStyle>>;
+pub type ToggleIconButtonStyle = Toggleable<IconButtonStyle>;
-pub trait ComponentExt<C: StyleableComponent> {
+pub trait ComponentExt<C: SafeStylable> {
fn toggleable(self, active: bool) -> Toggle<C, ()>;
+ fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()>;
}
-impl<C: StyleableComponent> ComponentExt<C> for C {
+impl<C: SafeStylable> ComponentExt<C> for C {
fn toggleable(self, active: bool) -> Toggle<C, ()> {
Toggle::new(self, active)
}
+
+ /// Some(True) => disclosed => content is visible
+ /// Some(false) => closed => content is hidden
+ /// None => No disclosure button, but reserve disclosure spacing
+ fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()> {
+ Disclosable::new(disclosed, self, action)
+ }
+}
+
+pub mod disclosure {
+
+ use gpui::{
+ elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable},
+ Action, Element,
+ };
+ use schemars::JsonSchema;
+ use serde_derive::Deserialize;
+
+ use super::{action_button::Button, svg::Svg, IconButtonStyle};
+
+ #[derive(Clone, Default, Deserialize, JsonSchema)]
+ pub struct DisclosureStyle<S> {
+ pub button: IconButtonStyle,
+ #[serde(flatten)]
+ pub container: ContainerStyle,
+ pub spacing: f32,
+ #[serde(flatten)]
+ content: S,
+ }
+
+ impl<S> DisclosureStyle<S> {
+ pub fn button_space(&self) -> f32 {
+ self.spacing + self.button.button_width.unwrap()
+ }
+ }
+
+ pub struct Disclosable<C, S> {
+ disclosed: Option<bool>,
+ action: Box<dyn Action>,
+ id: usize,
+ content: C,
+ style: S,
+ }
+
+ impl Disclosable<(), ()> {
+ pub fn new<C>(
+ disclosed: Option<bool>,
+ content: C,
+ action: Box<dyn Action>,
+ ) -> Disclosable<C, ()> {
+ Disclosable {
+ disclosed,
+ content,
+ action,
+ id: 0,
+ style: (),
+ }
+ }
+ }
+
+ impl<C> Disclosable<C, ()> {
+ pub fn with_id(mut self, id: usize) -> Disclosable<C, ()> {
+ self.id = id;
+ self
+ }
+ }
+
+ impl<C: SafeStylable> SafeStylable for Disclosable<C, ()> {
+ type Style = DisclosureStyle<C::Style>;
+
+ type Output = Disclosable<C, Self::Style>;
+
+ fn with_style(self, style: Self::Style) -> Self::Output {
+ Disclosable {
+ disclosed: self.disclosed,
+ action: self.action,
+ content: self.content,
+ id: self.id,
+ style,
+ }
+ }
+ }
+
+ impl<C: SafeStylable> Component for Disclosable<C, DisclosureStyle<C::Style>> {
+ fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+ Flex::row()
+ .with_spacing(self.style.spacing)
+ .with_child(if let Some(disclosed) = self.disclosed {
+ Button::dynamic_action(self.action)
+ .with_id(self.id)
+ .with_contents(Svg::new(if disclosed {
+ "icons/file_icons/chevron_down.svg"
+ } else {
+ "icons/file_icons/chevron_right.svg"
+ }))
+ .with_style(self.style.button)
+ .element()
+ .into_any()
+ } else {
+ Empty::new()
+ .into_any()
+ .constrained()
+ // TODO: Why is this optional at all?
+ .with_width(self.style.button.button_width.unwrap())
+ .into_any()
+ })
+ .with_child(
+ self.content
+ .with_style(self.style.content)
+ .render(cx)
+ .flex(1., true),
+ )
+ .align_children_center()
+ .contained()
+ .with_style(self.style.container)
+ .into_any()
+ }
+ }
}
pub mod toggle {
- use gpui::elements::{GeneralComponent, StyleableComponent};
+ use gpui::elements::{Component, SafeStylable};
use crate::Toggleable;
@@ -27,7 +147,7 @@ pub mod toggle {
component: C,
}
- impl<C: StyleableComponent> Toggle<C, ()> {
+ impl<C: SafeStylable> Toggle<C, ()> {
pub fn new(component: C, active: bool) -> Self {
Toggle {
active,
@@ -37,7 +157,7 @@ pub mod toggle {
}
}
- impl<C: StyleableComponent> StyleableComponent for Toggle<C, ()> {
+ impl<C: SafeStylable> SafeStylable for Toggle<C, ()> {
type Style = Toggleable<C::Style>;
type Output = Toggle<C, Self::Style>;
@@ -51,15 +171,11 @@ pub mod toggle {
}
}
- impl<C: StyleableComponent> GeneralComponent for Toggle<C, Toggleable<C::Style>> {
- fn render<V: gpui::View>(
- self,
- v: &mut V,
- cx: &mut gpui::ViewContext<V>,
- ) -> gpui::AnyElement<V> {
+ impl<C: SafeStylable> Component for Toggle<C, Toggleable<C::Style>> {
+ fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
self.component
.with_style(self.style.in_state(self.active).clone())
- .render(v, cx)
+ .render(cx)
}
}
}
@@ -68,96 +184,103 @@ pub mod action_button {
use std::borrow::Cow;
use gpui::{
- elements::{
- ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle,
- },
+ elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle},
platform::{CursorStyle, MouseButton},
- Action, Element, TypeTag, View,
+ Action, Element, TypeTag,
};
use schemars::JsonSchema;
use serde_derive::Deserialize;
use crate::Interactive;
- pub struct ActionButton<C, S> {
- action: Box<dyn Action>,
- tooltip: Cow<'static, str>,
- tooltip_style: TooltipStyle,
- tag: TypeTag,
- contents: C,
- style: Interactive<S>,
- }
-
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct ButtonStyle<C> {
#[serde(flatten)]
- container: ContainerStyle,
- button_width: Option<f32>,
- button_height: Option<f32>,
+ pub container: ContainerStyle,
+ // TODO: These are incorrect for the intended usage of the buttons.
+ // The size should be constant, but putting them here duplicates them
+ // across the states the buttons can be in
+ pub button_width: Option<f32>,
+ pub button_height: Option<f32>,
#[serde(flatten)]
contents: C,
}
- impl ActionButton<(), ()> {
- pub fn new_dynamic(
- action: Box<dyn Action>,
- tooltip: impl Into<Cow<'static, str>>,
- tooltip_style: TooltipStyle,
- ) -> Self {
+ pub struct Button<C, S> {
+ action: Box<dyn Action>,
+ tooltip: Option<(Cow<'static, str>, TooltipStyle)>,
+ tag: TypeTag,
+ id: usize,
+ contents: C,
+ style: Interactive<S>,
+ }
+
+ impl Button<(), ()> {
+ pub fn dynamic_action(action: Box<dyn Action>) -> Button<(), ()> {
Self {
contents: (),
tag: action.type_tag(),
- style: Interactive::new_blank(),
- tooltip: tooltip.into(),
- tooltip_style,
action,
+ style: Interactive::new_blank(),
+ tooltip: None,
+ id: 0,
}
}
- pub fn new<A: Action + Clone>(
- action: A,
+ pub fn action<A: Action + Clone>(action: A) -> Self {
+ Self::dynamic_action(Box::new(action))
+ }
+
+ pub fn with_tooltip(
+ mut self,
tooltip: impl Into<Cow<'static, str>>,
tooltip_style: TooltipStyle,
) -> Self {
- Self::new_dynamic(Box::new(action), tooltip, tooltip_style)
+ self.tooltip = Some((tooltip.into(), tooltip_style));
+ self
+ }
+
+ pub fn with_id(mut self, id: usize) -> Self {
+ self.id = id;
+ self
}
- pub fn with_contents<C: StyleableComponent>(self, contents: C) -> ActionButton<C, ()> {
- ActionButton {
+ pub fn with_contents<C: SafeStylable>(self, contents: C) -> Button<C, ()> {
+ Button {
action: self.action,
tag: self.tag,
style: self.style,
tooltip: self.tooltip,
- tooltip_style: self.tooltip_style,
+ id: self.id,
contents,
}
}
}
- impl<C: StyleableComponent> StyleableComponent for ActionButton<C, ()> {
+ impl<C: SafeStylable> SafeStylable for Button<C, ()> {
type Style = Interactive<ButtonStyle<C::Style>>;
- type Output = ActionButton<C, ButtonStyle<C::Style>>;
+ type Output = Button<C, ButtonStyle<C::Style>>;
fn with_style(self, style: Self::Style) -> Self::Output {
- ActionButton {
+ Button {
action: self.action,
tag: self.tag,
contents: self.contents,
tooltip: self.tooltip,
- tooltip_style: self.tooltip_style,
+ id: self.id,
style,
}
}
}
- impl<C: StyleableComponent> GeneralComponent for ActionButton<C, ButtonStyle<C::Style>> {
- fn render<V: View>(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
- MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| {
+ impl<C: SafeStylable> Component for Button<C, ButtonStyle<C::Style>> {
+ fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+ let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| {
let style = self.style.style_for(state);
let mut contents = self
.contents
.with_style(style.contents.to_owned())
- .render(v, cx)
+ .render(cx)
.contained()
.with_style(style.container)
.constrained();
@@ -185,15 +308,15 @@ pub mod action_button {
}
})
.with_cursor_style(CursorStyle::PointingHand)
- .with_dynamic_tooltip(
- self.tag,
- 0,
- self.tooltip,
- Some(self.action),
- self.tooltip_style,
- cx,
- )
- .into_any()
+ .into_any();
+
+ if let Some((tooltip, style)) = self.tooltip {
+ button = button
+ .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx)
+ .into_any()
+ }
+
+ button
}
}
}
@@ -202,7 +325,7 @@ pub mod svg {
use std::borrow::Cow;
use gpui::{
- elements::{GeneralComponent, StyleableComponent},
+ elements::{Component, Empty, SafeStylable},
Element,
};
use schemars::JsonSchema;
@@ -225,6 +348,7 @@ pub mod svg {
pub enum IconSize {
IconSize { icon_size: f32 },
Dimensions { width: f32, height: f32 },
+ IconDimensions { icon_width: f32, icon_height: f32 },
}
#[derive(Deserialize)]
@@ -248,6 +372,14 @@ pub mod svg {
icon_height: height,
color,
},
+ IconSize::IconDimensions {
+ icon_width,
+ icon_height,
+ } => SvgStyle {
+ icon_width,
+ icon_height,
+ color,
+ },
};
Ok(result)
@@ -255,20 +387,27 @@ pub mod svg {
}
pub struct Svg<S> {
- path: Cow<'static, str>,
+ path: Option<Cow<'static, str>>,
style: S,
}
impl Svg<()> {
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
Self {
- path: path.into(),
+ path: Some(path.into()),
+ style: (),
+ }
+ }
+
+ pub fn optional(path: Option<impl Into<Cow<'static, str>>>) -> Self {
+ Self {
+ path: path.map(Into::into),
style: (),
}
}
}
- impl StyleableComponent for Svg<()> {
+ impl SafeStylable for Svg<()> {
type Style = SvgStyle;
type Output = Svg<SvgStyle>;
@@ -281,18 +420,19 @@ pub mod svg {
}
}
- impl GeneralComponent for Svg<SvgStyle> {
- fn render<V: gpui::View>(
- self,
- _: &mut V,
- _: &mut gpui::ViewContext<V>,
- ) -> gpui::AnyElement<V> {
- gpui::elements::Svg::new(self.path)
- .with_color(self.style.color)
- .constrained()
- .with_width(self.style.icon_width)
- .with_height(self.style.icon_height)
- .into_any()
+ impl Component for Svg<SvgStyle> {
+ fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+ if let Some(path) = self.path {
+ gpui::elements::Svg::new(path)
+ .with_color(self.style.color)
+ .constrained()
+ } else {
+ Empty::new().constrained()
+ }
+ .constrained()
+ .with_width(self.style.icon_width)
+ .with_height(self.style.icon_height)
+ .into_any()
}
}
}
@@ -301,7 +441,8 @@ pub mod label {
use std::borrow::Cow;
use gpui::{
- elements::{GeneralComponent, LabelStyle, StyleableComponent},
+ elements::{Component, LabelStyle, SafeStylable},
+ fonts::TextStyle,
Element,
};
@@ -319,25 +460,21 @@ pub mod label {
}
}
- impl StyleableComponent for Label<()> {
- type Style = LabelStyle;
+ impl SafeStylable for Label<()> {
+ type Style = TextStyle;
type Output = Label<LabelStyle>;
fn with_style(self, style: Self::Style) -> Self::Output {
Label {
text: self.text,
- style,
+ style: style.into(),
}
}
}
- impl GeneralComponent for Label<LabelStyle> {
- fn render<V: gpui::View>(
- self,
- _: &mut V,
- _: &mut gpui::ViewContext<V>,
- ) -> gpui::AnyElement<V> {
+ impl Component for Label<LabelStyle> {
+ fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
gpui::elements::Label::new(self.text, self.style).into_any()
}
}
@@ -3,7 +3,7 @@ mod theme_registry;
mod theme_settings;
pub mod ui;
-use components::ToggleIconButtonStyle;
+use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle};
use gpui::{
color::Color,
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@@ -14,7 +14,7 @@ use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use settings::SettingsStore;
-use std::{collections::HashMap, sync::Arc};
+use std::{collections::HashMap, ops::Deref, sync::Arc};
use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
pub use theme_registry::*;
@@ -66,6 +66,7 @@ pub struct Theme {
pub feedback: FeedbackStyle,
pub welcome: WelcomeStyle,
pub titlebar: Titlebar,
+ pub component_test: ComponentTest,
}
#[derive(Deserialize, Default, Clone, JsonSchema)]
@@ -221,6 +222,7 @@ pub struct CopilotAuthAuthorized {
pub struct CollabPanel {
#[serde(flatten)]
pub container: ContainerStyle,
+ pub disclosure: DisclosureStyle<()>,
pub list_empty_state: Toggleable<Interactive<ContainedText>>,
pub list_empty_icon: Icon,
pub list_empty_label_container: ContainerStyle,
@@ -259,6 +261,13 @@ pub struct CollabPanel {
pub face_overlap: f32,
}
+#[derive(Deserialize, Default, JsonSchema)]
+pub struct ComponentTest {
+ pub button: Interactive<ButtonStyle<TextStyle>>,
+ pub toggle: Toggleable<Interactive<ButtonStyle<TextStyle>>>,
+ pub disclosure: DisclosureStyle<TextStyle>,
+}
+
#[derive(Deserialize, Default, JsonSchema)]
pub struct TabbedModal {
pub tab_button: Toggleable<Interactive<ContainedText>>,
@@ -747,6 +756,7 @@ pub struct Editor {
pub line_number: Color,
pub line_number_active: Color,
pub guest_selections: Vec<SelectionStyle>,
+ pub absent_selection: SelectionStyle,
pub syntax: Arc<SyntaxTheme>,
pub hint: HighlightStyle,
pub suggestion: HighlightStyle,
@@ -890,6 +900,14 @@ pub struct Interactive<T> {
pub disabled: Option<T>,
}
+impl<T> Deref for Interactive<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.default
+ }
+}
+
impl Interactive<()> {
pub fn new_blank() -> Self {
Self {
@@ -907,6 +925,14 @@ pub struct Toggleable<T> {
inactive: T,
}
+impl<T> Deref for Toggleable<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inactive
+ }
+}
+
impl Toggleable<()> {
pub fn new_blank() -> Self {
Self {
@@ -10,7 +10,7 @@ use gpui::{
platform,
platform::MouseButton,
scene::MouseClick,
- Action, Element, EventContext, MouseState, View, ViewContext,
+ Action, Element, EventContext, MouseState, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
@@ -37,7 +37,7 @@ pub fn checkbox<Tag, V, F>(
) -> MouseEventHandler<V>
where
Tag: 'static,
- V: View,
+ V: 'static,
F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
{
let label = Label::new(label, style.label.text.clone())
@@ -57,7 +57,7 @@ pub fn checkbox_with_label<Tag, D, V, F>(
where
Tag: 'static,
D: Element<V>,
- V: View,
+ V: 'static,
F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
{
MouseEventHandler::new::<Tag, _>(id, cx, |state, _| {
@@ -93,7 +93,7 @@ where
.with_cursor_style(platform::CursorStyle::PointingHand)
}
-pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
+pub fn svg<V: 'static>(style: &SvgStyle) -> ConstrainedBox<V> {
Svg::new(style.asset.clone())
.with_color(style.color)
.constrained()
@@ -117,11 +117,11 @@ impl IconStyle {
}
}
-pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
+pub fn icon<V: 'static>(style: &IconStyle) -> Container<V> {
svg(&style.icon).contained().with_style(style.container)
}
-pub fn keystroke_label<V: View>(
+pub fn keystroke_label<V: 'static>(
label_text: &'static str,
label_style: &ContainedText,
keystroke_style: &ContainedText,
@@ -157,7 +157,7 @@ pub fn cta_button<Tag, L, V, F>(
where
Tag: 'static,
L: Into<Cow<'static, str>>,
- V: View,
+ V: 'static,
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
{
MouseEventHandler::new::<Tag, _>(0, cx, |state, _| {
@@ -196,9 +196,9 @@ pub fn modal<Tag, V, I, D, F>(
) -> impl Element<V>
where
Tag: 'static,
- V: View,
I: Into<Cow<'static, str>>,
D: Element<V>,
+ V: 'static,
F: FnOnce(&mut gpui::ViewContext<V>) -> D,
{
const TITLEBAR_HEIGHT: f32 = 28.;
@@ -439,11 +439,12 @@ pub(crate) fn next_word_start(
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
+ let language = map.buffer_snapshot.language_at(point.to_point(map));
for _ in 0..times {
let mut crossed_newline = false;
point = movement::find_boundary(map, point, |left, right| {
- let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
let at_newline = right == '\n';
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
@@ -463,11 +464,12 @@ fn next_word_end(
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
+ let language = map.buffer_snapshot.language_at(point.to_point(map));
for _ in 0..times {
*point.column_mut() += 1;
point = movement::find_boundary(map, point, |left, right| {
- let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind && left_kind != CharKind::Whitespace
});
@@ -493,12 +495,13 @@ fn previous_word_start(
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
+ let language = map.buffer_snapshot.language_at(point.to_point(map));
for _ in 0..times {
// This works even though find_preceding_boundary is called for every character in the line containing
// cursor because the newline is checked only once.
point = movement::find_preceding_boundary(map, point, |left, right| {
- let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
});
@@ -508,6 +511,7 @@ fn previous_word_start(
fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
let mut last_point = DisplayPoint::new(from.row(), 0);
+ let language = map.buffer_snapshot.language_at(from.to_point(map));
for (ch, point) in map.chars_at(last_point) {
if ch == '\n' {
return from;
@@ -515,7 +519,7 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi
last_point = point;
- if char_kind(ch) != CharKind::Whitespace {
+ if char_kind(language, ch) != CharKind::Whitespace {
break;
}
}
@@ -1,12 +1,13 @@
mod case;
mod change;
mod delete;
+mod paste;
mod scroll;
mod search;
pub mod substitute;
mod yank;
-use std::{borrow::Cow, sync::Arc};
+use std::sync::Arc;
use crate::{
motion::Motion,
@@ -14,13 +15,11 @@ use crate::{
state::{Mode, Operator},
Vim,
};
-use collections::{HashMap, HashSet};
-use editor::{
- display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection,
- DisplayPoint,
-};
+use collections::HashSet;
+use editor::scroll::autoscroll::Autoscroll;
+use editor::{Bias, DisplayPoint};
use gpui::{actions, AppContext, ViewContext, WindowContext};
-use language::{AutoindentMode, Point, SelectionGoal};
+use language::SelectionGoal;
use log::error;
use workspace::Workspace;
@@ -44,7 +43,6 @@ actions!(
DeleteRight,
ChangeToEndOfLine,
DeleteToEndOfLine,
- Paste,
Yank,
Substitute,
ChangeCase,
@@ -89,9 +87,8 @@ pub fn init(cx: &mut AppContext) {
delete_motion(vim, Motion::EndOfLine, times, cx);
})
});
- cx.add_action(paste);
-
scroll::init(cx);
+ paste::init(cx);
}
pub fn normal_motion(
@@ -250,144 +247,6 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
});
}
-fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
- Vim::update(cx, |vim, cx| {
- vim.update_active_editor(cx, |editor, cx| {
- editor.transact(cx, |editor, cx| {
- editor.set_clip_at_line_ends(false, cx);
- if let Some(item) = cx.read_from_clipboard() {
- let mut clipboard_text = Cow::Borrowed(item.text());
- if let Some(mut clipboard_selections) =
- item.metadata::<Vec<ClipboardSelection>>()
- {
- let (display_map, selections) = editor.selections.all_display(cx);
- let all_selections_were_entire_line =
- clipboard_selections.iter().all(|s| s.is_entire_line);
- if clipboard_selections.len() != selections.len() {
- let mut newline_separated_text = String::new();
- let mut clipboard_selections =
- clipboard_selections.drain(..).peekable();
- let mut ix = 0;
- while let Some(clipboard_selection) = clipboard_selections.next() {
- newline_separated_text
- .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
- ix += clipboard_selection.len;
- if clipboard_selections.peek().is_some() {
- newline_separated_text.push('\n');
- }
- }
- clipboard_text = Cow::Owned(newline_separated_text);
- }
-
- // If the pasted text is a single line, the cursor should be placed after
- // the newly pasted text. This is easiest done with an anchor after the
- // insertion, and then with a fixup to move the selection back one position.
- // However if the pasted text is linewise, the cursor should be placed at the start
- // of the new text on the following line. This is easiest done with a manually adjusted
- // point.
- // This enum lets us represent both cases
- enum NewPosition {
- Inside(Point),
- After(Anchor),
- }
- let mut new_selections: HashMap<usize, NewPosition> = Default::default();
- editor.buffer().update(cx, |buffer, cx| {
- let snapshot = buffer.snapshot(cx);
- let mut start_offset = 0;
- let mut edits = Vec::new();
- for (ix, selection) in selections.iter().enumerate() {
- let to_insert;
- let linewise;
- if let Some(clipboard_selection) = clipboard_selections.get(ix) {
- let end_offset = start_offset + clipboard_selection.len;
- to_insert = &clipboard_text[start_offset..end_offset];
- linewise = clipboard_selection.is_entire_line;
- start_offset = end_offset;
- } else {
- to_insert = clipboard_text.as_str();
- linewise = all_selections_were_entire_line;
- }
-
- // If the clipboard text was copied linewise, and the current selection
- // is empty, then paste the text after this line and move the selection
- // to the start of the pasted text
- let insert_at = if linewise {
- let (point, _) = display_map
- .next_line_boundary(selection.start.to_point(&display_map));
-
- if !to_insert.starts_with('\n') {
- // Add newline before pasted text so that it shows up
- edits.push((point..point, "\n"));
- }
- // Drop selection at the start of the next line
- new_selections.insert(
- selection.id,
- NewPosition::Inside(Point::new(point.row + 1, 0)),
- );
- point
- } else {
- let mut point = selection.end;
- // Paste the text after the current selection
- *point.column_mut() = point.column() + 1;
- let point = display_map
- .clip_point(point, Bias::Right)
- .to_point(&display_map);
-
- new_selections.insert(
- selection.id,
- if to_insert.contains('\n') {
- NewPosition::Inside(point)
- } else {
- NewPosition::After(snapshot.anchor_after(point))
- },
- );
- point
- };
-
- if linewise && to_insert.ends_with('\n') {
- edits.push((
- insert_at..insert_at,
- &to_insert[0..to_insert.len().saturating_sub(1)],
- ))
- } else {
- edits.push((insert_at..insert_at, to_insert));
- }
- }
- drop(snapshot);
- buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
- });
-
- editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.move_with(|map, selection| {
- if let Some(new_position) = new_selections.get(&selection.id) {
- match new_position {
- NewPosition::Inside(new_point) => {
- selection.collapse_to(
- new_point.to_display_point(map),
- SelectionGoal::None,
- );
- }
- NewPosition::After(after_point) => {
- let mut new_point = after_point.to_display_point(map);
- *new_point.column_mut() =
- new_point.column().saturating_sub(1);
- new_point = map.clip_point(new_point, Bias::Left);
- selection.collapse_to(new_point, SelectionGoal::None);
- }
- }
- }
- });
- });
- } else {
- editor.insert(&clipboard_text, cx);
- }
- }
- editor.set_clip_at_line_ends(true, cx);
- });
- });
- });
-}
-
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
@@ -883,36 +742,6 @@ mod test {
.await;
}
- #[gpui::test]
- async fn test_p(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx).await;
- cx.set_shared_state(indoc! {"
- The quick brown
- fox juˇmps over
- the lazy dog"})
- .await;
-
- cx.simulate_shared_keystrokes(["d", "d"]).await;
- cx.assert_state_matches().await;
-
- cx.simulate_shared_keystroke("p").await;
- cx.assert_state_matches().await;
-
- cx.set_shared_state(indoc! {"
- The quick brown
- fox ˇjumps over
- the lazy dog"})
- .await;
- cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
- cx.set_shared_state(indoc! {"
- The quick brown
- fox jumps oveˇr
- the lazy dog"})
- .await;
- cx.simulate_shared_keystroke("p").await;
- cx.assert_state_matches().await;
- }
-
#[gpui::test]
async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -82,16 +82,19 @@ fn expand_changed_word_selection(
ignore_punctuation: bool,
) -> bool {
if times.is_none() || times.unwrap() == 1 {
+ let language = map
+ .buffer_snapshot
+ .language_at(selection.start.to_point(map));
let in_word = map
.chars_at(selection.head())
.next()
- .map(|(c, _)| char_kind(c) != CharKind::Whitespace)
+ .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace)
.unwrap_or_default();
if in_word {
selection.end = movement::find_boundary(map, selection.end, |left, right| {
- let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind && left_kind != CharKind::Whitespace
});
@@ -0,0 +1,468 @@
+use std::{borrow::Cow, cmp};
+
+use editor::{
+ display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection,
+ DisplayPoint,
+};
+use gpui::{impl_actions, AppContext, ViewContext};
+use language::{Bias, SelectionGoal};
+use serde::Deserialize;
+use workspace::Workspace;
+
+use crate::{state::Mode, utils::copy_selections_content, Vim};
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct Paste {
+ #[serde(default)]
+ before: bool,
+ #[serde(default)]
+ preserve_clipboard: bool,
+}
+
+impl_actions!(vim, [Paste]);
+
+pub(crate) fn init(cx: &mut AppContext) {
+ cx.add_action(paste);
+}
+
+fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
+ Vim::update(cx, |vim, cx| {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ editor.set_clip_at_line_ends(false, cx);
+
+ let Some(item) = cx.read_from_clipboard() else {
+ return
+ };
+ let clipboard_text = Cow::Borrowed(item.text());
+ if clipboard_text.is_empty() {
+ return;
+ }
+
+ if !action.preserve_clipboard && vim.state().mode.is_visual() {
+ copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
+ }
+
+ // if we are copying from multi-cursor (of visual block mode), we want
+ // to
+ let clipboard_selections =
+ item.metadata::<Vec<ClipboardSelection>>()
+ .filter(|clipboard_selections| {
+ clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
+ });
+
+ let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
+
+ // unlike zed, if you have a multi-cursor selection from vim block mode,
+ // pasting it will paste it on subsequent lines, even if you don't yet
+ // have a cursor there.
+ let mut selections_to_process = Vec::new();
+ let mut i = 0;
+ while i < current_selections.len() {
+ selections_to_process
+ .push((current_selections[i].start..current_selections[i].end, true));
+ i += 1;
+ }
+ if let Some(clipboard_selections) = clipboard_selections.as_ref() {
+ let left = current_selections
+ .iter()
+ .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
+ .min()
+ .unwrap();
+ let mut row = current_selections.last().unwrap().end.row() + 1;
+ while i < clipboard_selections.len() {
+ let cursor =
+ display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
+ selections_to_process.push((cursor..cursor, false));
+ i += 1;
+ row += 1;
+ }
+ }
+
+ let first_selection_indent_column =
+ clipboard_selections.as_ref().and_then(|zed_selections| {
+ zed_selections
+ .first()
+ .map(|selection| selection.first_line_indent)
+ });
+ let before = action.before || vim.state().mode == Mode::VisualLine;
+
+ let mut edits = Vec::new();
+ let mut new_selections = Vec::new();
+ let mut original_indent_columns = Vec::new();
+ let mut start_offset = 0;
+
+ for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
+ let (mut to_insert, original_indent_column) =
+ if let Some(clipboard_selections) = &clipboard_selections {
+ if let Some(clipboard_selection) = clipboard_selections.get(ix) {
+ let end_offset = start_offset + clipboard_selection.len;
+ let text = clipboard_text[start_offset..end_offset].to_string();
+ start_offset = end_offset + 1;
+ (text, Some(clipboard_selection.first_line_indent))
+ } else {
+ ("".to_string(), first_selection_indent_column)
+ }
+ } else {
+ (clipboard_text.to_string(), first_selection_indent_column)
+ };
+ let line_mode = to_insert.ends_with("\n");
+ let is_multiline = to_insert.contains("\n");
+
+ if line_mode && !before {
+ if selection.is_empty() {
+ to_insert =
+ "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
+ } else {
+ to_insert = "\n".to_owned() + &to_insert;
+ }
+ } else if !line_mode && vim.state().mode == Mode::VisualLine {
+ to_insert = to_insert + "\n";
+ }
+
+ let display_range = if !selection.is_empty() {
+ selection.start..selection.end
+ } else if line_mode {
+ let point = if before {
+ movement::line_beginning(&display_map, selection.start, false)
+ } else {
+ movement::line_end(&display_map, selection.start, false)
+ };
+ point..point
+ } else {
+ let point = if before {
+ selection.start
+ } else {
+ movement::saturating_right(&display_map, selection.start)
+ };
+ point..point
+ };
+
+ let point_range = display_range.start.to_point(&display_map)
+ ..display_range.end.to_point(&display_map);
+ let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
+ display_map.buffer_snapshot.anchor_before(point_range.start)
+ } else {
+ display_map.buffer_snapshot.anchor_after(point_range.end)
+ };
+
+ if *preserve {
+ new_selections.push((anchor, line_mode, is_multiline));
+ }
+ edits.push((point_range, to_insert));
+ original_indent_columns.extend(original_indent_column);
+ }
+
+ editor.edit_with_block_indent(edits, original_indent_columns, cx);
+
+ // in line_mode vim will insert the new text on the next (or previous if before) line
+ // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
+ // otherwise vim will insert the next text at (or before) the current cursor position,
+ // the cursor will go to the last (or first, if is_multiline) inserted character.
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.replace_cursors_with(|map| {
+ let mut cursors = Vec::new();
+ for (anchor, line_mode, is_multiline) in &new_selections {
+ let mut cursor = anchor.to_display_point(map);
+ if *line_mode {
+ if !before {
+ cursor =
+ movement::down(map, cursor, SelectionGoal::None, false).0;
+ }
+ cursor = movement::indented_line_beginning(map, cursor, true);
+ } else if !is_multiline {
+ cursor = movement::saturating_left(map, cursor)
+ }
+ cursors.push(cursor);
+ if vim.state().mode == Mode::VisualBlock {
+ break;
+ }
+ }
+
+ cursors
+ });
+ })
+ });
+ });
+ vim.switch_mode(Mode::Normal, true, cx);
+ });
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{
+ state::Mode,
+ test::{NeovimBackedTestContext, VimTestContext},
+ };
+ use indoc::indoc;
+
+ #[gpui::test]
+ async fn test_paste(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ // single line
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox ˇjumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
+ cx.assert_shared_clipboard("jumps o").await;
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox jumps oveˇr
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystroke("p").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps overjumps ˇo
+ the lazy dog"})
+ .await;
+
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox jumps oveˇr
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystroke("shift-p").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps ovejumps ˇor
+ the lazy dog"})
+ .await;
+
+ // line mode
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox juˇmps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["d", "d"]).await;
+ cx.assert_shared_clipboard("fox jumps over\n").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ the laˇzy dog"})
+ .await;
+ cx.simulate_shared_keystroke("p").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ the lazy dog
+ ˇfox jumps over"})
+ .await;
+ cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ ˇfox jumps over
+ the lazy dog
+ fox jumps over"})
+ .await;
+
+ // multiline, cursor to first character of pasted text.
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox jumps ˇover
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
+ cx.assert_shared_clipboard("over\nthe lazy do").await;
+
+ cx.simulate_shared_keystroke("p").await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps oˇover
+ the lazy dover
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps ˇover
+ the lazy doover
+ the lazy dog"})
+ .await;
+ }
+
+ #[gpui::test]
+ async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ // copy in visual mode
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox jˇumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox ˇjumps over
+ the lazy dog"})
+ .await;
+ // paste in visual mode
+ cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps jumpˇs
+ the lazy dog"})
+ .await;
+ cx.assert_shared_clipboard("over").await;
+ // paste in visual line mode
+ cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ ˇover
+ fox jumps jumps
+ the lazy dog"})
+ .await;
+ cx.assert_shared_clipboard("over").await;
+ // paste in visual block mode
+ cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ oveˇrver
+ overox jumps jumps
+ overhe lazy dog"})
+ .await;
+
+ // copy in visual line mode
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox juˇmps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ the laˇzy dog"})
+ .await;
+ // paste in visual mode
+ cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
+ cx.assert_shared_state(
+ &indoc! {"
+ The quick brown
+ the_
+ ˇfox jumps over
+ _dog"}
+ .replace("_", " "), // Hack for trailing whitespace
+ )
+ .await;
+ cx.assert_shared_clipboard("lazy").await;
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox juˇmps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ the laˇzy dog"})
+ .await;
+ // paste in visual line mode
+ cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
+ cx.assert_shared_state(indoc! {"
+ ˇfox jumps over
+ the lazy dog"})
+ .await;
+ cx.assert_shared_clipboard("The quick brown\n").await;
+ }
+
+ #[gpui::test]
+ async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ // copy in visual block mode
+ cx.set_shared_state(indoc! {"
+ The ˇquick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
+ .await;
+ cx.assert_shared_clipboard("q\nj\nl").await;
+ cx.simulate_shared_keystrokes(["p"]).await;
+ cx.assert_shared_state(indoc! {"
+ The qˇquick brown
+ fox jjumps over
+ the llazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ The ˇq brown
+ fox jjjumps over
+ the lllazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
+ .await;
+
+ cx.set_shared_state(indoc! {"
+ The ˇquick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
+ cx.assert_shared_clipboard("q\nj").await;
+ cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ The qˇqick brown
+ fox jjmps over
+ the lzy dog"})
+ .await;
+
+ cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
+ cx.assert_shared_state(indoc! {"
+ ˇq
+ j
+ fox jjmps over
+ the lzy dog"})
+ .await;
+ }
+
+ #[gpui::test]
+ async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new_typescript(cx).await;
+
+ cx.set_state(
+ indoc! {"
+ class A {ˇ
+ }
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
+ cx.assert_state(
+ indoc! {"
+ class A {
+ a()ˇ{}
+ }
+ "},
+ Mode::Normal,
+ );
+ // cursor goes to the first non-blank character in the line;
+ cx.simulate_keystrokes(["y", "y", "p"]);
+ cx.assert_state(
+ indoc! {"
+ class A {
+ a(){}
+ ˇa(){}
+ }
+ "},
+ Mode::Normal,
+ );
+ // indentation is preserved when pasting
+ cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
+ cx.assert_state(
+ indoc! {"
+ ˇclass A {
+ a(){}
+ class A {
+ a(){}
+ }
+ "},
+ Mode::Normal,
+ );
+ }
+}
@@ -177,17 +177,18 @@ fn in_word(
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
// Use motion::right so that we consider the character under the cursor when looking for the start
+ let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
let start = movement::find_preceding_boundary_in_line(
map,
right(map, relative_to, 1),
|left, right| {
- char_kind(left).coerce_punctuation(ignore_punctuation)
- != char_kind(right).coerce_punctuation(ignore_punctuation)
+ char_kind(language, left).coerce_punctuation(ignore_punctuation)
+ != char_kind(language, right).coerce_punctuation(ignore_punctuation)
},
);
let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
- char_kind(left).coerce_punctuation(ignore_punctuation)
- != char_kind(right).coerce_punctuation(ignore_punctuation)
+ char_kind(language, left).coerce_punctuation(ignore_punctuation)
+ != char_kind(language, right).coerce_punctuation(ignore_punctuation)
});
Some(start..end)
@@ -210,10 +211,11 @@ fn around_word(
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
+ let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
let in_word = map
.chars_at(relative_to)
.next()
- .map(|(c, _)| char_kind(c) != CharKind::Whitespace)
+ .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace)
.unwrap_or(false);
if in_word {
@@ -237,20 +239,21 @@ fn around_next_word(
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
+ let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
// Get the start of the word
let start = movement::find_preceding_boundary_in_line(
map,
right(map, relative_to, 1),
|left, right| {
- char_kind(left).coerce_punctuation(ignore_punctuation)
- != char_kind(right).coerce_punctuation(ignore_punctuation)
+ char_kind(language, left).coerce_punctuation(ignore_punctuation)
+ != char_kind(language, right).coerce_punctuation(ignore_punctuation)
},
);
let mut word_found = false;
let end = movement::find_boundary(map, relative_to, |left, right| {
- let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
@@ -1,7 +1,6 @@
mod neovim_backed_binding_test_context;
mod neovim_backed_test_context;
mod neovim_connection;
-mod vim_binding_test_context;
mod vim_test_context;
use std::sync::Arc;
@@ -10,7 +9,6 @@ use command_palette::CommandPalette;
use editor::DisplayPoint;
pub use neovim_backed_binding_test_context::*;
pub use neovim_backed_test_context::*;
-pub use vim_binding_test_context::*;
pub use vim_test_context::*;
use indoc::indoc;
@@ -261,3 +259,29 @@ async fn test_status_indicator(
assert!(mode_indicator.read(cx).mode.is_some());
});
}
+
+#[gpui::test]
+async fn test_word_characters(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new_typescript(cx).await;
+ cx.set_state(
+ indoc! { "
+ class A {
+ #ˇgoop = 99;
+ $ˇgoop () { return this.#gˇoop };
+ };
+ console.log(new A().$gooˇp())
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes(["v", "i", "w"]);
+ cx.assert_state(
+ indoc! {"
+ class A {
+ «#goopˇ» = 99;
+ «$goopˇ» () { return this.«#goopˇ» };
+ };
+ console.log(new A().«$goopˇ»())
+ "},
+ Mode::Visual,
+ )
+}
@@ -129,14 +129,23 @@ impl<'a> NeovimBackedTestContext<'a> {
pub async fn assert_shared_state(&mut self, marked_text: &str) {
let neovim = self.neovim_state().await;
- if neovim != marked_text {
- let initial_state = self
- .last_set_state
- .as_ref()
- .unwrap_or(&"N/A".to_string())
- .clone();
- panic!(
- indoc! {"Test is incorrect (currently expected != neovim state)
+ let editor = self.editor_state();
+ if neovim == marked_text && neovim == editor {
+ return;
+ }
+ let initial_state = self
+ .last_set_state
+ .as_ref()
+ .unwrap_or(&"N/A".to_string())
+ .clone();
+
+ let message = if neovim != marked_text {
+ "Test is incorrect (currently expected != neovim_state)"
+ } else {
+ "Editor does not match nvim behaviour"
+ };
+ panic!(
+ indoc! {"{}
# initial state:
{}
# keystrokes:
@@ -147,14 +156,59 @@ impl<'a> NeovimBackedTestContext<'a> {
{}
# zed state:
{}"},
- initial_state,
- self.recent_keystrokes.join(" "),
- marked_text,
- neovim,
- self.editor_state(),
- )
+ message,
+ initial_state,
+ self.recent_keystrokes.join(" "),
+ marked_text,
+ neovim,
+ editor
+ )
+ }
+
+ pub async fn assert_shared_clipboard(&mut self, text: &str) {
+ let neovim = self.neovim.read_register('"').await;
+ let editor = self
+ .platform()
+ .read_from_clipboard()
+ .unwrap()
+ .text()
+ .clone();
+
+ if text == neovim && text == editor {
+ return;
}
- self.assert_editor_state(marked_text)
+
+ let message = if neovim != text {
+ "Test is incorrect (currently expected != neovim)"
+ } else {
+ "Editor does not match nvim behaviour"
+ };
+
+ let initial_state = self
+ .last_set_state
+ .as_ref()
+ .unwrap_or(&"N/A".to_string())
+ .clone();
+
+ panic!(
+ indoc! {"{}
+ # initial state:
+ {}
+ # keystrokes:
+ {}
+ # currently expected:
+ {}
+ # neovim clipboard:
+ {}
+ # zed clipboard:
+ {}"},
+ message,
+ initial_state,
+ self.recent_keystrokes.join(" "),
+ text,
+ neovim,
+ editor
+ )
}
pub async fn neovim_state(&mut self) -> String {
@@ -40,6 +40,7 @@ pub enum NeovimData {
Put { state: String },
Key(String),
Get { state: String, mode: Option<Mode> },
+ ReadRegister { name: char, value: String },
}
pub struct NeovimConnection {
@@ -221,6 +222,36 @@ impl NeovimConnection {
);
}
+ #[cfg(not(feature = "neovim"))]
+ pub async fn read_register(&mut self, register: char) -> String {
+ if let Some(NeovimData::Get { .. }) = self.data.front() {
+ self.data.pop_front();
+ };
+ if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() {
+ if name == register {
+ return value;
+ }
+ }
+
+ panic!("operation does not match recorded script. re-record with --features=neovim")
+ }
+
+ #[cfg(feature = "neovim")]
+ pub async fn read_register(&mut self, name: char) -> String {
+ let value = self
+ .nvim
+ .command_output(format!("echo getreg('{}')", name).as_str())
+ .await
+ .unwrap();
+
+ self.data.push_back(NeovimData::ReadRegister {
+ name,
+ value: value.clone(),
+ });
+
+ value
+ }
+
#[cfg(feature = "neovim")]
async fn read_position(&mut self, cmd: &str) -> u32 {
self.nvim
@@ -1,64 +0,0 @@
-use std::ops::{Deref, DerefMut};
-
-use crate::*;
-
-use super::VimTestContext;
-
-pub struct VimBindingTestContext<'a, const COUNT: usize> {
- cx: VimTestContext<'a>,
- keystrokes_under_test: [&'static str; COUNT],
- mode_before: Mode,
- mode_after: Mode,
-}
-
-impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
- pub fn new(
- keystrokes_under_test: [&'static str; COUNT],
- mode_before: Mode,
- mode_after: Mode,
- cx: VimTestContext<'a>,
- ) -> Self {
- Self {
- cx,
- keystrokes_under_test,
- mode_before,
- mode_after,
- }
- }
-
- pub fn binding<const NEW_COUNT: usize>(
- self,
- keystrokes_under_test: [&'static str; NEW_COUNT],
- ) -> VimBindingTestContext<'a, NEW_COUNT> {
- VimBindingTestContext {
- keystrokes_under_test,
- cx: self.cx,
- mode_before: self.mode_before,
- mode_after: self.mode_after,
- }
- }
-
- pub fn assert(&mut self, initial_state: &str, state_after: &str) {
- self.cx.assert_binding(
- self.keystrokes_under_test,
- initial_state,
- self.mode_before,
- state_after,
- self.mode_after,
- )
- }
-}
-
-impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
- type Target = VimTestContext<'a>;
-
- fn deref(&self) -> &Self::Target {
- &self.cx
- }
-}
-
-impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.cx
- }
-}
@@ -8,16 +8,24 @@ use search::{BufferSearchBar, ProjectSearchBar};
use crate::{state::Operator, *};
-use super::VimBindingTestContext;
-
pub struct VimTestContext<'a> {
cx: EditorLspTestContext<'a>,
}
impl<'a> VimTestContext<'a> {
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
- let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
+ let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await;
+ Self::new_with_lsp(lsp, enabled)
+ }
+
+ pub async fn new_typescript(cx: &'a mut gpui::TestAppContext) -> VimTestContext<'a> {
+ Self::new_with_lsp(
+ EditorLspTestContext::new_typescript(Default::default(), cx).await,
+ true,
+ )
+ }
+ pub fn new_with_lsp(mut cx: EditorLspTestContext<'a>, enabled: bool) -> VimTestContext<'a> {
cx.update(|cx| {
search::init(cx);
crate::init(cx);
@@ -116,14 +124,6 @@ impl<'a> VimTestContext<'a> {
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
}
-
- pub fn binding<const COUNT: usize>(
- mut self,
- keystrokes: [&'static str; COUNT],
- ) -> VimBindingTestContext<'a, COUNT> {
- let mode = self.mode();
- VimBindingTestContext::new(keystrokes, mode, mode, self)
- }
}
impl<'a> Deref for VimTestContext<'a> {
@@ -1,5 +1,6 @@
use editor::{ClipboardSelection, Editor};
use gpui::{AppContext, ClipboardItem};
+use language::Point;
pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut AppContext) {
let selections = editor.selections.all_adjusted(cx);
@@ -7,13 +8,35 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App
let mut text = String::new();
let mut clipboard_selections = Vec::with_capacity(selections.len());
{
+ let mut is_first = true;
for selection in selections.iter() {
- let initial_len = text.len();
- let start = selection.start;
+ let mut start = selection.start;
let end = selection.end;
+ if is_first {
+ is_first = false;
+ } else {
+ text.push_str("\n");
+ }
+ let initial_len = text.len();
+
+ // if the file does not end with \n, and our line-mode selection ends on
+ // that line, we will have expanded the start of the selection to ensure it
+ // contains a newline (so that delete works as expected). We undo that change
+ // here.
+ let is_last_line = linewise
+ && end.row == buffer.max_buffer_row()
+ && buffer.max_point().column > 0
+ && start == Point::new(start.row, buffer.line_len(start.row));
+
+ if is_last_line {
+ start = Point::new(buffer.max_buffer_row(), 0);
+ }
for chunk in buffer.text_for_range(start..end) {
text.push_str(chunk);
}
+ if is_last_line {
+ text.push_str("\n");
+ }
clipboard_selections.push(ClipboardSelection {
len: text.len() - initial_len,
is_entire_line: linewise,
@@ -1,14 +1,14 @@
-use std::{borrow::Cow, cmp, sync::Arc};
+use std::{cmp, sync::Arc};
use collections::HashMap;
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement,
scroll::autoscroll::Autoscroll,
- Bias, ClipboardSelection, DisplayPoint, Editor,
+ Bias, DisplayPoint, Editor,
};
use gpui::{actions, AppContext, ViewContext, WindowContext};
-use language::{AutoindentMode, Selection, SelectionGoal};
+use language::{Selection, SelectionGoal};
use workspace::Workspace;
use crate::{
@@ -27,7 +27,6 @@ actions!(
ToggleVisualBlock,
VisualDelete,
VisualYank,
- VisualPaste,
OtherEnd,
]
);
@@ -47,7 +46,6 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(other_end);
cx.add_action(delete);
cx.add_action(yank);
- cx.add_action(paste);
}
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
@@ -331,110 +329,6 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
});
}
-pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>) {
- Vim::update(cx, |vim, cx| {
- vim.update_active_editor(cx, |editor, cx| {
- editor.transact(cx, |editor, cx| {
- if let Some(item) = cx.read_from_clipboard() {
- copy_selections_content(editor, editor.selections.line_mode, cx);
- let mut clipboard_text = Cow::Borrowed(item.text());
- if let Some(mut clipboard_selections) =
- item.metadata::<Vec<ClipboardSelection>>()
- {
- let (display_map, selections) = editor.selections.all_adjusted_display(cx);
- let all_selections_were_entire_line =
- clipboard_selections.iter().all(|s| s.is_entire_line);
- if clipboard_selections.len() != selections.len() {
- let mut newline_separated_text = String::new();
- let mut clipboard_selections =
- clipboard_selections.drain(..).peekable();
- let mut ix = 0;
- while let Some(clipboard_selection) = clipboard_selections.next() {
- newline_separated_text
- .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
- ix += clipboard_selection.len;
- if clipboard_selections.peek().is_some() {
- newline_separated_text.push('\n');
- }
- }
- clipboard_text = Cow::Owned(newline_separated_text);
- }
-
- let mut new_selections = Vec::new();
- editor.buffer().update(cx, |buffer, cx| {
- let snapshot = buffer.snapshot(cx);
- let mut start_offset = 0;
- let mut edits = Vec::new();
- for (ix, selection) in selections.iter().enumerate() {
- let to_insert;
- let linewise;
- if let Some(clipboard_selection) = clipboard_selections.get(ix) {
- let end_offset = start_offset + clipboard_selection.len;
- to_insert = &clipboard_text[start_offset..end_offset];
- linewise = clipboard_selection.is_entire_line;
- start_offset = end_offset;
- } else {
- to_insert = clipboard_text.as_str();
- linewise = all_selections_were_entire_line;
- }
-
- let mut selection = selection.clone();
- if !selection.reversed {
- let adjusted = selection.end;
- // If the selection is empty, move both the start and end forward one
- // character
- if selection.is_empty() {
- selection.start = adjusted;
- selection.end = adjusted;
- } else {
- selection.end = adjusted;
- }
- }
-
- let range = selection.map(|p| p.to_point(&display_map)).range();
-
- let new_position = if linewise {
- edits.push((range.start..range.start, "\n"));
- let mut new_position = range.start;
- new_position.column = 0;
- new_position.row += 1;
- new_position
- } else {
- range.start
- };
-
- new_selections.push(selection.map(|_| new_position));
-
- if linewise && to_insert.ends_with('\n') {
- edits.push((
- range.clone(),
- &to_insert[0..to_insert.len().saturating_sub(1)],
- ))
- } else {
- edits.push((range.clone(), to_insert));
- }
-
- if linewise {
- edits.push((range.end..range.end, "\n"));
- }
- }
- drop(snapshot);
- buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
- });
-
- editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.select(new_selections)
- });
- } else {
- editor.insert(&clipboard_text, cx);
- }
- }
- });
- });
- vim.switch_mode(Mode::Normal, true, cx);
- });
-}
-
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
@@ -497,7 +391,7 @@ mod test {
the lazy dog"
})
.await;
- let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
+ let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
// entering visual mode should select the character
// under cursor
@@ -506,7 +400,7 @@ mod test {
fox jumps over
the lazy dog"})
.await;
- cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
+ cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
// forwards motions should extend the selection
cx.simulate_shared_keystrokes(["w", "j"]).await;
@@ -536,7 +430,7 @@ mod test {
b
"})
.await;
- let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
+ let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
cx.simulate_shared_keystrokes(["v"]).await;
cx.assert_shared_state(indoc! {"
a
@@ -544,7 +438,7 @@ mod test {
ˇ»b
"})
.await;
- cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
+ cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
// toggles off again
cx.simulate_shared_keystrokes(["v"]).await;
@@ -616,7 +510,7 @@ mod test {
b
ˇ"})
.await;
- let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
+ let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
cx.simulate_shared_keystrokes(["shift-v"]).await;
cx.assert_shared_state(indoc! {"
a
@@ -624,7 +518,7 @@ mod test {
ˇ"})
.await;
assert_eq!(cx.mode(), cx.neovim_mode().await);
- cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
+ cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
cx.simulate_shared_keystrokes(["x"]).await;
cx.assert_shared_state(indoc! {"
a
@@ -669,38 +563,41 @@ mod test {
#[gpui::test]
async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx)
- .await
- .binding(["shift-v", "x"]);
- cx.assert(indoc! {"
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {"
The quˇick brown
fox jumps over
the lazy dog"})
.await;
- // Test pasting code copied on delete
- cx.simulate_shared_keystroke("p").await;
+ cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
cx.assert_state_matches().await;
- cx.assert_all(indoc! {"
- The quick brown
- fox juˇmps over
- the laˇzy dog"})
- .await;
- let mut cx = cx.binding(["shift-v", "j", "x"]);
- cx.assert(indoc! {"
- The quˇick brown
- fox jumps over
- the lazy dog"})
- .await;
// Test pasting code copied on delete
cx.simulate_shared_keystroke("p").await;
cx.assert_state_matches().await;
- cx.assert_all(indoc! {"
+ cx.set_shared_state(indoc! {"
The quick brown
- fox juˇmps over
+ fox jumps over
the laˇzy dog"})
.await;
+ cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
+ cx.assert_state_matches().await;
+ cx.assert_shared_clipboard("the lazy dog\n").await;
+
+ for marked_text in cx.each_marked_position(indoc! {"
+ The quˇick brown
+ fox jumps over
+ the lazy dog"})
+ {
+ cx.set_shared_state(&marked_text).await;
+ cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
+ cx.assert_state_matches().await;
+ // Test pasting code copied on delete
+ cx.simulate_shared_keystroke("p").await;
+ cx.assert_state_matches().await;
+ }
cx.set_shared_state(indoc! {"
The ˇlong line
@@ -714,145 +611,57 @@ mod test {
#[gpui::test]
async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
- let cx = VimTestContext::new(cx, true).await;
- let mut cx = cx.binding(["v", "w", "y"]);
- cx.assert("The quick ˇbrown", "The quick ˇbrown");
- cx.assert_clipboard_content(Some("brown"));
- let mut cx = cx.binding(["v", "w", "j", "y"]);
- cx.assert(
- indoc! {"
- The ˇquick brown
- fox jumps over
- the lazy dog"},
- indoc! {"
- The ˇquick brown
- fox jumps over
- the lazy dog"},
- );
- cx.assert_clipboard_content(Some(indoc! {"
- quick brown
- fox jumps o"}));
- cx.assert(
- indoc! {"
- The quick brown
- fox jumps over
- the ˇlazy dog"},
- indoc! {"
- The quick brown
- fox jumps over
- the ˇlazy dog"},
- );
- cx.assert_clipboard_content(Some("lazy d"));
- cx.assert(
- indoc! {"
- The quick brown
- fox jumps ˇover
- the lazy dog"},
- indoc! {"
- The quick brown
- fox jumps ˇover
- the lazy dog"},
- );
- cx.assert_clipboard_content(Some(indoc! {"
- over
- t"}));
- let mut cx = cx.binding(["v", "b", "k", "y"]);
- cx.assert(
- indoc! {"
- The ˇquick brown
- fox jumps over
- the lazy dog"},
- indoc! {"
- ˇThe quick brown
- fox jumps over
- the lazy dog"},
- );
- cx.assert_clipboard_content(Some("The q"));
- cx.assert(
- indoc! {"
- The quick brown
- fox jumps over
- the ˇlazy dog"},
- indoc! {"
- The quick brown
- ˇfox jumps over
- the lazy dog"},
- );
- cx.assert_clipboard_content(Some(indoc! {"
- fox jumps over
- the l"}));
- cx.assert(
- indoc! {"
- The quick brown
- fox jumps ˇover
- the lazy dog"},
- indoc! {"
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state("The quick ˇbrown").await;
+ cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
+ cx.assert_shared_state("The quick ˇbrown").await;
+ cx.assert_shared_clipboard("brown").await;
+
+ cx.set_shared_state(indoc! {"
The ˇquick brown
fox jumps over
- the lazy dog"},
- );
- cx.assert_clipboard_content(Some(indoc! {"
- quick brown
- fox jumps o"}));
- }
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
+ cx.assert_shared_state(indoc! {"
+ The ˇquick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+ cx.assert_shared_clipboard(indoc! {"
+ quick brown
+ fox jumps o"})
+ .await;
- #[gpui::test]
- async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, true).await;
- cx.set_state(
- indoc! {"
- The quick brown
- fox «jumpsˇ» over
- the lazy dog"},
- Mode::Visual,
- );
- cx.simulate_keystroke("y");
- cx.set_state(
- indoc! {"
- The quick brown
- fox jumpˇs over
- the lazy dog"},
- Mode::Normal,
- );
- cx.simulate_keystroke("p");
- cx.assert_state(
- indoc! {"
- The quick brown
- fox jumpsjumpˇs over
- the lazy dog"},
- Mode::Normal,
- );
+ cx.set_shared_state(indoc! {"
+ The quick brown
+ fox jumps over
+ the ˇlazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
+ cx.assert_shared_state(indoc! {"
+ The quick brown
+ fox jumps over
+ the ˇlazy dog"})
+ .await;
+ cx.assert_shared_clipboard("lazy d").await;
+ cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
+ cx.assert_shared_clipboard("the lazy dog\n").await;
- cx.set_state(
- indoc! {"
- The quick brown
- fox ju«mˇ»ps over
- the lazy dog"},
- Mode::VisualLine,
- );
- cx.simulate_keystroke("d");
- cx.assert_state(
- indoc! {"
- The quick brown
- the laˇzy dog"},
- Mode::Normal,
- );
- cx.set_state(
- indoc! {"
- The quick brown
- the «lazyˇ» dog"},
- Mode::Visual,
- );
- cx.simulate_keystroke("p");
- cx.assert_state(
- &indoc! {"
- The quick brown
- the_
- ˇfox jumps over
- dog"}
- .replace("_", " "), // Hack for trailing whitespace
- Mode::Normal,
- );
+ let mut cx = cx.binding(["v", "b", "k", "y"]);
+ cx.set_shared_state(indoc! {"
+ The ˇquick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+ cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
+ cx.assert_shared_state(indoc! {"
+ ˇThe quick brown
+ fox jumps over
+ the lazy dog"})
+ .await;
+ cx.assert_clipboard_content(Some("The q"));
}
#[gpui::test]
@@ -1,13 +0,0 @@
-{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
-{"Key":"d"}
-{"Key":"d"}
-{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
-{"Key":"p"}
-{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
-{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
-{"Key":"v"}
-{"Key":"w"}
-{"Key":"y"}
-{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
-{"Key":"p"}
-{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}
@@ -0,0 +1,31 @@
+{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"jumps o"}}
+{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
+{"Key":"shift-p"}
+{"Get":{"state":"The quick brown\nfox jumps ovejumps ˇor\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"d"}
+{"Key":"d"}
+{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"shift-p"}
+{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog\nfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"j"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"over\nthe lazy do"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps oˇover\nthe lazy dover\nthe lazy dog","mode":"Normal"}}
+{"Key":"u"}
+{"Key":"shift-p"}
+{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy doover\nthe lazy dog","mode":"Normal"}}
@@ -0,0 +1,42 @@
+{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"y"}
+{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"w"}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps jumpˇs\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"over"}}
+{"Key":"up"}
+{"Key":"shift-v"}
+{"Key":"shift-p"}
+{"Get":{"state":"ˇover\nfox jumps jumps\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"over"}}
+{"Key":"ctrl-v"}
+{"Key":"down"}
+{"Key":"down"}
+{"Key":"p"}
+{"Get":{"state":"oveˇrver\noverox jumps jumps\noverhe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"shift-v"}
+{"Key":"p"}
+{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"The quick brown\n"}}
@@ -0,0 +1,31 @@
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"ctrl-v"}
+{"Key":"2"}
+{"Key":"j"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"q\nj\nl"}}
+{"Key":"p"}
+{"Get":{"state":"The qˇquick brown\nfox jjumps over\nthe llazy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"shift-p"}
+{"Get":{"state":"The ˇq brown\nfox jjjumps over\nthe lllazy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"shift-p"}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"q\nj"}}
+{"Key":"l"}
+{"Key":"ctrl-v"}
+{"Key":"2"}
+{"Key":"j"}
+{"Key":"shift-p"}
+{"Get":{"state":"The qˇqick brown\nfox jjmps over\nthe lzy dog","mode":"Normal"}}
+{"Key":"shift-v"}
+{"Key":"p"}
+{"Get":{"state":"ˇq\nj\nfox jjmps over\nthe lzy dog","mode":"Normal"}}
@@ -4,14 +4,11 @@
{"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}}
{"Key":"p"}
{"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}}
-{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
-{"Key":"shift-v"}
-{"Key":"x"}
-{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
{"Key":"shift-v"}
{"Key":"x"}
{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}}
{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"j"}
@@ -19,16 +16,6 @@
{"Get":{"state":"the laˇzy dog","mode":"Normal"}}
{"Key":"p"}
{"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}}
-{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
-{"Key":"shift-v"}
-{"Key":"j"}
-{"Key":"x"}
-{"Get":{"state":"The quˇick brown","mode":"Normal"}}
-{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
-{"Key":"shift-v"}
-{"Key":"j"}
-{"Key":"x"}
-{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
{"Put":{"state":"The ˇlong line\nshould not\ncrash\n"}}
{"Key":"shift-v"}
{"Key":"$"}
@@ -0,0 +1,26 @@
+{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"y"}
+{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jjumpˇsumps over\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"shift-v"}
+{"Key":"p"}
+{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}}
@@ -0,0 +1,29 @@
+{"Put":{"state":"The quick ˇbrown"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"y"}
+{"Get":{"state":"The quick ˇbrown","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"brown"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"y"}
+{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"quick brown\nfox jumps o"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"y"}
+{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy d"}}
+{"Key":"shift-v"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"y"}
+{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
@@ -232,7 +232,7 @@ impl Item for WelcomePage {
Some("Welcome to Zed!".into())
}
- fn tab_content<T: View>(
+ fn tab_content<T: 'static>(
&self,
_detail: Option<usize>,
style: &theme::Tab,
@@ -22,6 +22,7 @@ test-support = [
db = { path = "../db" }
call = { path = "../call" }
client = { path = "../client" }
+channel = { path = "../channel" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
drag_and_drop = { path = "../drag_and_drop" }
@@ -101,7 +101,7 @@ pub trait Item: View {
fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<str>> {
None
}
- fn tab_content<V: View>(
+ fn tab_content<V: 'static>(
&self,
detail: Option<usize>,
style: &theme::Tab,
@@ -158,9 +158,7 @@ pub trait Item: View {
fn should_update_tab_on_event(_: &Self::Event) -> bool {
false
}
- fn is_edit_event(_: &Self::Event) -> bool {
- false
- }
+
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
@@ -205,7 +203,7 @@ pub trait Item: View {
fn show_toolbar(&self) -> bool {
true
}
- fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
+ fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
None
}
}
@@ -623,7 +621,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
}
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
- self.read(cx).pixel_position_of_cursor()
+ self.read(cx).pixel_position_of_cursor(cx)
}
}
@@ -674,7 +672,7 @@ pub trait FollowableItem: Item {
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
fn from_state_proto(
pane: ViewHandle<Pane>,
- project: ModelHandle<Project>,
+ project: ViewHandle<Workspace>,
id: ViewId,
state: &mut Option<proto::view::Variant>,
cx: &mut AppContext,
@@ -943,7 +941,7 @@ pub mod test {
})
}
- fn tab_content<V: View>(
+ fn tab_content<V: 'static>(
&self,
detail: Option<usize>,
_: &theme::Tab,
@@ -1976,12 +1976,12 @@ impl NavHistoryState {
}
}
-pub struct PaneBackdrop<V: View> {
+pub struct PaneBackdrop<V> {
child_view: usize,
child: AnyElement<V>,
}
-impl<V: View> PaneBackdrop<V> {
+impl<V> PaneBackdrop<V> {
pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
PaneBackdrop {
child,
@@ -1990,7 +1990,7 @@ impl<V: View> PaneBackdrop<V> {
}
}
-impl<V: View> Element<V> for PaneBackdrop<V> {
+impl<V: 'static> Element<V> for PaneBackdrop<V> {
type LayoutState = ();
type PaintState = ();
@@ -7,7 +7,7 @@ use gpui::{
geometry::{rect::RectF, vector::Vector2F},
platform::MouseButton,
scene::MouseUp,
- AppContext, Element, EventContext, MouseState, Quad, View, ViewContext, WeakViewHandle,
+ AppContext, Element, EventContext, MouseState, Quad, ViewContext, WeakViewHandle,
};
use project::ProjectEntryId;
@@ -42,7 +42,11 @@ where
let mut handler = MouseEventHandler::above::<Tag, _>(region_id, cx, |state, cx| {
// Observing hovered will cause a render when the mouse enters regardless
// of if mouse position was accessed before
- let drag_position = if state.hovered() { drag_position } else { None };
+ let drag_position = if state.dragging() {
+ drag_position
+ } else {
+ None
+ };
Stack::new()
.with_child(render_child(state, cx))
.with_children(drag_position.map(|drag_position| {
@@ -107,7 +111,7 @@ where
handler
}
-pub fn handle_dropped_item<V: View>(
+pub fn handle_dropped_item<V: 'static>(
event: MouseUp,
workspace: WeakViewHandle<Workspace>,
pane: &WeakViewHandle<Pane>,
@@ -104,7 +104,7 @@ impl Item for SharedScreen {
}
}
- fn tab_content<V: View>(
+ fn tab_content<V: 'static>(
&self,
_: Option<usize>,
style: &theme::Tab,
@@ -12,9 +12,10 @@ mod workspace_settings;
use anyhow::{anyhow, Context, Result};
use call::ActiveCall;
+use channel::ChannelStore;
use client::{
proto::{self, PeerId},
- ChannelStore, Client, TypedEnvelope, UserStore,
+ Client, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use drag_and_drop::DragAndDrop;
@@ -344,7 +345,7 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
type FollowableItemBuilder = fn(
ViewHandle<Pane>,
- ModelHandle<Project>,
+ ViewHandle<Workspace>,
ViewId,
&mut Option<proto::view::Variant>,
&mut AppContext,
@@ -361,8 +362,8 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
builders.insert(
TypeId::of::<I>(),
(
- |pane, project, id, state, cx| {
- I::from_state_proto(pane, project, id, state, cx).map(|task| {
+ |pane, workspace, id, state, cx| {
+ I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
cx.foreground()
.spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
})
@@ -2847,7 +2848,13 @@ impl Workspace {
views: Vec<proto::View>,
cx: &mut AsyncAppContext,
) -> Result<()> {
- let project = this.read_with(cx, |this, _| this.project.clone())?;
+ let this = this
+ .upgrade(cx)
+ .ok_or_else(|| anyhow!("workspace dropped"))?;
+ let project = this
+ .read_with(cx, |this, _| this.project.clone())
+ .ok_or_else(|| anyhow!("window dropped"))?;
+
let replica_id = project
.read_with(cx, |project, _| {
project
@@ -2873,12 +2880,11 @@ impl Workspace {
let id = ViewId::from_proto(id.clone())?;
let mut variant = view.variant.clone();
if variant.is_none() {
- Err(anyhow!("missing variant"))?;
+ Err(anyhow!("missing view variant"))?;
}
for build_item in &item_builders {
- let task = cx.update(|cx| {
- build_item(pane.clone(), project.clone(), id, &mut variant, cx)
- });
+ let task = cx
+ .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx));
if let Some(task) = task {
item_tasks.push(task);
leader_view_ids.push(id);
@@ -2906,7 +2912,7 @@ impl Workspace {
}
Some(())
- })?;
+ });
}
Ok(())
}
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
-version = "0.101.0"
+version = "0.102.0"
publish = false
[lib]
@@ -21,10 +21,12 @@ activity_indicator = { path = "../activity_indicator" }
auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" }
call = { path = "../call" }
+channel = { path = "../channel" }
cli = { path = "../cli" }
collab_ui = { path = "../collab_ui" }
collections = { path = "../collections" }
command_palette = { path = "../command_palette" }
+component_test = { path = "../component_test" }
context_menu = { path = "../context_menu" }
client = { path = "../client" }
clock = { path = "../clock" }
@@ -93,7 +95,7 @@ postage.workspace = true
rand.workspace = true
regex.workspace = true
rsa = "0.4"
-rust-embed = { version = "6.3", features = ["include-exclude"] }
+rust-embed.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
@@ -13,6 +13,7 @@ brackets = [
{ start = "`", end = "`", close = true, newline = false, not_in = ["comment", "string"] },
{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
]
+word_characters = ["$", "#"]
[overrides.element]
line_comment = { remove = true }
@@ -10,3 +10,4 @@ brackets = [
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
]
collapsed_placeholder = "/* ... */"
+word_characters = ["$"]
@@ -12,6 +12,7 @@ brackets = [
{ start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
]
+word_characters = ["#", "$"]
[overrides.element]
line_comment = { remove = true }
@@ -12,3 +12,4 @@ brackets = [
{ start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
]
+word_characters = ["#", "$"]
@@ -3,13 +3,12 @@
use anyhow::{anyhow, Context, Result};
use backtrace::Backtrace;
+use channel::ChannelStore;
use cli::{
ipc::{self, IpcSender},
CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
};
-use client::{
- self, ChannelStore, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
-};
+use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use futures::{
@@ -159,6 +158,7 @@ fn main() {
outline::init(cx);
project_symbols::init(cx);
project_panel::init(Assets, cx);
+ channel::init(&client);
diagnostics::init(cx);
search::init(cx);
semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
@@ -166,6 +166,7 @@ fn main() {
terminal_view::init(cx);
copilot::init(http.clone(), node_runtime, cx);
ai::init(cx);
+ component_test::init(cx);
cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
@@ -44,10 +44,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO
}
const padding = {
- top: size === Button.size.Small ? 0 : 2,
- bottom: size === Button.size.Small ? 0 : 2,
- left: size === Button.size.Small ? 0 : 4,
- right: size === Button.size.Small ? 0 : 4,
+ top: size === Button.size.Small ? 2 : 2,
+ bottom: size === Button.size.Small ? 2 : 2,
+ left: size === Button.size.Small ? 2 : 4,
+ right: size === Button.size.Small ? 2 : 4,
}
return interactive({
@@ -55,10 +55,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO
corner_radius: 6,
padding: padding,
margin: m,
- icon_width: 14,
+ icon_width: 12,
icon_height: 14,
- button_width: 20,
- button_height: 16,
+ button_width: size === Button.size.Small ? 16 : 20,
+ button_height: 14,
},
state: {
default: {
@@ -12,7 +12,6 @@ import simple_message_notification from "./simple_message_notification"
import project_shared_notification from "./project_shared_notification"
import tooltip from "./tooltip"
import terminal from "./terminal"
-import contact_finder from "./contact_finder"
import collab_panel from "./collab_panel"
import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
import incoming_call_notification from "./incoming_call_notification"
@@ -22,6 +21,7 @@ import assistant from "./assistant"
import { titlebar } from "./titlebar"
import editor from "./editor"
import feedback from "./feedback"
+import component_test from "./component_test"
import { useTheme } from "../common"
export default function app(): any {
@@ -54,6 +54,7 @@ export default function app(): any {
tooltip: tooltip(),
terminal: terminal(),
assistant: assistant(),
- feedback: feedback()
+ feedback: feedback(),
+ component_test: component_test(),
}
}
@@ -14,6 +14,7 @@ import { indicator } from "../component/indicator"
export default function contacts_panel(): any {
const theme = useTheme()
+ const CHANNEL_SPACING = 4 as const
const NAME_MARGIN = 6 as const
const SPACING = 12 as const
const INDENT_SIZE = 8 as const
@@ -152,6 +153,10 @@ export default function contacts_panel(): any {
return {
...collab_modals(),
+ disclosure: {
+ button: icon_button({ variant: "ghost", size: "sm" }),
+ spacing: CHANNEL_SPACING,
+ },
log_in_button: interactive({
base: {
background: background(theme.middle),
@@ -194,7 +199,7 @@ export default function contacts_panel(): any {
add_channel_button: header_icon_button,
leave_call_button: header_icon_button,
row_height: ITEM_HEIGHT,
- channel_indent: INDENT_SIZE * 2,
+ channel_indent: INDENT_SIZE * 2 + 2,
section_icon_size: 14,
header_row: {
...text(layer, "sans", { size: "sm", weight: "bold" }),
@@ -264,7 +269,7 @@ export default function contacts_panel(): any {
channel_name: {
...text(layer, "sans", { size: "sm" }),
margin: {
- left: NAME_MARGIN,
+ left: CHANNEL_SPACING,
},
},
list_empty_label_container: {
@@ -0,0 +1,27 @@
+
+import { useTheme } from "../common"
+import { text_button } from "../component/text_button"
+import { icon_button } from "../component/icon_button"
+import { text } from "./components"
+import { toggleable } from "../element"
+
+export default function contacts_panel(): any {
+ const theme = useTheme()
+
+ return {
+ button: text_button({}),
+ toggle: toggleable({
+ base: text_button({}),
+ state: {
+ active: {
+ ...text_button({ color: "accent" })
+ }
+ }
+ }),
+ disclosure: {
+ ...text(theme.lowest, "sans", "base"),
+ button: icon_button({ variant: "ghost" }),
+ spacing: 4,
+ }
+ }
+}
@@ -184,6 +184,7 @@ export default function editor(): any {
theme.players[6],
theme.players[7],
],
+ absent_selection: theme.players[7],
autocomplete: {
background: background(theme.middle),
corner_radius: 8,
@@ -0,0 +1,5670 @@
+#![feature(prelude_import)]
+#![allow(dead_code, unused_variables)]
+#[prelude_import]
+use std::prelude::rust_2021::*;
+#[macro_use]
+extern crate std;
+use color::black;
+use components::button;
+use element::Element;
+use frame::frame;
+use gpui::{
+ geometry::{rect::RectF, vector::vec2f},
+ platform::WindowOptions,
+};
+use log::LevelFilter;
+use simplelog::SimpleLogger;
+use themes::{rose_pine, ThemeColors};
+use view::view;
+mod adapter {
+ use crate::element::{LayoutContext, PaintContext};
+ use gpui::{geometry::rect::RectF, LayoutEngine};
+ use util::ResultExt;
+ use crate::element::AnyElement;
+ pub struct Adapter<V>(pub(crate) AnyElement<V>);
+ impl<V: 'static> gpui::Element<V> for Adapter<V> {
+ type LayoutState = Option<LayoutEngine>;
+ type PaintState = ();
+ fn layout(
+ &mut self,
+ constraint: gpui::SizeConstraint,
+ view: &mut V,
+ cx: &mut LayoutContext<V>,
+ ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+ cx.push_layout_engine(LayoutEngine::new());
+ let node = self.0.layout(view, cx).log_err();
+ if let Some(node) = node {
+ let layout_engine = cx.layout_engine().unwrap();
+ layout_engine.compute_layout(node, constraint.max).log_err();
+ }
+ let layout_engine = cx.pop_layout_engine();
+ if true {
+ if !layout_engine.is_some() {
+ ::core::panicking::panic("assertion failed: layout_engine.is_some()")
+ }
+ }
+ (constraint.max, layout_engine)
+ }
+ fn paint(
+ &mut self,
+ scene: &mut gpui::SceneBuilder,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout_engine: &mut Option<LayoutEngine>,
+ view: &mut V,
+ legacy_cx: &mut gpui::PaintContext<V>,
+ ) -> Self::PaintState {
+ legacy_cx.push_layout_engine(layout_engine.take().unwrap());
+ let mut cx = PaintContext::new(legacy_cx, scene);
+ self.0.paint(view, &mut cx).log_err();
+ *layout_engine = legacy_cx.pop_layout_engine();
+ if true {
+ if !layout_engine.is_some() {
+ ::core::panicking::panic("assertion failed: layout_engine.is_some()")
+ }
+ }
+ }
+ fn rect_for_text_range(
+ &self,
+ range_utf16: std::ops::Range<usize>,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout: &Self::LayoutState,
+ paint: &Self::PaintState,
+ view: &V,
+ cx: &gpui::ViewContext<V>,
+ ) -> Option<RectF> {
+ ::core::panicking::panic("not yet implemented")
+ }
+ fn debug(
+ &self,
+ bounds: RectF,
+ layout: &Self::LayoutState,
+ paint: &Self::PaintState,
+ view: &V,
+ cx: &gpui::ViewContext<V>,
+ ) -> gpui::serde_json::Value {
+ ::core::panicking::panic("not yet implemented")
+ }
+ }
+}
+mod color {
+ #![allow(dead_code)]
+ use std::{num::ParseIntError, ops::Range};
+ use smallvec::SmallVec;
+ pub fn rgb<C: From<Rgba>>(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()
+ }
+ pub struct Rgba {
+ pub r: f32,
+ pub g: f32,
+ pub b: f32,
+ pub a: f32,
+ }
+ #[automatically_derived]
+ impl ::core::clone::Clone for Rgba {
+ #[inline]
+ fn clone(&self) -> Rgba {
+ let _: ::core::clone::AssertParamIsClone<f32>;
+ *self
+ }
+ }
+ #[automatically_derived]
+ impl ::core::marker::Copy for Rgba {}
+ #[automatically_derived]
+ impl ::core::default::Default for Rgba {
+ #[inline]
+ fn default() -> Rgba {
+ Rgba {
+ r: ::core::default::Default::default(),
+ g: ::core::default::Default::default(),
+ b: ::core::default::Default::default(),
+ a: ::core::default::Default::default(),
+ }
+ }
+ }
+ #[automatically_derived]
+ impl ::core::fmt::Debug for Rgba {
+ fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
+ ::core::fmt::Formatter::debug_struct_field4_finish(
+ f,
+ "Rgba",
+ "r",
+ &self.r,
+ "g",
+ &self.g,
+ "b",
+ &self.b,
+ "a",
+ &&self.a,
+ )
+ }
+ }
+ pub trait Lerp {
+ fn lerp(&self, level: f32) -> Hsla;
+ }
+ impl Lerp for Range<Hsla> {
+ fn lerp(&self, level: f32) -> Hsla {
+ let level = level.clamp(0., 1.);
+ Hsla {
+ h: self.start.h + (level * (self.end.h - self.start.h)),
+ s: self.start.s + (level * (self.end.s - self.start.s)),
+ l: self.start.l + (level * (self.end.l - self.start.l)),
+ a: self.start.a + (level * (self.end.a - self.start.a)),
+ }
+ }
+ }
+ impl From<gpui::color::Color> for Rgba {
+ fn from(value: gpui::color::Color) -> Self {
+ Self {
+ r: value.0.r as f32 / 255.0,
+ g: value.0.g as f32 / 255.0,
+ b: value.0.b as f32 / 255.0,
+ a: value.0.a as f32 / 255.0,
+ }
+ }
+ }
+ impl From<Hsla> 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<Self, Self::Error> {
+ 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 })
+ }
+ }
+ impl Into<gpui::color::Color> for Rgba {
+ fn into(self) -> gpui::color::Color {
+ gpui::color::rgba(self.r, self.g, self.b, self.a)
+ }
+ }
+ pub struct Hsla {
+ pub h: f32,
+ pub s: f32,
+ pub l: f32,
+ pub a: f32,
+ }
+ #[automatically_derived]
+ impl ::core::default::Default for Hsla {
+ #[inline]
+ fn default() -> Hsla {
+ Hsla {
+ h: ::core::default::Default::default(),
+ s: ::core::default::Default::default(),
+ l: ::core::default::Default::default(),
+ a: ::core::default::Default::default(),
+ }
+ }
+ }
+ #[automatically_derived]
+ impl ::core::marker::Copy for Hsla {}
+ #[automatically_derived]
+ impl ::core::clone::Clone for Hsla {
+ #[inline]
+ fn clone(&self) -> Hsla {
+ let _: ::core::clone::AssertParamIsClone<f32>;
+ *self
+ }
+ }
+ #[automatically_derived]
+ impl ::core::fmt::Debug for Hsla {
+ fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
+ ::core::fmt::Formatter::debug_struct_field4_finish(
+ f,
+ "Hsla",
+ "h",
+ &self.h,
+ "s",
+ &self.s,
+ "l",
+ &self.l,
+ "a",
+ &&self.a,
+ )
+ }
+ }
+ #[automatically_derived]
+ impl ::core::marker::StructuralPartialEq for Hsla {}
+ #[automatically_derived]
+ impl ::core::cmp::PartialEq for Hsla {
+ #[inline]
+ fn eq(&self, other: &Hsla) -> bool {
+ self.h == other.h && self.s == other.s && self.l == other.l
+ && self.a == other.a
+ }
+ }
+ 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 From<Rgba> 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 Hsla {
+ /// Scales the saturation and lightness by the given values, clamping at 1.0.
+ pub fn scale_sl(mut self, s: f32, l: f32) -> Self {
+ self.s = (self.s * s).clamp(0., 1.);
+ self.l = (self.l * l).clamp(0., 1.);
+ self
+ }
+ /// Increases the saturation of the color by a certain amount, with a max
+ /// value of 1.0.
+ pub fn saturate(mut self, amount: f32) -> Self {
+ self.s += amount;
+ self.s = self.s.clamp(0.0, 1.0);
+ self
+ }
+ /// Decreases the saturation of the color by a certain amount, with a min
+ /// value of 0.0.
+ pub fn desaturate(mut self, amount: f32) -> Self {
+ self.s -= amount;
+ self.s = self.s.max(0.0);
+ if self.s < 0.0 {
+ self.s = 0.0;
+ }
+ self
+ }
+ /// Brightens the color by increasing the lightness by a certain amount,
+ /// with a max value of 1.0.
+ pub fn brighten(mut self, amount: f32) -> Self {
+ self.l += amount;
+ self.l = self.l.clamp(0.0, 1.0);
+ self
+ }
+ /// Darkens the color by decreasing the lightness by a certain amount,
+ /// with a max value of 0.0.
+ pub fn darken(mut self, amount: f32) -> Self {
+ self.l -= amount;
+ self.l = self.l.clamp(0.0, 1.0);
+ self
+ }
+ }
+ impl From<gpui::color::Color> for Hsla {
+ fn from(value: gpui::color::Color) -> Self {
+ Rgba::from(value).into()
+ }
+ }
+ impl Into<gpui::color::Color> for Hsla {
+ fn into(self) -> gpui::color::Color {
+ Rgba::from(self).into()
+ }
+ }
+ pub struct ColorScale {
+ colors: SmallVec<[Hsla; 2]>,
+ positions: SmallVec<[f32; 2]>,
+ }
+ pub fn scale<I, C>(colors: I) -> ColorScale
+ where
+ I: IntoIterator<Item = C>,
+ C: Into<Hsla>,
+ {
+ let mut scale = ColorScale {
+ colors: colors.into_iter().map(Into::into).collect(),
+ positions: SmallVec::new(),
+ };
+ let num_colors: f32 = scale.colors.len() as f32 - 1.0;
+ scale
+ .positions = (0..scale.colors.len())
+ .map(|i| i as f32 / num_colors)
+ .collect();
+ scale
+ }
+ impl ColorScale {
+ fn at(&self, t: f32) -> Hsla {
+ if true {
+ if !(0.0 <= t && t <= 1.0) {
+ {
+ ::core::panicking::panic_fmt(
+ format_args!(
+ "t value {0} is out of range. Expected value in range 0.0 to 1.0",
+ t,
+ ),
+ );
+ }
+ }
+ }
+ let position = match self
+ .positions
+ .binary_search_by(|a| a.partial_cmp(&t).unwrap())
+ {
+ Ok(index) | Err(index) => index,
+ };
+ let lower_bound = position.saturating_sub(1);
+ let upper_bound = position.min(self.colors.len() - 1);
+ let lower_color = &self.colors[lower_bound];
+ let upper_color = &self.colors[upper_bound];
+ match upper_bound.checked_sub(lower_bound) {
+ Some(0) | None => *lower_color,
+ Some(_) => {
+ let interval_t = (t - self.positions[lower_bound])
+ / (self.positions[upper_bound] - self.positions[lower_bound]);
+ let h = lower_color.h + interval_t * (upper_color.h - lower_color.h);
+ let s = lower_color.s + interval_t * (upper_color.s - lower_color.s);
+ let l = lower_color.l + interval_t * (upper_color.l - lower_color.l);
+ let a = lower_color.a + interval_t * (upper_color.a - lower_color.a);
+ Hsla { h, s, l, a }
+ }
+ }
+ }
+ }
+}
+mod components {
+ use crate::{
+ element::{Element, ElementMetadata},
+ frame, text::ArcCow, themes::rose_pine,
+ };
+ use gpui::{platform::MouseButton, ViewContext};
+ use playground_macros::Element;
+ use std::{marker::PhantomData, rc::Rc};
+ struct ButtonHandlers<V, D> {
+ click: Option<Rc<dyn Fn(&mut V, &D, &mut ViewContext<V>)>>,
+ }
+ impl<V, D> Default for ButtonHandlers<V, D> {
+ fn default() -> Self {
+ Self { click: None }
+ }
+ }
+ #[element_crate = "crate"]
+ pub struct Button<V: 'static, D: 'static> {
+ metadata: ElementMetadata<V>,
+ handlers: ButtonHandlers<V, D>,
+ label: Option<ArcCow<'static, str>>,
+ icon: Option<ArcCow<'static, str>>,
+ data: Rc<D>,
+ view_type: PhantomData<V>,
+ }
+ impl<V: 'static, D: 'static> crate::element::Element<V> for Button<V, D> {
+ type Layout = crate::element::AnyElement<V>;
+ fn declared_style(&mut self) -> &mut crate::style::OptionalStyle {
+ &mut self.metadata.style
+ }
+ fn handlers_mut(&mut self) -> &mut Vec<crate::element::EventHandler<V>> {
+ &mut self.metadata.handlers
+ }
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut crate::element::LayoutContext<V>,
+ ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
+ let mut element = self.render(view, cx).into_any();
+ let node_id = element.layout(view, cx)?;
+ Ok((node_id, element))
+ }
+ fn paint<'a>(
+ &mut self,
+ layout: crate::element::Layout<'a, Self::Layout>,
+ view: &mut V,
+ cx: &mut crate::element::PaintContext<V>,
+ ) -> anyhow::Result<()> {
+ layout.from_element.paint(view, cx)?;
+ Ok(())
+ }
+ }
+ impl<V: 'static, D: 'static> crate::element::IntoElement<V> for Button<V, D> {
+ type Element = Self;
+ fn into_element(self) -> Self {
+ self
+ }
+ }
+ impl<V: 'static> Button<V, ()> {
+ fn new() -> Self {
+ Self {
+ metadata: Default::default(),
+ handlers: ButtonHandlers::default(),
+ label: None,
+ icon: None,
+ data: Rc::new(()),
+ view_type: PhantomData,
+ }
+ }
+ pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
+ Button {
+ metadata: Default::default(),
+ handlers: ButtonHandlers::default(),
+ label: self.label,
+ icon: self.icon,
+ data: Rc::new(data),
+ view_type: PhantomData,
+ }
+ }
+ }
+ impl<V: 'static, D: 'static> Button<V, D> {
+ pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
+ self.label = Some(label.into());
+ self
+ }
+ pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
+ self.icon = Some(icon.into());
+ self
+ }
+ pub fn click(
+ self,
+ handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static,
+ ) -> Self {
+ let data = self.data.clone();
+ Element::click(
+ self,
+ MouseButton::Left,
+ move |view, _, cx| {
+ handler(view, data.as_ref(), cx);
+ },
+ )
+ }
+ }
+ pub fn button<V>() -> Button<V, ()> {
+ Button::new()
+ }
+ impl<V: 'static, D: 'static> Button<V, D> {
+ fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
+ let button = frame()
+ .fill(rose_pine::dawn().error(0.5))
+ .h_4()
+ .children(self.label.clone());
+ if let Some(handler) = self.handlers.click.clone() {
+ let data = self.data.clone();
+ button
+ .mouse_down(
+ MouseButton::Left,
+ move |view, event, cx| { handler(view, data.as_ref(), cx) },
+ )
+ } else {
+ button
+ }
+ }
+ }
+}
+mod element {
+ use crate::{
+ adapter::Adapter, color::Hsla, hoverable::Hoverable,
+ style::{Display, Fill, OptionalStyle, Overflow, Position},
+ };
+ use anyhow::Result;
+ pub use gpui::LayoutContext;
+ use gpui::{
+ geometry::{DefinedLength, Length, OptionalPoint},
+ platform::{MouseButton, MouseButtonEvent},
+ EngineLayout, EventContext, RenderContext, ViewContext,
+ };
+ use playground_macros::tailwind_lengths;
+ use std::{
+ any::{Any, TypeId},
+ cell::Cell, rc::Rc,
+ };
+ pub use crate::paint_context::PaintContext;
+ pub use taffy::tree::NodeId;
+ pub struct Layout<'a, E: ?Sized> {
+ pub from_engine: EngineLayout,
+ pub from_element: &'a mut E,
+ }
+ pub struct ElementMetadata<V> {
+ pub style: OptionalStyle,
+ pub handlers: Vec<EventHandler<V>>,
+ }
+ pub struct EventHandler<V> {
+ handler: Rc<dyn Fn(&mut V, &dyn Any, &mut EventContext<V>)>,
+ event_type: TypeId,
+ outside_bounds: bool,
+ }
+ impl<V> Clone for EventHandler<V> {
+ fn clone(&self) -> Self {
+ Self {
+ handler: self.handler.clone(),
+ event_type: self.event_type,
+ outside_bounds: self.outside_bounds,
+ }
+ }
+ }
+ impl<V> Default for ElementMetadata<V> {
+ fn default() -> Self {
+ Self {
+ style: OptionalStyle::default(),
+ handlers: Vec::new(),
+ }
+ }
+ }
+ pub trait Element<V: 'static>: 'static {
+ type Layout: 'static;
+ fn declared_style(&mut self) -> &mut OptionalStyle;
+ fn computed_style(&mut self) -> &OptionalStyle {
+ self.declared_style()
+ }
+ fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>>;
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut LayoutContext<V>,
+ ) -> Result<(NodeId, Self::Layout)>;
+ fn paint<'a>(
+ &mut self,
+ layout: Layout<Self::Layout>,
+ view: &mut V,
+ cx: &mut PaintContext<V>,
+ ) -> Result<()>;
+ /// Convert to a dynamically-typed element suitable for layout and paint.
+ fn into_any(self) -> AnyElement<V>
+ where
+ Self: 'static + Sized,
+ {
+ AnyElement {
+ element: Box::new(self) as Box<dyn ElementObject<V>>,
+ layout: None,
+ }
+ }
+ fn adapt(self) -> Adapter<V>
+ where
+ Self: Sized,
+ Self: Element<V>,
+ {
+ Adapter(self.into_any())
+ }
+ fn click(
+ self,
+ button: MouseButton,
+ handler: impl Fn(&mut V, &MouseButtonEvent, &mut ViewContext<V>) + 'static,
+ ) -> Self
+ where
+ Self: Sized,
+ {
+ let pressed: Rc<Cell<bool>> = Default::default();
+ self.mouse_down(
+ button,
+ {
+ let pressed = pressed.clone();
+ move |_, _, _| {
+ pressed.set(true);
+ }
+ },
+ )
+ .mouse_up_outside(
+ button,
+ {
+ let pressed = pressed.clone();
+ move |_, _, _| {
+ pressed.set(false);
+ }
+ },
+ )
+ .mouse_up(
+ button,
+ move |view, event, event_cx| {
+ if pressed.get() {
+ pressed.set(false);
+ handler(view, event, event_cx);
+ }
+ },
+ )
+ }
+ fn mouse_down(
+ mut self,
+ button: MouseButton,
+ handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+ ) -> Self
+ where
+ Self: Sized,
+ {
+ self.handlers_mut()
+ .push(EventHandler {
+ handler: Rc::new(move |view, event, event_cx| {
+ let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+ if event.button == button && event.is_down {
+ handler(view, event, event_cx);
+ }
+ }),
+ event_type: TypeId::of::<MouseButtonEvent>(),
+ outside_bounds: false,
+ });
+ self
+ }
+ fn mouse_down_outside(
+ mut self,
+ button: MouseButton,
+ handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+ ) -> Self
+ where
+ Self: Sized,
+ {
+ self.handlers_mut()
+ .push(EventHandler {
+ handler: Rc::new(move |view, event, event_cx| {
+ let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+ if event.button == button && event.is_down {
+ handler(view, event, event_cx);
+ }
+ }),
+ event_type: TypeId::of::<MouseButtonEvent>(),
+ outside_bounds: true,
+ });
+ self
+ }
+ fn mouse_up(
+ mut self,
+ button: MouseButton,
+ handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+ ) -> Self
+ where
+ Self: Sized,
+ {
+ self.handlers_mut()
+ .push(EventHandler {
+ handler: Rc::new(move |view, event, event_cx| {
+ let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+ if event.button == button && !event.is_down {
+ handler(view, event, event_cx);
+ }
+ }),
+ event_type: TypeId::of::<MouseButtonEvent>(),
+ outside_bounds: false,
+ });
+ self
+ }
+ fn mouse_up_outside(
+ mut self,
+ button: MouseButton,
+ handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+ ) -> Self
+ where
+ Self: Sized,
+ {
+ self.handlers_mut()
+ .push(EventHandler {
+ handler: Rc::new(move |view, event, event_cx| {
+ let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+ if event.button == button && !event.is_down {
+ handler(view, event, event_cx);
+ }
+ }),
+ event_type: TypeId::of::<MouseButtonEvent>(),
+ outside_bounds: true,
+ });
+ self
+ }
+ fn block(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().display = Some(Display::Block);
+ self
+ }
+ fn flex(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().display = Some(Display::Flex);
+ self
+ }
+ fn grid(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().display = Some(Display::Grid);
+ self
+ }
+ fn overflow_visible(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self
+ .declared_style()
+ .overflow = OptionalPoint {
+ x: Some(Overflow::Visible),
+ y: Some(Overflow::Visible),
+ };
+ self
+ }
+ fn overflow_hidden(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self
+ .declared_style()
+ .overflow = OptionalPoint {
+ x: Some(Overflow::Hidden),
+ y: Some(Overflow::Hidden),
+ };
+ self
+ }
+ fn overflow_scroll(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self
+ .declared_style()
+ .overflow = OptionalPoint {
+ x: Some(Overflow::Scroll),
+ y: Some(Overflow::Scroll),
+ };
+ self
+ }
+ fn overflow_x_visible(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().overflow.x = Some(Overflow::Visible);
+ self
+ }
+ fn overflow_x_hidden(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().overflow.x = Some(Overflow::Hidden);
+ self
+ }
+ fn overflow_x_scroll(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().overflow.x = Some(Overflow::Scroll);
+ self
+ }
+ fn overflow_y_visible(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().overflow.y = Some(Overflow::Visible);
+ self
+ }
+ fn overflow_y_hidden(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().overflow.y = Some(Overflow::Hidden);
+ self
+ }
+ fn overflow_y_scroll(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().overflow.y = Some(Overflow::Scroll);
+ self
+ }
+ fn relative(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().position = Some(Position::Relative);
+ self
+ }
+ fn absolute(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().position = Some(Position::Absolute);
+ self
+ }
+ fn inset_0(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Pixels(0.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_px(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Pixels(1.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_0_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.125).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_1(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.25).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_1_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.375).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_2(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.5).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_2_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.625).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_3(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.75).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_3_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.875).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_4(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.25).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_6(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.5).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_7(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.75).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_8(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_9(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.25).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_10(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.5).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_11(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.75).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_12(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(3.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_14(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(3.5).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_16(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(4.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_20(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(5.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_24(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(6.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_28(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(7.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_32(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(8.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_36(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(9.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_40(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(10.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_44(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(11.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_48(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(12.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_52(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(13.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_56(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(14.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_60(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(15.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_64(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(16.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_72(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(18.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_80(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(20.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_96(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(24.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_half(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_1_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_2_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_1_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(25.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_2_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_3_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(75.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_1_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(20.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_2_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(40.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_3_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(60.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_4_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(80.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_1_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(16.666667).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_2_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_3_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_4_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_5_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(83.333333).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_1_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(8.333333).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_2_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(16.666667).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_3_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(25.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_4_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_5_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(41.666667).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_6_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_7_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(58.333333).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_8_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_9_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(75.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_10_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(83.333333).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_11_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(91.666667).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn inset_full(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(100.).into();
+ {
+ let inset = self
+ .computed_style()
+ .inset
+ .get_or_insert_with(Default::default);
+ inset.top = length;
+ inset.right = length;
+ inset.bottom = length;
+ inset.left = length;
+ self
+ }
+ }
+ fn w(mut self, width: impl Into<Length>) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().size.width = Some(width.into());
+ self
+ }
+ fn w_auto(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().size.width = Some(Length::Auto);
+ self
+ }
+ fn w_0(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Pixels(0.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_px(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Pixels(1.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_0_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.125).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_1(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.25).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_1_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.375).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_2(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.5).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_2_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.625).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_3(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.75).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_3_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.875).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_4(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.25).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_6(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.5).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_7(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.75).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_8(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_9(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.25).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_10(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.5).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_11(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.75).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_12(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(3.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_14(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(3.5).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_16(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(4.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_20(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(5.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_24(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(6.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_28(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(7.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_32(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(8.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_36(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(9.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_40(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(10.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_44(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(11.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_48(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(12.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_52(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(13.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_56(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(14.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_60(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(15.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_64(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(16.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_72(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(18.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_80(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(20.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_96(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(24.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_half(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_1_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_2_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_1_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(25.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_2_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_3_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(75.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_1_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(20.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_2_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(40.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_3_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(60.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_4_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(80.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_1_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(16.666667).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_2_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_3_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_4_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_5_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(83.333333).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_1_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(8.333333).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_2_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(16.666667).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_3_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(25.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_4_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_5_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(41.666667).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_6_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_7_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(58.333333).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_8_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_9_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(75.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_10_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(83.333333).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_11_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(91.666667).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn w_full(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(100.).into();
+ {
+ self.declared_style().size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_0(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Pixels(0.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_px(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Pixels(1.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_0_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.125).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_1(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.25).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_1_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.375).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_2(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.5).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_2_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.625).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_3(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.75).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_3_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.875).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_4(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.25).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_6(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.5).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_7(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.75).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_8(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_9(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.25).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_10(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.5).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_11(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.75).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_12(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(3.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_14(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(3.5).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_16(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(4.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_20(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(5.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_24(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(6.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_28(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(7.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_32(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(8.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_36(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(9.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_40(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(10.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_44(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(11.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_48(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(12.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_52(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(13.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_56(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(14.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_60(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(15.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_64(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(16.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_72(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(18.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_80(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(20.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_96(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(24.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_half(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_1_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_2_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_1_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(25.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_2_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_3_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(75.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_1_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(20.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_2_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(40.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_3_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(60.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_4_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(80.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_1_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(16.666667).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_2_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_3_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_4_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_5_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(83.333333).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_1_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(8.333333).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_2_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(16.666667).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_3_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(25.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_4_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_5_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(41.666667).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_6_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_7_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(58.333333).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_8_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_9_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(75.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_10_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(83.333333).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_11_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(91.666667).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn min_w_full(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(100.).into();
+ {
+ self.declared_style().min_size.width = Some(length);
+ self
+ }
+ }
+ fn h(mut self, height: impl Into<Length>) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().size.height = Some(height.into());
+ self
+ }
+ fn h_auto(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().size.height = Some(Length::Auto);
+ self
+ }
+ fn h_0(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Pixels(0.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_px(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Pixels(1.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_0_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(0.125).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_1(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(0.25).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_1_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(0.375).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_2(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(0.5).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_2_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(0.625).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_3(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(0.75).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_3_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(0.875).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_4(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(1.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(1.25).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_6(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(1.5).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_7(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(1.75).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_8(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(2.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_9(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(2.25).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_10(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(2.5).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_11(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(2.75).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_12(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(3.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_14(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(3.5).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_16(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(4.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_20(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(5.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_24(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(6.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_28(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(7.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_32(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(8.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_36(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(9.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_40(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(10.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_44(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(11.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_48(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(12.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_52(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(13.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_56(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(14.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_60(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(15.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_64(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(16.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_72(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(18.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_80(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(20.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_96(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Rems(24.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_half(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_1_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_2_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_1_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(25.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_2_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_3_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(75.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_1_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(20.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_2_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(40.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_3_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(60.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_4_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(80.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_1_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(16.666667).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_2_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_3_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_4_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_5_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(83.333333).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_1_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(8.333333).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_2_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(16.666667).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_3_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(25.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_4_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_5_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(41.666667).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_6_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_7_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(58.333333).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_8_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_9_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(75.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_10_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(83.333333).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_11_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(91.666667).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn h_full(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let height = DefinedLength::Percent(100.).into();
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+ }
+ fn min_h_0(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Pixels(0.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_px(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Pixels(1.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_0_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.125).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_1(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.25).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_1_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.375).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_2(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.5).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_2_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.625).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_3(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.75).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_3_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(0.875).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_4(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_5(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.25).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_6(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.5).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_7(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(1.75).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_8(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_9(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.25).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_10(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.5).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_11(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(2.75).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_12(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(3.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_14(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(3.5).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_16(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(4.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_20(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(5.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_24(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(6.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_28(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(7.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_32(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(8.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_36(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(9.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_40(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(10.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_44(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(11.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_48(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(12.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_52(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(13.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_56(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(14.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_60(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(15.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_64(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(16.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_72(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(18.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_80(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(20.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_96(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Rems(24.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_half(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_1_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_2_3rd(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_1_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(25.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_2_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_3_4th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(75.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_1_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(20.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_2_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(40.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_3_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(60.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_4_5th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(80.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_1_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(16.666667).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_2_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_3_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_4_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_5_6th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(83.333333).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_1_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(8.333333).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_2_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(16.666667).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_3_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(25.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_4_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(33.333333).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_5_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(41.666667).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_6_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(50.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_7_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(58.333333).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_8_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(66.666667).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_9_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(75.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_10_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(83.333333).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_11_12th(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(91.666667).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn min_h_full(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ let length = DefinedLength::Percent(100.).into();
+ {
+ self.declared_style().min_size.height = Some(length);
+ self
+ }
+ }
+ fn hoverable(self) -> Hoverable<V, Self>
+ where
+ Self: Sized,
+ {
+ Hoverable::new(self)
+ }
+ fn fill(mut self, fill: impl Into<Fill>) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().fill = Some(Some(fill.into()));
+ self
+ }
+ fn text_color(mut self, color: impl Into<Hsla>) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().text_color = Some(Some(color.into()));
+ self
+ }
+ }
+ trait ElementObject<V> {
+ fn style(&mut self) -> &mut OptionalStyle;
+ fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>>;
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut LayoutContext<V>,
+ ) -> Result<(NodeId, Box<dyn Any>)>;
+ fn paint(
+ &mut self,
+ layout: Layout<dyn Any>,
+ view: &mut V,
+ cx: &mut PaintContext<V>,
+ ) -> Result<()>;
+ }
+ impl<V: 'static, E: Element<V>> ElementObject<V> for E {
+ fn style(&mut self) -> &mut OptionalStyle {
+ Element::declared_style(self)
+ }
+ fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
+ Element::handlers_mut(self)
+ }
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut LayoutContext<V>,
+ ) -> Result<(NodeId, Box<dyn Any>)> {
+ let (node_id, layout) = self.layout(view, cx)?;
+ let layout = Box::new(layout) as Box<dyn Any>;
+ Ok((node_id, layout))
+ }
+ fn paint(
+ &mut self,
+ layout: Layout<dyn Any>,
+ view: &mut V,
+ cx: &mut PaintContext<V>,
+ ) -> Result<()> {
+ let layout = Layout {
+ from_engine: layout.from_engine,
+ from_element: layout.from_element.downcast_mut::<E::Layout>().unwrap(),
+ };
+ self.paint(layout, view, cx)
+ }
+ }
+ /// A dynamically typed element.
+ pub struct AnyElement<V> {
+ element: Box<dyn ElementObject<V>>,
+ layout: Option<(NodeId, Box<dyn Any>)>,
+ }
+ impl<V: 'static> AnyElement<V> {
+ pub fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut LayoutContext<V>,
+ ) -> Result<NodeId> {
+ let pushed_text_style = self.push_text_style(cx);
+ let (node_id, layout) = self.element.layout(view, cx)?;
+ self.layout = Some((node_id, layout));
+ if pushed_text_style {
+ cx.pop_text_style();
+ }
+ Ok(node_id)
+ }
+ pub fn push_text_style(&mut self, cx: &mut impl RenderContext) -> bool {
+ let text_style = self.element.style().text_style();
+ if let Some(text_style) = text_style {
+ let mut current_text_style = cx.text_style();
+ text_style.apply(&mut current_text_style);
+ cx.push_text_style(current_text_style);
+ true
+ } else {
+ false
+ }
+ }
+ pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) -> Result<()> {
+ let pushed_text_style = self.push_text_style(cx);
+ let (layout_node_id, element_layout) = self
+ .layout
+ .as_mut()
+ .expect("paint called before layout");
+ let layout = Layout {
+ from_engine: cx
+ .layout_engine()
+ .unwrap()
+ .computed_layout(*layout_node_id)
+ .expect(
+ "you can currently only use playground elements within an adapter",
+ ),
+ from_element: element_layout.as_mut(),
+ };
+ let style = self.element.style();
+ let fill_color = style.fill.flatten().and_then(|fill| fill.color());
+ if let Some(fill_color) = fill_color {
+ cx.scene
+ .push_quad(gpui::scene::Quad {
+ bounds: layout.from_engine.bounds,
+ background: Some(fill_color.into()),
+ border: Default::default(),
+ corner_radii: Default::default(),
+ });
+ }
+ for event_handler in self.element.handlers_mut().iter().cloned() {
+ let EngineLayout { order, bounds } = layout.from_engine;
+ let view_id = cx.view_id();
+ let view_event_handler = event_handler.handler.clone();
+ cx.scene
+ .interactive_regions
+ .push(gpui::scene::InteractiveRegion {
+ order,
+ bounds,
+ outside_bounds: event_handler.outside_bounds,
+ event_handler: Rc::new(move |view, event, window_cx, view_id| {
+ let mut view_context = ViewContext::mutable(
+ window_cx,
+ view_id,
+ );
+ let mut event_context = EventContext::new(&mut view_context);
+ view_event_handler(
+ view.downcast_mut().unwrap(),
+ event,
+ &mut event_context,
+ );
+ }),
+ event_type: event_handler.event_type,
+ view_id,
+ });
+ }
+ self.element.paint(layout, view, cx)?;
+ if pushed_text_style {
+ cx.pop_text_style();
+ }
+ Ok(())
+ }
+ }
+ impl<V: 'static> Element<V> for AnyElement<V> {
+ type Layout = ();
+ fn declared_style(&mut self) -> &mut OptionalStyle {
+ self.element.style()
+ }
+ fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
+ self.element.handlers_mut()
+ }
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut LayoutContext<V>,
+ ) -> Result<(NodeId, Self::Layout)> {
+ Ok((self.layout(view, cx)?, ()))
+ }
+ fn paint(
+ &mut self,
+ layout: Layout<()>,
+ view: &mut V,
+ cx: &mut PaintContext<V>,
+ ) -> Result<()> {
+ self.paint(view, cx)
+ }
+ }
+ pub trait IntoElement<V: 'static> {
+ type Element: Element<V>;
+ fn into_element(self) -> Self::Element;
+ fn into_any_element(self) -> AnyElement<V>
+ where
+ Self: Sized,
+ {
+ self.into_element().into_any()
+ }
+ }
+}
+mod frame {
+ use crate::{
+ element::{
+ AnyElement, Element, EventHandler, IntoElement, Layout, LayoutContext,
+ NodeId, PaintContext,
+ },
+ style::{OptionalStyle, Style},
+ };
+ use anyhow::{anyhow, Result};
+ use gpui::LayoutNodeId;
+ use playground_macros::IntoElement;
+ #[element_crate = "crate"]
+ pub struct Frame<V: 'static> {
+ style: OptionalStyle,
+ handlers: Vec<EventHandler<V>>,
+ children: Vec<AnyElement<V>>,
+ }
+ impl<V: 'static> crate::element::IntoElement<V> for Frame<V> {
+ type Element = Self;
+ fn into_element(self) -> Self {
+ self
+ }
+ }
+ pub fn frame<V>() -> Frame<V> {
+ Frame {
+ style: OptionalStyle::default(),
+ handlers: Vec::new(),
+ children: Vec::new(),
+ }
+ }
+ impl<V: 'static> Element<V> for Frame<V> {
+ type Layout = ();
+ fn declared_style(&mut self) -> &mut OptionalStyle {
+ &mut self.style
+ }
+ fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
+ &mut self.handlers
+ }
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut LayoutContext<V>,
+ ) -> Result<(NodeId, Self::Layout)> {
+ let child_layout_node_ids = self
+ .children
+ .iter_mut()
+ .map(|child| child.layout(view, cx))
+ .collect::<Result<Vec<LayoutNodeId>>>()?;
+ let rem_size = cx.rem_pixels();
+ let style: Style = self.style.into();
+ let node_id = cx
+ .layout_engine()
+ .ok_or_else(|| ::anyhow::__private::must_use({
+ let error = ::anyhow::__private::format_err(
+ format_args!("no layout engine"),
+ );
+ error
+ }))?
+ .add_node(style.to_taffy(rem_size), child_layout_node_ids)?;
+ Ok((node_id, ()))
+ }
+ fn paint(
+ &mut self,
+ layout: Layout<()>,
+ view: &mut V,
+ cx: &mut PaintContext<V>,
+ ) -> Result<()> {
+ for child in &mut self.children {
+ child.paint(view, cx)?;
+ }
+ Ok(())
+ }
+ }
+ impl<V: 'static> Frame<V> {
+ pub fn child(mut self, child: impl IntoElement<V>) -> Self {
+ self.children.push(child.into_any_element());
+ self
+ }
+ pub fn children<I, E>(mut self, children: I) -> Self
+ where
+ I: IntoIterator<Item = E>,
+ E: IntoElement<V>,
+ {
+ self.children.extend(children.into_iter().map(|e| e.into_any_element()));
+ self
+ }
+ }
+}
+mod hoverable {
+ use std::{cell::Cell, marker::PhantomData, rc::Rc};
+ use gpui::{
+ geometry::{rect::RectF, vector::Vector2F},
+ scene::MouseMove, EngineLayout,
+ };
+ use crate::{element::Element, style::{OptionalStyle, Style}};
+ pub struct Hoverable<V, E> {
+ hover_style: OptionalStyle,
+ computed_style: Option<Style>,
+ view_type: PhantomData<V>,
+ child: E,
+ }
+ impl<V, E> Hoverable<V, E> {
+ pub fn new(child: E) -> Self {
+ Self {
+ hover_style: OptionalStyle::default(),
+ computed_style: None,
+ view_type: PhantomData,
+ child,
+ }
+ }
+ }
+ impl<V: 'static, E: Element<V>> Element<V> for Hoverable<V, E> {
+ type Layout = E::Layout;
+ fn declared_style(&mut self) -> &mut OptionalStyle {
+ &mut self.hover_style
+ }
+ fn computed_style(&mut self) -> &OptionalStyle {
+ ::core::panicking::panic("not yet implemented")
+ }
+ fn handlers_mut(&mut self) -> &mut Vec<crate::element::EventHandler<V>> {
+ self.child.handlers_mut()
+ }
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut gpui::LayoutContext<V>,
+ ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
+ self.child.layout(view, cx)
+ }
+ fn paint<'a>(
+ &mut self,
+ layout: crate::element::Layout<Self::Layout>,
+ view: &mut V,
+ cx: &mut crate::element::PaintContext<V>,
+ ) -> anyhow::Result<()> {
+ let EngineLayout { bounds, order } = layout.from_engine;
+ let window_bounds = RectF::new(Vector2F::zero(), cx.window_size());
+ let was_hovered = Rc::new(Cell::new(false));
+ self.child.paint(layout, view, cx)?;
+ cx.draw_interactive_region(
+ order,
+ window_bounds,
+ false,
+ move |view, event: &MouseMove, cx| {
+ let is_hovered = bounds.contains_point(cx.mouse_position());
+ if is_hovered != was_hovered.get() {
+ was_hovered.set(is_hovered);
+ cx.repaint();
+ }
+ },
+ );
+ Ok(())
+ }
+ }
+}
+mod paint_context {
+ use std::{any::TypeId, rc::Rc};
+ use derive_more::{Deref, DerefMut};
+ use gpui::{geometry::rect::RectF, EventContext, RenderContext, ViewContext};
+ pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext};
+ pub use taffy::tree::NodeId;
+ pub struct PaintContext<'a, 'b, 'c, 'd, V> {
+ #[deref]
+ #[deref_mut]
+ pub(crate) legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
+ pub(crate) scene: &'d mut gpui::SceneBuilder,
+ }
+ impl<'a, 'b, 'c, 'd, V> ::core::ops::Deref for PaintContext<'a, 'b, 'c, 'd, V> {
+ type Target = &'d mut LegacyPaintContext<'a, 'b, 'c, V>;
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &self.legacy_cx
+ }
+ }
+ impl<'a, 'b, 'c, 'd, V> ::core::ops::DerefMut for PaintContext<'a, 'b, 'c, 'd, V> {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.legacy_cx
+ }
+ }
+ impl<V> RenderContext for PaintContext<'_, '_, '_, '_, V> {
+ fn text_style(&self) -> gpui::fonts::TextStyle {
+ self.legacy_cx.text_style()
+ }
+ fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
+ self.legacy_cx.push_text_style(style)
+ }
+ fn pop_text_style(&mut self) {
+ self.legacy_cx.pop_text_style()
+ }
+ }
+ impl<'a, 'b, 'c, 'd, V: 'static> PaintContext<'a, 'b, 'c, 'd, V> {
+ pub fn new(
+ legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
+ scene: &'d mut gpui::SceneBuilder,
+ ) -> Self {
+ Self { legacy_cx, scene }
+ }
+ pub fn draw_interactive_region<E: 'static>(
+ &mut self,
+ order: u32,
+ bounds: RectF,
+ outside_bounds: bool,
+ event_handler: impl Fn(&mut V, &E, &mut EventContext<V>) + 'static,
+ ) {
+ self.scene
+ .interactive_regions
+ .push(gpui::scene::InteractiveRegion {
+ order,
+ bounds,
+ outside_bounds,
+ event_handler: Rc::new(move |view, event, window_cx, view_id| {
+ let mut view_context = ViewContext::mutable(window_cx, view_id);
+ let mut event_context = EventContext::new(&mut view_context);
+ event_handler(
+ view.downcast_mut().unwrap(),
+ event.downcast_ref().unwrap(),
+ &mut event_context,
+ );
+ }),
+ event_type: TypeId::of::<E>(),
+ view_id: self.view_id(),
+ });
+ }
+ }
+}
+mod style {
+ use crate::color::Hsla;
+ use gpui::geometry::{
+ DefinedLength, Edges, Length, OptionalEdges, OptionalPoint, OptionalSize, Point,
+ Size,
+ };
+ use optional::Optional;
+ pub use taffy::style::{
+ AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap,
+ JustifyContent, Overflow, Position,
+ };
+ pub struct Style {
+ /// What layout strategy should be used?
+ pub display: Display,
+ /// How children overflowing their container should affect layout
+ #[optional]
+ pub overflow: Point<Overflow>,
+ /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
+ pub scrollbar_width: f32,
+ /// 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?
+ pub inset: Edges<Length>,
+ /// Sets the initial size of the item
+ #[optional]
+ pub size: Size<Length>,
+ /// Controls the minimum size of the item
+ #[optional]
+ pub min_size: Size<Length>,
+ /// Controls the maximum size of the item
+ #[optional]
+ pub max_size: Size<Length>,
+ /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
+ pub aspect_ratio: Option<f32>,
+ /// How large should the margin be on each side?
+ #[optional]
+ pub margin: Edges<Length>,
+ /// How large should the padding be on each side?
+ pub padding: Edges<DefinedLength>,
+ /// How large should the border be on each side?
+ pub border: Edges<DefinedLength>,
+ /// How this node's children aligned in the cross/block axis?
+ pub align_items: Option<AlignItems>,
+ /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
+ pub align_self: Option<AlignSelf>,
+ /// How should content contained within this item be aligned in the cross/block axis
+ pub align_content: Option<AlignContent>,
+ /// How should contained within this item be aligned in the main/inline axis
+ pub justify_content: Option<JustifyContent>,
+ /// How large should the gaps between items in a flex container be?
+ pub gap: Size<DefinedLength>,
+ /// 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<Fill>,
+ /// The color of text within this element. Cascades to children unless overridden.
+ pub text_color: Option<Hsla>,
+ }
+ #[automatically_derived]
+ impl ::core::clone::Clone for Style {
+ #[inline]
+ fn clone(&self) -> Style {
+ Style {
+ display: ::core::clone::Clone::clone(&self.display),
+ overflow: ::core::clone::Clone::clone(&self.overflow),
+ scrollbar_width: ::core::clone::Clone::clone(&self.scrollbar_width),
+ position: ::core::clone::Clone::clone(&self.position),
+ inset: ::core::clone::Clone::clone(&self.inset),
+ size: ::core::clone::Clone::clone(&self.size),
+ min_size: ::core::clone::Clone::clone(&self.min_size),
+ max_size: ::core::clone::Clone::clone(&self.max_size),
+ aspect_ratio: ::core::clone::Clone::clone(&self.aspect_ratio),
+ margin: ::core::clone::Clone::clone(&self.margin),
+ padding: ::core::clone::Clone::clone(&self.padding),
+ border: ::core::clone::Clone::clone(&self.border),
+ align_items: ::core::clone::Clone::clone(&self.align_items),
+ align_self: ::core::clone::Clone::clone(&self.align_self),
+ align_content: ::core::clone::Clone::clone(&self.align_content),
+ justify_content: ::core::clone::Clone::clone(&self.justify_content),
+ gap: ::core::clone::Clone::clone(&self.gap),
+ flex_direction: ::core::clone::Clone::clone(&self.flex_direction),
+ flex_wrap: ::core::clone::Clone::clone(&self.flex_wrap),
+ flex_basis: ::core::clone::Clone::clone(&self.flex_basis),
+ flex_grow: ::core::clone::Clone::clone(&self.flex_grow),
+ flex_shrink: ::core::clone::Clone::clone(&self.flex_shrink),
+ fill: ::core::clone::Clone::clone(&self.fill),
+ text_color: ::core::clone::Clone::clone(&self.text_color),
+ }
+ }
+ }
+ pub struct OptionalStyle {
+ pub display: Option<Display>,
+ pub overflow: OptionalPoint<Overflow>,
+ pub scrollbar_width: Option<f32>,
+ pub position: Option<Position>,
+ pub inset: Option<Edges<Length>>,
+ pub size: OptionalSize<Length>,
+ pub min_size: OptionalSize<Length>,
+ pub max_size: OptionalSize<Length>,
+ pub aspect_ratio: Option<Option<f32>>,
+ pub margin: OptionalEdges<Length>,
+ pub padding: Option<Edges<DefinedLength>>,
+ pub border: Option<Edges<DefinedLength>>,
+ pub align_items: Option<Option<AlignItems>>,
+ pub align_self: Option<Option<AlignSelf>>,
+ pub align_content: Option<Option<AlignContent>>,
+ pub justify_content: Option<Option<JustifyContent>>,
+ pub gap: Option<Size<DefinedLength>>,
+ pub flex_direction: Option<FlexDirection>,
+ pub flex_wrap: Option<FlexWrap>,
+ pub flex_basis: Option<Length>,
+ pub flex_grow: Option<f32>,
+ pub flex_shrink: Option<f32>,
+ pub fill: Option<Option<Fill>>,
+ pub text_color: Option<Option<Hsla>>,
+ }
+ #[automatically_derived]
+ impl ::core::default::Default for OptionalStyle {
+ #[inline]
+ fn default() -> OptionalStyle {
+ OptionalStyle {
+ display: ::core::default::Default::default(),
+ overflow: ::core::default::Default::default(),
+ scrollbar_width: ::core::default::Default::default(),
+ position: ::core::default::Default::default(),
+ inset: ::core::default::Default::default(),
+ size: ::core::default::Default::default(),
+ min_size: ::core::default::Default::default(),
+ max_size: ::core::default::Default::default(),
+ aspect_ratio: ::core::default::Default::default(),
+ margin: ::core::default::Default::default(),
+ padding: ::core::default::Default::default(),
+ border: ::core::default::Default::default(),
+ align_items: ::core::default::Default::default(),
+ align_self: ::core::default::Default::default(),
+ align_content: ::core::default::Default::default(),
+ justify_content: ::core::default::Default::default(),
+ gap: ::core::default::Default::default(),
+ flex_direction: ::core::default::Default::default(),
+ flex_wrap: ::core::default::Default::default(),
+ flex_basis: ::core::default::Default::default(),
+ flex_grow: ::core::default::Default::default(),
+ flex_shrink: ::core::default::Default::default(),
+ fill: ::core::default::Default::default(),
+ text_color: ::core::default::Default::default(),
+ }
+ }
+ }
+ #[automatically_derived]
+ impl ::core::clone::Clone for OptionalStyle {
+ #[inline]
+ fn clone(&self) -> OptionalStyle {
+ OptionalStyle {
+ display: ::core::clone::Clone::clone(&self.display),
+ overflow: ::core::clone::Clone::clone(&self.overflow),
+ scrollbar_width: ::core::clone::Clone::clone(&self.scrollbar_width),
+ position: ::core::clone::Clone::clone(&self.position),
+ inset: ::core::clone::Clone::clone(&self.inset),
+ size: ::core::clone::Clone::clone(&self.size),
+ min_size: ::core::clone::Clone::clone(&self.min_size),
+ max_size: ::core::clone::Clone::clone(&self.max_size),
+ aspect_ratio: ::core::clone::Clone::clone(&self.aspect_ratio),
+ margin: ::core::clone::Clone::clone(&self.margin),
+ padding: ::core::clone::Clone::clone(&self.padding),
+ border: ::core::clone::Clone::clone(&self.border),
+ align_items: ::core::clone::Clone::clone(&self.align_items),
+ align_self: ::core::clone::Clone::clone(&self.align_self),
+ align_content: ::core::clone::Clone::clone(&self.align_content),
+ justify_content: ::core::clone::Clone::clone(&self.justify_content),
+ gap: ::core::clone::Clone::clone(&self.gap),
+ flex_direction: ::core::clone::Clone::clone(&self.flex_direction),
+ flex_wrap: ::core::clone::Clone::clone(&self.flex_wrap),
+ flex_basis: ::core::clone::Clone::clone(&self.flex_basis),
+ flex_grow: ::core::clone::Clone::clone(&self.flex_grow),
+ flex_shrink: ::core::clone::Clone::clone(&self.flex_shrink),
+ fill: ::core::clone::Clone::clone(&self.fill),
+ text_color: ::core::clone::Clone::clone(&self.text_color),
+ }
+ }
+ }
+ impl Optional for OptionalStyle {
+ type Base = Style;
+ fn assign(&self, base: &mut Self::Base) {
+ if let Some(value) = self.display.clone() {
+ base.display = value;
+ }
+ if let Some(value) = self.overflow.clone() {
+ base.overflow = value;
+ }
+ if let Some(value) = self.scrollbar_width.clone() {
+ base.scrollbar_width = value;
+ }
+ if let Some(value) = self.position.clone() {
+ base.position = value;
+ }
+ if let Some(value) = self.inset.clone() {
+ base.inset = value;
+ }
+ if let Some(value) = self.size.clone() {
+ base.size = value;
+ }
+ if let Some(value) = self.min_size.clone() {
+ base.min_size = value;
+ }
+ if let Some(value) = self.max_size.clone() {
+ base.max_size = value;
+ }
+ if let Some(value) = self.aspect_ratio.clone() {
+ base.aspect_ratio = value;
+ }
+ if let Some(value) = self.margin.clone() {
+ base.margin = value;
+ }
+ if let Some(value) = self.padding.clone() {
+ base.padding = value;
+ }
+ if let Some(value) = self.border.clone() {
+ base.border = value;
+ }
+ if let Some(value) = self.align_items.clone() {
+ base.align_items = value;
+ }
+ if let Some(value) = self.align_self.clone() {
+ base.align_self = value;
+ }
+ if let Some(value) = self.align_content.clone() {
+ base.align_content = value;
+ }
+ if let Some(value) = self.justify_content.clone() {
+ base.justify_content = value;
+ }
+ if let Some(value) = self.gap.clone() {
+ base.gap = value;
+ }
+ if let Some(value) = self.flex_direction.clone() {
+ base.flex_direction = value;
+ }
+ if let Some(value) = self.flex_wrap.clone() {
+ base.flex_wrap = value;
+ }
+ if let Some(value) = self.flex_basis.clone() {
+ base.flex_basis = value;
+ }
+ if let Some(value) = self.flex_grow.clone() {
+ base.flex_grow = value;
+ }
+ if let Some(value) = self.flex_shrink.clone() {
+ base.flex_shrink = value;
+ }
+ if let Some(value) = self.fill.clone() {
+ base.fill = value;
+ }
+ if let Some(value) = self.text_color.clone() {
+ base.text_color = value;
+ }
+ }
+ }
+ impl From<OptionalStyle> for Style
+ where
+ Style: Default,
+ {
+ fn from(wrapper: OptionalStyle) -> Self {
+ let mut base = Self::default();
+ wrapper.assign(&mut base);
+ base
+ }
+ }
+ impl Style {
+ pub const DEFAULT: Style = Style {
+ display: Display::DEFAULT,
+ overflow: Point {
+ x: Overflow::Visible,
+ y: Overflow::Visible,
+ },
+ scrollbar_width: 0.0,
+ position: Position::Relative,
+ inset: Edges::auto(),
+ margin: Edges::<Length>::zero(),
+ padding: Edges::<DefinedLength>::zero(),
+ border: Edges::<DefinedLength>::zero(),
+ size: Size::auto(),
+ min_size: Size::auto(),
+ max_size: Size::auto(),
+ aspect_ratio: None,
+ gap: Size::zero(),
+ align_items: None,
+ align_self: None,
+ align_content: None,
+ justify_content: None,
+ flex_direction: FlexDirection::Row,
+ flex_wrap: FlexWrap::NoWrap,
+ flex_grow: 0.0,
+ flex_shrink: 1.0,
+ flex_basis: Length::Auto,
+ fill: None,
+ text_color: None,
+ };
+ pub fn new() -> Self {
+ Self::DEFAULT.clone()
+ }
+ pub fn to_taffy(&self, rem_size: f32) -> 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.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).into(),
+ flex_grow: self.flex_grow,
+ flex_shrink: self.flex_shrink,
+ ..Default::default()
+ }
+ }
+ }
+ impl Default for Style {
+ fn default() -> Self {
+ Self::DEFAULT.clone()
+ }
+ }
+ impl OptionalStyle {
+ pub fn text_style(&self) -> Option<OptionalTextStyle> {
+ self.text_color.map(|color| OptionalTextStyle { color })
+ }
+ }
+ pub struct OptionalTextStyle {
+ color: Option<Hsla>,
+ }
+ impl OptionalTextStyle {
+ pub fn apply(&self, style: &mut gpui::fonts::TextStyle) {
+ if let Some(color) = self.color {
+ style.color = color.into();
+ }
+ }
+ }
+ pub enum Fill {
+ Color(Hsla),
+ }
+ #[automatically_derived]
+ impl ::core::clone::Clone for Fill {
+ #[inline]
+ fn clone(&self) -> Fill {
+ match self {
+ Fill::Color(__self_0) => {
+ Fill::Color(::core::clone::Clone::clone(__self_0))
+ }
+ }
+ }
+ }
+ impl Fill {
+ pub fn color(&self) -> Option<Hsla> {
+ match self {
+ Fill::Color(color) => Some(*color),
+ }
+ }
+ }
+ impl Default for Fill {
+ fn default() -> Self {
+ Self::Color(Hsla::default())
+ }
+ }
+ impl From<Hsla> for Fill {
+ fn from(color: Hsla) -> Self {
+ Self::Color(color)
+ }
+ }
+}
+mod text {
+ use crate::{
+ element::{Element, ElementMetadata, EventHandler, IntoElement},
+ style::Style,
+ };
+ use gpui::{geometry::Size, text_layout::LineLayout, RenderContext};
+ use parking_lot::Mutex;
+ use std::sync::Arc;
+ impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
+ type Element = Text<V>;
+ fn into_element(self) -> Self::Element {
+ Text {
+ text: self.into(),
+ metadata: Default::default(),
+ }
+ }
+ }
+ pub struct Text<V> {
+ text: ArcCow<'static, str>,
+ metadata: ElementMetadata<V>,
+ }
+ impl<V: 'static> Element<V> for Text<V> {
+ type Layout = Arc<Mutex<Option<TextLayout>>>;
+ fn declared_style(&mut self) -> &mut crate::style::OptionalStyle {
+ &mut self.metadata.style
+ }
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut gpui::LayoutContext<V>,
+ ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
+ let rem_size = cx.rem_pixels();
+ let fonts = cx.platform().fonts();
+ let text_style = cx.text_style();
+ let line_height = cx.font_cache().line_height(text_style.font_size);
+ let layout_engine = cx.layout_engine().expect("no layout engine present");
+ let text = self.text.clone();
+ let layout = Arc::new(Mutex::new(None));
+ let style: Style = self.metadata.style.into();
+ let node_id = layout_engine
+ .add_measured_node(
+ style.to_taffy(rem_size),
+ {
+ let layout = layout.clone();
+ move |params| {
+ let line_layout = fonts
+ .layout_line(
+ text.as_ref(),
+ text_style.font_size,
+ &[(text.len(), text_style.to_run())],
+ );
+ let size = Size {
+ width: line_layout.width,
+ height: line_height,
+ };
+ layout
+ .lock()
+ .replace(TextLayout {
+ line_layout: Arc::new(line_layout),
+ line_height,
+ });
+ size
+ }
+ },
+ )?;
+ Ok((node_id, layout))
+ }
+ fn paint<'a>(
+ &mut self,
+ layout: crate::element::Layout<Arc<Mutex<Option<TextLayout>>>>,
+ view: &mut V,
+ cx: &mut crate::element::PaintContext<V>,
+ ) -> anyhow::Result<()> {
+ let element_layout_lock = layout.from_element.lock();
+ let element_layout = element_layout_lock
+ .as_ref()
+ .expect("layout has not been performed");
+ let line_layout = element_layout.line_layout.clone();
+ let line_height = element_layout.line_height;
+ drop(element_layout_lock);
+ let text_style = cx.text_style();
+ let line = gpui::text_layout::Line::new(
+ line_layout,
+ &[(self.text.len(), text_style.to_run())],
+ );
+ line.paint(
+ cx.scene,
+ layout.from_engine.bounds.origin(),
+ layout.from_engine.bounds,
+ line_height,
+ cx.legacy_cx,
+ );
+ Ok(())
+ }
+ fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
+ &mut self.metadata.handlers
+ }
+ }
+ pub struct TextLayout {
+ line_layout: Arc<LineLayout>,
+ line_height: f32,
+ }
+ pub enum ArcCow<'a, T: ?Sized> {
+ Borrowed(&'a T),
+ Owned(Arc<T>),
+ }
+ impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
+ fn clone(&self) -> Self {
+ match self {
+ Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
+ Self::Owned(owned) => Self::Owned(owned.clone()),
+ }
+ }
+ }
+ impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
+ fn from(s: &'a T) -> Self {
+ Self::Borrowed(s)
+ }
+ }
+ impl<T> From<Arc<T>> for ArcCow<'_, T> {
+ fn from(s: Arc<T>) -> Self {
+ Self::Owned(s)
+ }
+ }
+ impl From<String> for ArcCow<'_, str> {
+ fn from(value: String) -> Self {
+ Self::Owned(value.into())
+ }
+ }
+ impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
+ type Target = T;
+ fn deref(&self) -> &Self::Target {
+ match self {
+ ArcCow::Borrowed(s) => s,
+ ArcCow::Owned(s) => s.as_ref(),
+ }
+ }
+ }
+ impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
+ fn as_ref(&self) -> &T {
+ match self {
+ ArcCow::Borrowed(borrowed) => borrowed,
+ ArcCow::Owned(owned) => owned.as_ref(),
+ }
+ }
+ }
+}
+mod themes {
+ use crate::color::{Hsla, Lerp};
+ use std::ops::Range;
+ pub mod rose_pine {
+ use std::ops::Range;
+ use crate::{
+ color::{hsla, rgb, Hsla},
+ ThemeColors,
+ };
+ pub struct RosePineThemes {
+ pub default: RosePinePalette,
+ pub dawn: RosePinePalette,
+ pub moon: RosePinePalette,
+ }
+ pub struct RosePinePalette {
+ pub base: Hsla,
+ pub surface: Hsla,
+ pub overlay: Hsla,
+ pub muted: Hsla,
+ pub subtle: Hsla,
+ pub text: Hsla,
+ pub love: Hsla,
+ pub gold: Hsla,
+ pub rose: Hsla,
+ pub pine: Hsla,
+ pub foam: Hsla,
+ pub iris: Hsla,
+ pub highlight_low: Hsla,
+ pub highlight_med: Hsla,
+ pub highlight_high: Hsla,
+ }
+ #[automatically_derived]
+ impl ::core::clone::Clone for RosePinePalette {
+ #[inline]
+ fn clone(&self) -> RosePinePalette {
+ let _: ::core::clone::AssertParamIsClone<Hsla>;
+ *self
+ }
+ }
+ #[automatically_derived]
+ impl ::core::marker::Copy for RosePinePalette {}
+ #[automatically_derived]
+ impl ::core::fmt::Debug for RosePinePalette {
+ fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
+ let names: &'static _ = &[
+ "base",
+ "surface",
+ "overlay",
+ "muted",
+ "subtle",
+ "text",
+ "love",
+ "gold",
+ "rose",
+ "pine",
+ "foam",
+ "iris",
+ "highlight_low",
+ "highlight_med",
+ "highlight_high",
+ ];
+ let values: &[&dyn ::core::fmt::Debug] = &[
+ &self.base,
+ &self.surface,
+ &self.overlay,
+ &self.muted,
+ &self.subtle,
+ &self.text,
+ &self.love,
+ &self.gold,
+ &self.rose,
+ &self.pine,
+ &self.foam,
+ &self.iris,
+ &self.highlight_low,
+ &self.highlight_med,
+ &&self.highlight_high,
+ ];
+ ::core::fmt::Formatter::debug_struct_fields_finish(
+ f,
+ "RosePinePalette",
+ names,
+ values,
+ )
+ }
+ }
+ impl RosePinePalette {
+ pub fn default() -> RosePinePalette {
+ RosePinePalette {
+ base: rgb(0x191724),
+ surface: rgb(0x1f1d2e),
+ overlay: rgb(0x26233a),
+ muted: rgb(0x6e6a86),
+ subtle: rgb(0x908caa),
+ text: rgb(0xe0def4),
+ love: rgb(0xeb6f92),
+ gold: rgb(0xf6c177),
+ rose: rgb(0xebbcba),
+ pine: rgb(0x31748f),
+ foam: rgb(0x9ccfd8),
+ iris: rgb(0xc4a7e7),
+ highlight_low: rgb(0x21202e),
+ highlight_med: rgb(0x403d52),
+ highlight_high: rgb(0x524f67),
+ }
+ }
+ pub fn moon() -> RosePinePalette {
+ RosePinePalette {
+ base: rgb(0x232136),
+ surface: rgb(0x2a273f),
+ overlay: rgb(0x393552),
+ muted: rgb(0x6e6a86),
+ subtle: rgb(0x908caa),
+ text: rgb(0xe0def4),
+ love: rgb(0xeb6f92),
+ gold: rgb(0xf6c177),
+ rose: rgb(0xea9a97),
+ pine: rgb(0x3e8fb0),
+ foam: rgb(0x9ccfd8),
+ iris: rgb(0xc4a7e7),
+ highlight_low: rgb(0x2a283e),
+ highlight_med: rgb(0x44415a),
+ highlight_high: rgb(0x56526e),
+ }
+ }
+ pub fn dawn() -> RosePinePalette {
+ RosePinePalette {
+ base: rgb(0xfaf4ed),
+ surface: rgb(0xfffaf3),
+ overlay: rgb(0xf2e9e1),
+ muted: rgb(0x9893a5),
+ subtle: rgb(0x797593),
+ text: rgb(0x575279),
+ love: rgb(0xb4637a),
+ gold: rgb(0xea9d34),
+ rose: rgb(0xd7827e),
+ pine: rgb(0x286983),
+ foam: rgb(0x56949f),
+ iris: rgb(0x907aa9),
+ highlight_low: rgb(0xf4ede8),
+ highlight_med: rgb(0xdfdad9),
+ highlight_high: rgb(0xcecacd),
+ }
+ }
+ }
+ pub fn default() -> ThemeColors {
+ theme_colors(&RosePinePalette::default())
+ }
+ pub fn moon() -> ThemeColors {
+ theme_colors(&RosePinePalette::moon())
+ }
+ pub fn dawn() -> ThemeColors {
+ theme_colors(&RosePinePalette::dawn())
+ }
+ fn theme_colors(p: &RosePinePalette) -> ThemeColors {
+ ThemeColors {
+ base: scale_sl(p.base, (0.8, 0.8), (1.2, 1.2)),
+ surface: scale_sl(p.surface, (0.8, 0.8), (1.2, 1.2)),
+ overlay: scale_sl(p.overlay, (0.8, 0.8), (1.2, 1.2)),
+ muted: scale_sl(p.muted, (0.8, 0.8), (1.2, 1.2)),
+ subtle: scale_sl(p.subtle, (0.8, 0.8), (1.2, 1.2)),
+ text: scale_sl(p.text, (0.8, 0.8), (1.2, 1.2)),
+ highlight_low: scale_sl(p.highlight_low, (0.8, 0.8), (1.2, 1.2)),
+ highlight_med: scale_sl(p.highlight_med, (0.8, 0.8), (1.2, 1.2)),
+ highlight_high: scale_sl(p.highlight_high, (0.8, 0.8), (1.2, 1.2)),
+ success: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
+ warning: scale_sl(p.gold, (0.8, 0.8), (1.2, 1.2)),
+ error: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
+ inserted: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
+ deleted: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
+ modified: scale_sl(p.rose, (0.8, 0.8), (1.2, 1.2)),
+ }
+ }
+ /// Produces a range by multiplying the saturation and lightness of the base color by the given
+ /// start and end factors.
+ fn scale_sl(
+ base: Hsla,
+ (start_s, start_l): (f32, f32),
+ (end_s, end_l): (f32, f32),
+ ) -> Range<Hsla> {
+ let start = hsla(base.h, base.s * start_s, base.l * start_l, base.a);
+ let end = hsla(base.h, base.s * end_s, base.l * end_l, base.a);
+ Range { start, end }
+ }
+ }
+ pub struct ThemeColors {
+ pub base: Range<Hsla>,
+ pub surface: Range<Hsla>,
+ pub overlay: Range<Hsla>,
+ pub muted: Range<Hsla>,
+ pub subtle: Range<Hsla>,
+ pub text: Range<Hsla>,
+ pub highlight_low: Range<Hsla>,
+ pub highlight_med: Range<Hsla>,
+ pub highlight_high: Range<Hsla>,
+ pub success: Range<Hsla>,
+ pub warning: Range<Hsla>,
+ pub error: Range<Hsla>,
+ pub inserted: Range<Hsla>,
+ pub deleted: Range<Hsla>,
+ pub modified: Range<Hsla>,
+ }
+ impl ThemeColors {
+ pub fn base(&self, level: f32) -> Hsla {
+ self.base.lerp(level)
+ }
+ pub fn surface(&self, level: f32) -> Hsla {
+ self.surface.lerp(level)
+ }
+ pub fn overlay(&self, level: f32) -> Hsla {
+ self.overlay.lerp(level)
+ }
+ pub fn muted(&self, level: f32) -> Hsla {
+ self.muted.lerp(level)
+ }
+ pub fn subtle(&self, level: f32) -> Hsla {
+ self.subtle.lerp(level)
+ }
+ pub fn text(&self, level: f32) -> Hsla {
+ self.text.lerp(level)
+ }
+ pub fn highlight_low(&self, level: f32) -> Hsla {
+ self.highlight_low.lerp(level)
+ }
+ pub fn highlight_med(&self, level: f32) -> Hsla {
+ self.highlight_med.lerp(level)
+ }
+ pub fn highlight_high(&self, level: f32) -> Hsla {
+ self.highlight_high.lerp(level)
+ }
+ pub fn success(&self, level: f32) -> Hsla {
+ self.success.lerp(level)
+ }
+ pub fn warning(&self, level: f32) -> Hsla {
+ self.warning.lerp(level)
+ }
+ pub fn error(&self, level: f32) -> Hsla {
+ self.error.lerp(level)
+ }
+ pub fn inserted(&self, level: f32) -> Hsla {
+ self.inserted.lerp(level)
+ }
+ pub fn deleted(&self, level: f32) -> Hsla {
+ self.deleted.lerp(level)
+ }
+ pub fn modified(&self, level: f32) -> Hsla {
+ self.modified.lerp(level)
+ }
+ }
+}
+mod view {
+ use crate::element::{AnyElement, Element};
+ use gpui::{Element as _, ViewContext};
+ pub fn view<F, E>(mut render: F) -> ViewFn
+ where
+ F: 'static + FnMut(&mut ViewContext<ViewFn>) -> E,
+ E: Element<ViewFn>,
+ {
+ ViewFn(Box::new(move |cx| (render)(cx).into_any()))
+ }
+ pub struct ViewFn(Box<dyn FnMut(&mut ViewContext<ViewFn>) -> AnyElement<ViewFn>>);
+ impl gpui::Entity for ViewFn {
+ type Event = ();
+ }
+ impl gpui::View for ViewFn {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
+ (self.0)(cx).adapt().into_any()
+ }
+ }
+}
+fn main() {
+ SimpleLogger::init(LevelFilter::Info, Default::default())
+ .expect("could not initialize logger");
+ gpui::App::new(())
+ .unwrap()
+ .run(|cx| {
+ cx.add_window(
+ WindowOptions {
+ bounds: gpui::platform::WindowBounds::Fixed(
+ RectF::new(vec2f(0., 0.), vec2f(400., 300.)),
+ ),
+ center: true,
+ ..Default::default()
+ },
+ |_| view(|_| playground(&rose_pine::moon())),
+ );
+ cx.platform().activate(true);
+ });
+}
+fn playground<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+ frame()
+ .text_color(black())
+ .h_full()
+ .w_half()
+ .fill(theme.success(0.5))
+ .child(
+ button()
+ .label("Hello")
+ .click(|_, _, _| {
+ ::std::io::_print(format_args!("click!\n"));
+ }),
+ )
+}