Move all crates to a top-level crates folder

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Nathan Sobo 2021-10-04 13:22:21 -06:00
parent d768224182
commit fdfed3d7db
282 changed files with 195588 additions and 16 deletions

29
crates/buffer/Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "buffer"
version = "0.1.0"
edition = "2018"
[features]
test-support = ["rand"]
[dependencies]
anyhow = "1.0.38"
arrayvec = "0.7.1"
clock = { path = "../clock" }
gpui = { path = "../gpui" }
lazy_static = "1.4"
log = "0.4"
parking_lot = "0.11.1"
rand = { version = "0.8.3", optional = true }
seahash = "4.1"
serde = { version = "1", features = ["derive"] }
similar = "1.3"
smallvec = { version = "1.6", features = ["union"] }
sum_tree = { path = "../sum_tree" }
tree-sitter = "0.19.5"
zrpc = { path = "../zrpc" }
[dev-dependencies]
rand = "0.8.3"
tree-sitter-rust = "0.19.0"
unindent = "0.1.7"

View file

@ -0,0 +1,77 @@
use super::{Buffer, Content};
use anyhow::Result;
use std::{cmp::Ordering, ops::Range};
use sum_tree::Bias;
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub struct Anchor {
pub offset: usize,
pub bias: Bias,
pub version: clock::Global,
}
impl Anchor {
pub fn min() -> Self {
Self {
offset: 0,
bias: Bias::Left,
version: Default::default(),
}
}
pub fn max() -> Self {
Self {
offset: usize::MAX,
bias: Bias::Right,
version: Default::default(),
}
}
pub fn cmp<'a>(&self, other: &Anchor, buffer: impl Into<Content<'a>>) -> Result<Ordering> {
let buffer = buffer.into();
if self == other {
return Ok(Ordering::Equal);
}
let offset_comparison = if self.version == other.version {
self.offset.cmp(&other.offset)
} else {
buffer
.full_offset_for_anchor(self)
.cmp(&buffer.full_offset_for_anchor(other))
};
Ok(offset_comparison.then_with(|| self.bias.cmp(&other.bias)))
}
pub fn bias_left(&self, buffer: &Buffer) -> Anchor {
if self.bias == Bias::Left {
self.clone()
} else {
buffer.anchor_before(self)
}
}
pub fn bias_right(&self, buffer: &Buffer) -> Anchor {
if self.bias == Bias::Right {
self.clone()
} else {
buffer.anchor_after(self)
}
}
}
pub trait AnchorRangeExt {
fn cmp<'a>(&self, b: &Range<Anchor>, buffer: impl Into<Content<'a>>) -> Result<Ordering>;
}
impl AnchorRangeExt for Range<Anchor> {
fn cmp<'a>(&self, other: &Range<Anchor>, buffer: impl Into<Content<'a>>) -> Result<Ordering> {
let buffer = buffer.into();
Ok(match self.start.cmp(&other.start, &buffer)? {
Ordering::Equal => other.end.cmp(&self.end, buffer)?,
ord @ _ => ord,
})
}
}

View file

@ -0,0 +1,96 @@
use crate::syntax_theme::SyntaxTheme;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct HighlightMap(Arc<[HighlightId]>);
#[derive(Clone, Copy, Debug)]
pub struct HighlightId(pub u32);
const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
impl HighlightMap {
pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self {
// For each capture name in the highlight query, find the longest
// key in the theme's syntax styles that matches all of the
// dot-separated components of the capture name.
HighlightMap(
capture_names
.iter()
.map(|capture_name| {
theme
.highlights
.iter()
.enumerate()
.filter_map(|(i, (key, _))| {
let mut len = 0;
let capture_parts = capture_name.split('.');
for key_part in key.split('.') {
if capture_parts.clone().any(|part| part == key_part) {
len += 1;
} else {
return None;
}
}
Some((i, len))
})
.max_by_key(|(_, len)| *len)
.map_or(DEFAULT_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32))
})
.collect(),
)
}
pub fn get(&self, capture_id: u32) -> HighlightId {
self.0
.get(capture_id as usize)
.copied()
.unwrap_or(DEFAULT_HIGHLIGHT_ID)
}
}
impl Default for HighlightMap {
fn default() -> Self {
Self(Arc::new([]))
}
}
impl Default for HighlightId {
fn default() -> Self {
DEFAULT_HIGHLIGHT_ID
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::color::Color;
#[test]
fn test_highlight_map() {
let theme = SyntaxTheme::new(
[
("function", Color::from_u32(0x100000ff)),
("function.method", Color::from_u32(0x200000ff)),
("function.async", Color::from_u32(0x300000ff)),
("variable.builtin.self.rust", Color::from_u32(0x400000ff)),
("variable.builtin", Color::from_u32(0x500000ff)),
("variable", Color::from_u32(0x600000ff)),
]
.iter()
.map(|(name, color)| (name.to_string(), (*color).into()))
.collect(),
);
let capture_names = &[
"function.special".to_string(),
"function.async.rust".to_string(),
"variable.builtin.self".to_string(),
];
let map = HighlightMap::new(capture_names, &theme);
assert_eq!(theme.highlight_name(map.get(0)), Some("function"));
assert_eq!(theme.highlight_name(map.get(1)), Some("function.async"));
assert_eq!(theme.highlight_name(map.get(2)), Some("variable.builtin"));
}
}

View file

@ -0,0 +1,135 @@
use crate::{HighlightMap, SyntaxTheme};
use parking_lot::Mutex;
use serde::Deserialize;
use std::{path::Path, str, sync::Arc};
use tree_sitter::{Language as Grammar, Query};
pub use tree_sitter::{Parser, Tree};
#[derive(Default, Deserialize)]
pub struct LanguageConfig {
pub name: String,
pub path_suffixes: Vec<String>,
}
#[derive(Deserialize)]
pub struct BracketPair {
pub start: String,
pub end: String,
}
pub struct Language {
pub config: LanguageConfig,
pub grammar: Grammar,
pub highlight_query: Query,
pub brackets_query: Query,
pub highlight_map: Mutex<HighlightMap>,
}
#[derive(Default)]
pub struct LanguageRegistry {
languages: Vec<Arc<Language>>,
}
impl LanguageRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, language: Arc<Language>) {
self.languages.push(language);
}
pub fn set_theme(&self, theme: &SyntaxTheme) {
for language in &self.languages {
language.set_theme(theme);
}
}
pub fn select_language(&self, path: impl AsRef<Path>) -> Option<&Arc<Language>> {
let path = path.as_ref();
let filename = path.file_name().and_then(|name| name.to_str());
let extension = path.extension().and_then(|name| name.to_str());
let path_suffixes = [extension, filename];
self.languages.iter().find(|language| {
language
.config
.path_suffixes
.iter()
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
})
}
}
impl Language {
pub fn name(&self) -> &str {
self.config.name.as_str()
}
pub fn highlight_map(&self) -> HighlightMap {
self.highlight_map.lock().clone()
}
pub fn set_theme(&self, theme: &SyntaxTheme) {
*self.highlight_map.lock() = HighlightMap::new(self.highlight_query.capture_names(), theme);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_select_language() {
let grammar = tree_sitter_rust::language();
let registry = LanguageRegistry {
languages: vec![
Arc::new(Language {
config: LanguageConfig {
name: "Rust".to_string(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
grammar,
highlight_query: Query::new(grammar, "").unwrap(),
brackets_query: Query::new(grammar, "").unwrap(),
highlight_map: Default::default(),
}),
Arc::new(Language {
config: LanguageConfig {
name: "Make".to_string(),
path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
..Default::default()
},
grammar,
highlight_query: Query::new(grammar, "").unwrap(),
brackets_query: Query::new(grammar, "").unwrap(),
highlight_map: Default::default(),
}),
],
};
// matching file extension
assert_eq!(
registry.select_language("zed/lib.rs").map(|l| l.name()),
Some("Rust")
);
assert_eq!(
registry.select_language("zed/lib.mk").map(|l| l.name()),
Some("Make")
);
// matching filename
assert_eq!(
registry.select_language("zed/Makefile").map(|l| l.name()),
Some("Make")
);
// matching suffix that is not the full file extension or filename
assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None);
assert_eq!(
registry.select_language("zed/a.cars").map(|l| l.name()),
None
);
assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
}
}

4098
crates/buffer/src/lib.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,127 @@
use super::Operation;
use std::{fmt::Debug, ops::Add};
use sum_tree::{Cursor, Dimension, Edit, Item, KeyedItem, SumTree, Summary};
#[derive(Clone, Debug)]
pub struct OperationQueue(SumTree<Operation>);
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct OperationKey(clock::Lamport);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct OperationSummary {
pub key: OperationKey,
pub len: usize,
}
impl OperationKey {
pub fn new(timestamp: clock::Lamport) -> Self {
Self(timestamp)
}
}
impl OperationQueue {
pub fn new() -> Self {
OperationQueue(SumTree::new())
}
pub fn len(&self) -> usize {
self.0.summary().len
}
pub fn insert(&mut self, mut ops: Vec<Operation>) {
ops.sort_by_key(|op| op.lamport_timestamp());
ops.dedup_by_key(|op| op.lamport_timestamp());
self.0
.edit(ops.into_iter().map(Edit::Insert).collect(), &());
}
pub fn drain(&mut self) -> Self {
let clone = self.clone();
self.0 = SumTree::new();
clone
}
pub fn cursor(&self) -> Cursor<Operation, ()> {
self.0.cursor()
}
}
impl Summary for OperationSummary {
type Context = ();
fn add_summary(&mut self, other: &Self, _: &()) {
assert!(self.key < other.key);
self.key = other.key;
self.len += other.len;
}
}
impl<'a> Add<&'a Self> for OperationSummary {
type Output = Self;
fn add(self, other: &Self) -> Self {
assert!(self.key < other.key);
OperationSummary {
key: other.key,
len: self.len + other.len,
}
}
}
impl<'a> Dimension<'a, OperationSummary> for OperationKey {
fn add_summary(&mut self, summary: &OperationSummary, _: &()) {
assert!(*self <= summary.key);
*self = summary.key;
}
}
impl Item for Operation {
type Summary = OperationSummary;
fn summary(&self) -> Self::Summary {
OperationSummary {
key: OperationKey::new(self.lamport_timestamp()),
len: 1,
}
}
}
impl KeyedItem for Operation {
type Key = OperationKey;
fn key(&self) -> Self::Key {
OperationKey::new(self.lamport_timestamp())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_len() {
let mut clock = clock::Lamport::new(0);
let mut queue = OperationQueue::new();
assert_eq!(queue.len(), 0);
queue.insert(vec![
Operation::Test(clock.tick()),
Operation::Test(clock.tick()),
]);
assert_eq!(queue.len(), 2);
queue.insert(vec![Operation::Test(clock.tick())]);
assert_eq!(queue.len(), 3);
drop(queue.drain());
assert_eq!(queue.len(), 0);
queue.insert(vec![Operation::Test(clock.tick())]);
assert_eq!(queue.len(), 1);
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct TestOperation(clock::Lamport);
}

129
crates/buffer/src/point.rs Normal file
View file

@ -0,0 +1,129 @@
use std::{
cmp::Ordering,
ops::{Add, AddAssign, Sub},
};
#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash)]
pub struct Point {
pub row: u32,
pub column: u32,
}
impl Point {
pub const MAX: Self = Self {
row: u32::MAX,
column: u32::MAX,
};
pub fn new(row: u32, column: u32) -> Self {
Point { row, column }
}
pub fn zero() -> Self {
Point::new(0, 0)
}
pub fn is_zero(&self) -> bool {
self.row == 0 && self.column == 0
}
}
impl<'a> Add<&'a Self> for Point {
type Output = Point;
fn add(self, other: &'a Self) -> Self::Output {
if other.row == 0 {
Point::new(self.row, self.column + other.column)
} else {
Point::new(self.row + other.row, other.column)
}
}
}
impl Add for Point {
type Output = Point;
fn add(self, other: Self) -> Self::Output {
self + &other
}
}
impl<'a> Sub<&'a Self> for Point {
type Output = Point;
fn sub(self, other: &'a Self) -> Self::Output {
debug_assert!(*other <= self);
if self.row == other.row {
Point::new(0, self.column - other.column)
} else {
Point::new(self.row - other.row, self.column)
}
}
}
impl Sub for Point {
type Output = Point;
fn sub(self, other: Self) -> Self::Output {
self - &other
}
}
impl<'a> AddAssign<&'a Self> for Point {
fn add_assign(&mut self, other: &'a Self) {
*self += *other;
}
}
impl AddAssign<Self> for Point {
fn add_assign(&mut self, other: Self) {
if other.row == 0 {
self.column += other.column;
} else {
self.row += other.row;
self.column = other.column;
}
}
}
impl PartialOrd for Point {
fn partial_cmp(&self, other: &Point) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Point {
#[cfg(target_pointer_width = "64")]
fn cmp(&self, other: &Point) -> Ordering {
let a = (self.row as usize) << 32 | self.column as usize;
let b = (other.row as usize) << 32 | other.column as usize;
a.cmp(&b)
}
#[cfg(target_pointer_width = "32")]
fn cmp(&self, other: &Point) -> Ordering {
match self.row.cmp(&other.row) {
Ordering::Equal => self.column.cmp(&other.column),
comparison @ _ => comparison,
}
}
}
impl Into<tree_sitter::Point> for Point {
fn into(self) -> tree_sitter::Point {
tree_sitter::Point {
row: self.row as usize,
column: self.column as usize,
}
}
}
impl From<tree_sitter::Point> for Point {
fn from(point: tree_sitter::Point) -> Self {
Self {
row: point.row as u32,
column: point.column as u32,
}
}
}

View file

@ -0,0 +1,28 @@
use rand::prelude::*;
pub struct RandomCharIter<T: Rng>(T);
impl<T: Rng> RandomCharIter<T> {
pub fn new(rng: T) -> Self {
Self(rng)
}
}
impl<T: Rng> Iterator for RandomCharIter<T> {
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
match self.0.gen_range(0..100) {
// whitespace
0..=19 => [' ', '\n', '\t'].choose(&mut self.0).copied(),
// two-byte greek letters
20..=32 => char::from_u32(self.0.gen_range(('α' as u32)..('ω' as u32 + 1))),
// three-byte characters
33..=45 => ['✋', '✅', '❌', '❎', '⭐'].choose(&mut self.0).copied(),
// four-byte characters
46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.0).copied(),
// ascii letters
_ => Some(self.0.gen_range(b'a'..b'z' + 1).into()),
}
}
}

613
crates/buffer/src/rope.rs Normal file
View file

@ -0,0 +1,613 @@
use super::Point;
use arrayvec::ArrayString;
use smallvec::SmallVec;
use std::{cmp, ops::Range, str};
use sum_tree::{self, Bias, SumTree};
#[cfg(test)]
const CHUNK_BASE: usize = 6;
#[cfg(not(test))]
const CHUNK_BASE: usize = 16;
#[derive(Clone, Default, Debug)]
pub struct Rope {
chunks: SumTree<Chunk>,
}
impl Rope {
pub fn new() -> Self {
Self::default()
}
pub fn append(&mut self, rope: Rope) {
let mut chunks = rope.chunks.cursor::<()>();
chunks.next(&());
if let Some(chunk) = chunks.item() {
if self.chunks.last().map_or(false, |c| c.0.len() < CHUNK_BASE)
|| chunk.0.len() < CHUNK_BASE
{
self.push(&chunk.0);
chunks.next(&());
}
}
self.chunks.push_tree(chunks.suffix(&()), &());
self.check_invariants();
}
pub fn push(&mut self, text: &str) {
let mut new_chunks = SmallVec::<[_; 16]>::new();
let mut new_chunk = ArrayString::new();
for ch in text.chars() {
if new_chunk.len() + ch.len_utf8() > 2 * CHUNK_BASE {
new_chunks.push(Chunk(new_chunk));
new_chunk = ArrayString::new();
}
new_chunk.push(ch);
}
if !new_chunk.is_empty() {
new_chunks.push(Chunk(new_chunk));
}
let mut new_chunks = new_chunks.into_iter();
let mut first_new_chunk = new_chunks.next();
self.chunks.update_last(
|last_chunk| {
if let Some(first_new_chunk_ref) = first_new_chunk.as_mut() {
if last_chunk.0.len() + first_new_chunk_ref.0.len() <= 2 * CHUNK_BASE {
last_chunk.0.push_str(&first_new_chunk.take().unwrap().0);
} else {
let mut text = ArrayString::<{ 4 * CHUNK_BASE }>::new();
text.push_str(&last_chunk.0);
text.push_str(&first_new_chunk_ref.0);
let (left, right) = text.split_at(find_split_ix(&text));
last_chunk.0.clear();
last_chunk.0.push_str(left);
first_new_chunk_ref.0.clear();
first_new_chunk_ref.0.push_str(right);
}
}
},
&(),
);
self.chunks
.extend(first_new_chunk.into_iter().chain(new_chunks), &());
self.check_invariants();
}
fn check_invariants(&self) {
#[cfg(test)]
{
// Ensure all chunks except maybe the last one are not underflowing.
// Allow some wiggle room for multibyte characters at chunk boundaries.
let mut chunks = self.chunks.cursor::<()>().peekable();
while let Some(chunk) = chunks.next() {
if chunks.peek().is_some() {
assert!(chunk.0.len() + 3 >= CHUNK_BASE);
}
}
}
}
pub fn summary(&self) -> TextSummary {
self.chunks.summary()
}
pub fn len(&self) -> usize {
self.chunks.extent(&())
}
pub fn max_point(&self) -> Point {
self.chunks.extent(&())
}
pub fn cursor(&self, offset: usize) -> Cursor {
Cursor::new(self, offset)
}
pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
self.chars_at(0)
}
pub fn chars_at(&self, start: usize) -> impl Iterator<Item = char> + '_ {
self.chunks_in_range(start..self.len()).flat_map(str::chars)
}
pub fn chunks<'a>(&'a self) -> Chunks<'a> {
self.chunks_in_range(0..self.len())
}
pub fn chunks_in_range<'a>(&'a self, range: Range<usize>) -> Chunks<'a> {
Chunks::new(self, range)
}
pub fn to_point(&self, offset: usize) -> Point {
assert!(offset <= self.summary().bytes);
let mut cursor = self.chunks.cursor::<(usize, Point)>();
cursor.seek(&offset, Bias::Left, &());
let overshoot = offset - cursor.start().0;
cursor.start().1
+ cursor
.item()
.map_or(Point::zero(), |chunk| chunk.to_point(overshoot))
}
pub fn to_offset(&self, point: Point) -> usize {
assert!(point <= self.summary().lines);
let mut cursor = self.chunks.cursor::<(Point, usize)>();
cursor.seek(&point, Bias::Left, &());
let overshoot = point - cursor.start().0;
cursor.start().1 + cursor.item().map_or(0, |chunk| chunk.to_offset(overshoot))
}
pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize {
let mut cursor = self.chunks.cursor::<usize>();
cursor.seek(&offset, Bias::Left, &());
if let Some(chunk) = cursor.item() {
let mut ix = offset - cursor.start();
while !chunk.0.is_char_boundary(ix) {
match bias {
Bias::Left => {
ix -= 1;
offset -= 1;
}
Bias::Right => {
ix += 1;
offset += 1;
}
}
}
offset
} else {
self.summary().bytes
}
}
pub fn clip_point(&self, point: Point, bias: Bias) -> Point {
let mut cursor = self.chunks.cursor::<Point>();
cursor.seek(&point, Bias::Right, &());
if let Some(chunk) = cursor.item() {
let overshoot = point - cursor.start();
*cursor.start() + chunk.clip_point(overshoot, bias)
} else {
self.summary().lines
}
}
}
impl<'a> From<&'a str> for Rope {
fn from(text: &'a str) -> Self {
let mut rope = Self::new();
rope.push(text);
rope
}
}
impl Into<String> for Rope {
fn into(self) -> String {
self.chunks().collect()
}
}
pub struct Cursor<'a> {
rope: &'a Rope,
chunks: sum_tree::Cursor<'a, Chunk, usize>,
offset: usize,
}
impl<'a> Cursor<'a> {
pub fn new(rope: &'a Rope, offset: usize) -> Self {
let mut chunks = rope.chunks.cursor();
chunks.seek(&offset, Bias::Right, &());
Self {
rope,
chunks,
offset,
}
}
pub fn seek_forward(&mut self, end_offset: usize) {
debug_assert!(end_offset >= self.offset);
self.chunks.seek_forward(&end_offset, Bias::Right, &());
self.offset = end_offset;
}
pub fn slice(&mut self, end_offset: usize) -> Rope {
debug_assert!(
end_offset >= self.offset,
"cannot slice backwards from {} to {}",
self.offset,
end_offset
);
let mut slice = Rope::new();
if let Some(start_chunk) = self.chunks.item() {
let start_ix = self.offset - self.chunks.start();
let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start();
slice.push(&start_chunk.0[start_ix..end_ix]);
}
if end_offset > self.chunks.end(&()) {
self.chunks.next(&());
slice.append(Rope {
chunks: self.chunks.slice(&end_offset, Bias::Right, &()),
});
if let Some(end_chunk) = self.chunks.item() {
let end_ix = end_offset - self.chunks.start();
slice.push(&end_chunk.0[..end_ix]);
}
}
self.offset = end_offset;
slice
}
pub fn summary(&mut self, end_offset: usize) -> TextSummary {
debug_assert!(end_offset >= self.offset);
let mut summary = TextSummary::default();
if let Some(start_chunk) = self.chunks.item() {
let start_ix = self.offset - self.chunks.start();
let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start();
summary = TextSummary::from(&start_chunk.0[start_ix..end_ix]);
}
if end_offset > self.chunks.end(&()) {
self.chunks.next(&());
summary += &self.chunks.summary(&end_offset, Bias::Right, &());
if let Some(end_chunk) = self.chunks.item() {
let end_ix = end_offset - self.chunks.start();
summary += TextSummary::from(&end_chunk.0[..end_ix]);
}
}
summary
}
pub fn suffix(mut self) -> Rope {
self.slice(self.rope.chunks.extent(&()))
}
pub fn offset(&self) -> usize {
self.offset
}
}
pub struct Chunks<'a> {
chunks: sum_tree::Cursor<'a, Chunk, usize>,
range: Range<usize>,
}
impl<'a> Chunks<'a> {
pub fn new(rope: &'a Rope, range: Range<usize>) -> Self {
let mut chunks = rope.chunks.cursor();
chunks.seek(&range.start, Bias::Right, &());
Self { chunks, range }
}
pub fn offset(&self) -> usize {
self.range.start.max(*self.chunks.start())
}
pub fn seek(&mut self, offset: usize) {
if offset >= self.chunks.end(&()) {
self.chunks.seek_forward(&offset, Bias::Right, &());
} else {
self.chunks.seek(&offset, Bias::Right, &());
}
self.range.start = offset;
}
pub fn peek(&self) -> Option<&'a str> {
if let Some(chunk) = self.chunks.item() {
let offset = *self.chunks.start();
if self.range.end > offset {
let start = self.range.start.saturating_sub(*self.chunks.start());
let end = self.range.end - self.chunks.start();
return Some(&chunk.0[start..chunk.0.len().min(end)]);
}
}
None
}
}
impl<'a> Iterator for Chunks<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let result = self.peek();
if result.is_some() {
self.chunks.next(&());
}
result
}
}
#[derive(Clone, Debug, Default)]
struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>);
impl Chunk {
fn to_point(&self, target: usize) -> Point {
let mut offset = 0;
let mut point = Point::new(0, 0);
for ch in self.0.chars() {
if offset >= target {
break;
}
if ch == '\n' {
point.row += 1;
point.column = 0;
} else {
point.column += ch.len_utf8() as u32;
}
offset += ch.len_utf8();
}
point
}
fn to_offset(&self, target: Point) -> usize {
let mut offset = 0;
let mut point = Point::new(0, 0);
for ch in self.0.chars() {
if point >= target {
if point > target {
panic!("point {:?} is inside of character {:?}", target, ch);
}
break;
}
if ch == '\n' {
point.row += 1;
point.column = 0;
} else {
point.column += ch.len_utf8() as u32;
}
offset += ch.len_utf8();
}
offset
}
fn clip_point(&self, target: Point, bias: Bias) -> Point {
for (row, line) in self.0.split('\n').enumerate() {
if row == target.row as usize {
let mut column = target.column.min(line.len() as u32);
while !line.is_char_boundary(column as usize) {
match bias {
Bias::Left => column -= 1,
Bias::Right => column += 1,
}
}
return Point::new(row as u32, column);
}
}
unreachable!()
}
}
impl sum_tree::Item for Chunk {
type Summary = TextSummary;
fn summary(&self) -> Self::Summary {
TextSummary::from(self.0.as_str())
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TextSummary {
pub bytes: usize,
pub lines: Point,
pub first_line_chars: u32,
pub last_line_chars: u32,
pub longest_row: u32,
pub longest_row_chars: u32,
}
impl<'a> From<&'a str> for TextSummary {
fn from(text: &'a str) -> Self {
let mut lines = Point::new(0, 0);
let mut first_line_chars = 0;
let mut last_line_chars = 0;
let mut longest_row = 0;
let mut longest_row_chars = 0;
for c in text.chars() {
if c == '\n' {
lines.row += 1;
lines.column = 0;
last_line_chars = 0;
} else {
lines.column += c.len_utf8() as u32;
last_line_chars += 1;
}
if lines.row == 0 {
first_line_chars = last_line_chars;
}
if last_line_chars > longest_row_chars {
longest_row = lines.row;
longest_row_chars = last_line_chars;
}
}
TextSummary {
bytes: text.len(),
lines,
first_line_chars,
last_line_chars,
longest_row,
longest_row_chars,
}
}
}
impl sum_tree::Summary for TextSummary {
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &Self::Context) {
*self += summary;
}
}
impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
fn add_assign(&mut self, other: &'a Self) {
let joined_chars = self.last_line_chars + other.first_line_chars;
if joined_chars > self.longest_row_chars {
self.longest_row = self.lines.row;
self.longest_row_chars = joined_chars;
}
if other.longest_row_chars > self.longest_row_chars {
self.longest_row = self.lines.row + other.longest_row;
self.longest_row_chars = other.longest_row_chars;
}
if self.lines.row == 0 {
self.first_line_chars += other.first_line_chars;
}
if other.lines.row == 0 {
self.last_line_chars += other.first_line_chars;
} else {
self.last_line_chars = other.last_line_chars;
}
self.bytes += other.bytes;
self.lines += &other.lines;
}
}
impl std::ops::AddAssign<Self> for TextSummary {
fn add_assign(&mut self, other: Self) {
*self += &other;
}
}
impl<'a> sum_tree::Dimension<'a, TextSummary> for usize {
fn add_summary(&mut self, summary: &'a TextSummary, _: &()) {
*self += summary.bytes;
}
}
impl<'a> sum_tree::Dimension<'a, TextSummary> for Point {
fn add_summary(&mut self, summary: &'a TextSummary, _: &()) {
*self += &summary.lines;
}
}
fn find_split_ix(text: &str) -> usize {
let mut ix = text.len() / 2;
while !text.is_char_boundary(ix) {
if ix < 2 * CHUNK_BASE {
ix += 1;
} else {
ix = (text.len() / 2) - 1;
break;
}
}
while !text.is_char_boundary(ix) {
ix -= 1;
}
debug_assert!(ix <= 2 * CHUNK_BASE);
debug_assert!(text.len() - ix <= 2 * CHUNK_BASE);
ix
}
#[cfg(test)]
mod tests {
use super::*;
use crate::random_char_iter::RandomCharIter;
use rand::prelude::*;
use std::env;
use Bias::{Left, Right};
#[test]
fn test_all_4_byte_chars() {
let mut rope = Rope::new();
let text = "🏀".repeat(256);
rope.push(&text);
assert_eq!(rope.text(), text);
}
#[gpui::test(iterations = 100)]
fn test_random(mut rng: StdRng) {
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let mut expected = String::new();
let mut actual = Rope::new();
for _ in 0..operations {
let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right);
let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left);
let len = rng.gen_range(0..=64);
let new_text: String = RandomCharIter::new(&mut rng).take(len).collect();
let mut new_actual = Rope::new();
let mut cursor = actual.cursor(0);
new_actual.append(cursor.slice(start_ix));
new_actual.push(&new_text);
cursor.seek_forward(end_ix);
new_actual.append(cursor.suffix());
actual = new_actual;
expected.replace_range(start_ix..end_ix, &new_text);
assert_eq!(actual.text(), expected);
log::info!("text: {:?}", expected);
for _ in 0..5 {
let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right);
let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left);
assert_eq!(
actual.chunks_in_range(start_ix..end_ix).collect::<String>(),
&expected[start_ix..end_ix]
);
}
let mut point = Point::new(0, 0);
for (ix, ch) in expected.char_indices().chain(Some((expected.len(), '\0'))) {
assert_eq!(actual.to_point(ix), point, "to_point({})", ix);
assert_eq!(actual.to_offset(point), ix, "to_offset({:?})", point);
if ch == '\n' {
point.row += 1;
point.column = 0
} else {
point.column += ch.len_utf8() as u32;
}
}
for _ in 0..5 {
let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right);
let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left);
assert_eq!(
actual.cursor(start_ix).summary(end_ix),
TextSummary::from(&expected[start_ix..end_ix])
);
}
}
}
fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize {
while !text.is_char_boundary(offset) {
match bias {
Bias::Left => offset -= 1,
Bias::Right => offset += 1,
}
}
offset
}
impl Rope {
fn text(&self) -> String {
let mut text = String::new();
for chunk in self.chunks.cursor::<()>() {
text.push_str(&chunk.0);
}
text
}
}
}

View file

@ -0,0 +1,75 @@
use crate::{Anchor, Buffer, Point, ToOffset as _, ToPoint as _};
use std::{cmp::Ordering, mem, ops::Range};
pub type SelectionSetId = clock::Lamport;
pub type SelectionsVersion = usize;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum SelectionGoal {
None,
Column(u32),
ColumnRange { start: u32, end: u32 },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Selection {
pub id: usize,
pub start: Anchor,
pub end: Anchor,
pub reversed: bool,
pub goal: SelectionGoal,
}
impl Selection {
pub fn head(&self) -> &Anchor {
if self.reversed {
&self.start
} else {
&self.end
}
}
pub fn set_head(&mut self, buffer: &Buffer, cursor: Anchor) {
if cursor.cmp(self.tail(), buffer).unwrap() < Ordering::Equal {
if !self.reversed {
mem::swap(&mut self.start, &mut self.end);
self.reversed = true;
}
self.start = cursor;
} else {
if self.reversed {
mem::swap(&mut self.start, &mut self.end);
self.reversed = false;
}
self.end = cursor;
}
}
pub fn tail(&self) -> &Anchor {
if self.reversed {
&self.end
} else {
&self.start
}
}
pub fn point_range(&self, buffer: &Buffer) -> Range<Point> {
let start = self.start.to_point(buffer);
let end = self.end.to_point(buffer);
if self.reversed {
end..start
} else {
start..end
}
}
pub fn offset_range(&self, buffer: &Buffer) -> Range<usize> {
let start = self.start.to_offset(buffer);
let end = self.end.to_offset(buffer);
if self.reversed {
end..start
} else {
start..end
}
}
}

View file

@ -0,0 +1,49 @@
use std::collections::HashMap;
use crate::HighlightId;
use gpui::fonts::HighlightStyle;
use serde::Deserialize;
pub struct SyntaxTheme {
pub(crate) highlights: Vec<(String, HighlightStyle)>,
}
impl SyntaxTheme {
pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
Self { highlights }
}
pub fn highlight_style(&self, id: HighlightId) -> Option<HighlightStyle> {
self.highlights
.get(id.0 as usize)
.map(|entry| entry.1.clone())
}
#[cfg(any(test, feature = "test-support"))]
pub fn highlight_name(&self, id: HighlightId) -> Option<&str> {
self.highlights.get(id.0 as usize).map(|e| e.0.as_str())
}
}
impl<'de> Deserialize<'de> for SyntaxTheme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let syntax_data: HashMap<String, HighlightStyle> = Deserialize::deserialize(deserializer)?;
let mut result = Self::new(Vec::new());
for (key, style) in syntax_data {
match result
.highlights
.binary_search_by(|(needle, _)| needle.cmp(&key))
{
Ok(i) | Err(i) => {
result.highlights.insert(i, (key, style));
}
}
}
Ok(result)
}
}

8
crates/clock/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "clock"
version = "0.1.0"
edition = "2018"
[dependencies]
smallvec = { version = "1.6", features = ["union"] }
zrpc = { path = "../zrpc" }

224
crates/clock/src/lib.rs Normal file
View file

@ -0,0 +1,224 @@
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
fmt,
ops::{Add, AddAssign},
slice,
};
pub type ReplicaId = u16;
pub type Seq = u32;
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd)]
pub struct Local {
pub replica_id: ReplicaId,
pub value: Seq,
}
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
pub struct Lamport {
pub replica_id: ReplicaId,
pub value: Seq,
}
impl Local {
pub fn new(replica_id: ReplicaId) -> Self {
Self {
replica_id,
value: 1,
}
}
pub fn tick(&mut self) -> Self {
let timestamp = *self;
self.value += 1;
timestamp
}
pub fn observe(&mut self, timestamp: Self) {
if timestamp.replica_id == self.replica_id {
self.value = cmp::max(self.value, timestamp.value + 1);
}
}
}
impl<'a> Add<&'a Self> for Local {
type Output = Local;
fn add(self, other: &'a Self) -> Self::Output {
cmp::max(&self, other).clone()
}
}
impl<'a> AddAssign<&'a Local> for Local {
fn add_assign(&mut self, other: &Self) {
if *self < *other {
*self = other.clone();
}
}
}
#[derive(Clone, Default, Hash, Eq, PartialEq)]
pub struct Global(SmallVec<[Local; 3]>);
impl From<Vec<zrpc::proto::VectorClockEntry>> for Global {
fn from(message: Vec<zrpc::proto::VectorClockEntry>) -> Self {
let mut version = Self::new();
for entry in message {
version.observe(Local {
replica_id: entry.replica_id as ReplicaId,
value: entry.timestamp,
});
}
version
}
}
impl<'a> From<&'a Global> for Vec<zrpc::proto::VectorClockEntry> {
fn from(version: &'a Global) -> Self {
version
.iter()
.map(|entry| zrpc::proto::VectorClockEntry {
replica_id: entry.replica_id as u32,
timestamp: entry.value,
})
.collect()
}
}
impl Global {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, replica_id: ReplicaId) -> Seq {
self.0
.iter()
.find(|t| t.replica_id == replica_id)
.map_or(0, |t| t.value)
}
pub fn observe(&mut self, timestamp: Local) {
if let Some(entry) = self
.0
.iter_mut()
.find(|t| t.replica_id == timestamp.replica_id)
{
entry.value = cmp::max(entry.value, timestamp.value);
} else {
self.0.push(timestamp);
}
}
pub fn join(&mut self, other: &Self) {
for timestamp in other.0.iter() {
self.observe(*timestamp);
}
}
pub fn meet(&mut self, other: &Self) {
for timestamp in other.0.iter() {
if let Some(entry) = self
.0
.iter_mut()
.find(|t| t.replica_id == timestamp.replica_id)
{
entry.value = cmp::min(entry.value, timestamp.value);
} else {
self.0.push(*timestamp);
}
}
}
pub fn observed(&self, timestamp: Local) -> bool {
self.get(timestamp.replica_id) >= timestamp.value
}
pub fn changed_since(&self, other: &Self) -> bool {
self.0.iter().any(|t| t.value > other.get(t.replica_id))
}
pub fn iter(&self) -> slice::Iter<Local> {
self.0.iter()
}
}
impl PartialOrd for Global {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let mut global_ordering = Ordering::Equal;
for timestamp in self.0.iter().chain(other.0.iter()) {
let ordering = self
.get(timestamp.replica_id)
.cmp(&other.get(timestamp.replica_id));
if ordering != Ordering::Equal {
if global_ordering == Ordering::Equal {
global_ordering = ordering;
} else if ordering != global_ordering {
return None;
}
}
}
Some(global_ordering)
}
}
impl Ord for Lamport {
fn cmp(&self, other: &Self) -> Ordering {
// Use the replica id to break ties between concurrent events.
self.value
.cmp(&other.value)
.then_with(|| self.replica_id.cmp(&other.replica_id))
}
}
impl PartialOrd for Lamport {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Lamport {
pub fn new(replica_id: ReplicaId) -> Self {
Self {
value: 1,
replica_id,
}
}
pub fn tick(&mut self) -> Self {
let timestamp = *self;
self.value += 1;
timestamp
}
pub fn observe(&mut self, timestamp: Self) {
self.value = cmp::max(self.value, timestamp.value) + 1;
}
}
impl fmt::Debug for Local {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Local {{{}: {}}}", self.replica_id, self.value)
}
}
impl fmt::Debug for Lamport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value)
}
}
impl fmt::Debug for Global {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Global {{")?;
for (i, element) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}: {}", element.replica_id, element.value)?;
}
write!(f, "}}")
}
}

16
crates/fsevent/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "fsevent"
version = "2.0.2"
license = "MIT"
edition = "2018"
[dependencies]
bitflags = "1"
fsevent-sys = "3.0.2"
parking_lot = "0.11.1"
[dev-dependencies]
tempdir = "0.3.7"
[package.metadata.docs.rs]
targets = ["x86_64-apple-darwin"]

View file

@ -0,0 +1,16 @@
use fsevent::EventStream;
use std::{env::args, path::Path, time::Duration};
fn main() {
let paths = args().skip(1).collect::<Vec<_>>();
let paths = paths.iter().map(Path::new).collect::<Vec<_>>();
assert!(paths.len() > 0, "Must pass 1 or more paths as arguments");
let (stream, _handle) = EventStream::new(&paths, Duration::from_millis(100));
stream.run(|events| {
eprintln!("event batch");
for event in events {
eprintln!(" {:?}", event);
}
true
});
}

413
crates/fsevent/src/lib.rs Normal file
View file

@ -0,0 +1,413 @@
#![cfg(target_os = "macos")]
use bitflags::bitflags;
use fsevent_sys::{self as fs, core_foundation as cf};
use parking_lot::Mutex;
use std::{
convert::AsRef,
ffi::{c_void, CStr, OsStr},
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
slice,
sync::Arc,
time::Duration,
};
#[derive(Clone, Debug)]
pub struct Event {
pub event_id: u64,
pub flags: StreamFlags,
pub path: PathBuf,
}
pub struct EventStream {
stream: fs::FSEventStreamRef,
state: Arc<Mutex<Lifecycle>>,
callback: Box<Option<RunCallback>>,
}
type RunCallback = Box<dyn FnMut(Vec<Event>) -> bool>;
enum Lifecycle {
New,
Running(cf::CFRunLoopRef),
Stopped,
}
pub struct Handle(Arc<Mutex<Lifecycle>>);
unsafe impl Send for EventStream {}
unsafe impl Send for Lifecycle {}
impl EventStream {
pub fn new(paths: &[&Path], latency: Duration) -> (Self, Handle) {
unsafe {
let callback = Box::new(None);
let stream_context = fs::FSEventStreamContext {
version: 0,
info: callback.as_ref() as *const _ as *mut c_void,
retain: None,
release: None,
copy_description: None,
};
let cf_paths =
cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks);
assert!(!cf_paths.is_null());
for path in paths {
let path_bytes = path.as_os_str().as_bytes();
let cf_url = cf::CFURLCreateFromFileSystemRepresentation(
cf::kCFAllocatorDefault,
path_bytes.as_ptr() as *const i8,
path_bytes.len() as cf::CFIndex,
false,
);
let cf_path = cf::CFURLCopyFileSystemPath(cf_url, cf::kCFURLPOSIXPathStyle);
cf::CFArrayAppendValue(cf_paths, cf_path);
cf::CFRelease(cf_path);
cf::CFRelease(cf_url);
}
let stream = fs::FSEventStreamCreate(
cf::kCFAllocatorDefault,
Self::trampoline,
&stream_context,
cf_paths,
FSEventsGetCurrentEventId(),
latency.as_secs_f64(),
fs::kFSEventStreamCreateFlagFileEvents
| fs::kFSEventStreamCreateFlagNoDefer
| fs::kFSEventStreamCreateFlagWatchRoot,
);
cf::CFRelease(cf_paths);
let state = Arc::new(Mutex::new(Lifecycle::New));
(
EventStream {
stream,
state: state.clone(),
callback,
},
Handle(state),
)
}
}
pub fn run<F>(mut self, f: F)
where
F: FnMut(Vec<Event>) -> bool + 'static,
{
*self.callback = Some(Box::new(f));
unsafe {
let run_loop = cf::CFRunLoopGetCurrent();
{
let mut state = self.state.lock();
match *state {
Lifecycle::New => *state = Lifecycle::Running(run_loop),
Lifecycle::Running(_) => unreachable!(),
Lifecycle::Stopped => return,
}
}
fs::FSEventStreamScheduleWithRunLoop(self.stream, run_loop, cf::kCFRunLoopDefaultMode);
fs::FSEventStreamStart(self.stream);
cf::CFRunLoopRun();
fs::FSEventStreamRelease(self.stream);
}
}
extern "C" fn trampoline(
stream_ref: fs::FSEventStreamRef,
info: *mut ::std::os::raw::c_void,
num: usize, // size_t numEvents
event_paths: *mut ::std::os::raw::c_void, // void *eventPaths
event_flags: *const ::std::os::raw::c_void, // const FSEventStreamEventFlags eventFlags[]
event_ids: *const ::std::os::raw::c_void, // const FSEventStreamEventId eventIds[]
) {
unsafe {
let event_paths = event_paths as *const *const ::std::os::raw::c_char;
let e_ptr = event_flags as *mut u32;
let i_ptr = event_ids as *mut u64;
let callback_ptr = (info as *mut Option<RunCallback>).as_mut().unwrap();
let callback = if let Some(callback) = callback_ptr.as_mut() {
callback
} else {
return;
};
let paths = slice::from_raw_parts(event_paths, num);
let flags = slice::from_raw_parts_mut(e_ptr, num);
let ids = slice::from_raw_parts_mut(i_ptr, num);
let mut events = Vec::with_capacity(num);
for p in 0..num {
let path_c_str = CStr::from_ptr(paths[p]);
let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes()));
if let Some(flag) = StreamFlags::from_bits(flags[p]) {
if flag.contains(StreamFlags::HISTORY_DONE) {
events.clear();
} else {
events.push(Event {
event_id: ids[p],
flags: flag,
path,
});
}
} else {
debug_assert!(false, "unknown flag set for fs event: {}", flags[p]);
}
}
if !events.is_empty() {
if !callback(events) {
fs::FSEventStreamStop(stream_ref);
cf::CFRunLoopStop(cf::CFRunLoopGetCurrent());
}
}
}
}
}
impl Drop for Handle {
fn drop(&mut self) {
let mut state = self.0.lock();
if let Lifecycle::Running(run_loop) = *state {
unsafe {
cf::CFRunLoopStop(run_loop);
}
}
*state = Lifecycle::Stopped;
}
}
// Synchronize with
// /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Headers/FSEvents.h
bitflags! {
#[repr(C)]
pub struct StreamFlags: u32 {
const NONE = 0x00000000;
const MUST_SCAN_SUBDIRS = 0x00000001;
const USER_DROPPED = 0x00000002;
const KERNEL_DROPPED = 0x00000004;
const IDS_WRAPPED = 0x00000008;
const HISTORY_DONE = 0x00000010;
const ROOT_CHANGED = 0x00000020;
const MOUNT = 0x00000040;
const UNMOUNT = 0x00000080;
const ITEM_CREATED = 0x00000100;
const ITEM_REMOVED = 0x00000200;
const INODE_META_MOD = 0x00000400;
const ITEM_RENAMED = 0x00000800;
const ITEM_MODIFIED = 0x00001000;
const FINDER_INFO_MOD = 0x00002000;
const ITEM_CHANGE_OWNER = 0x00004000;
const ITEM_XATTR_MOD = 0x00008000;
const IS_FILE = 0x00010000;
const IS_DIR = 0x00020000;
const IS_SYMLINK = 0x00040000;
const OWN_EVENT = 0x00080000;
const IS_HARDLINK = 0x00100000;
const IS_LAST_HARDLINK = 0x00200000;
const ITEM_CLONED = 0x400000;
}
}
impl std::fmt::Display for StreamFlags {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.contains(StreamFlags::MUST_SCAN_SUBDIRS) {
let _d = write!(f, "MUST_SCAN_SUBDIRS ");
}
if self.contains(StreamFlags::USER_DROPPED) {
let _d = write!(f, "USER_DROPPED ");
}
if self.contains(StreamFlags::KERNEL_DROPPED) {
let _d = write!(f, "KERNEL_DROPPED ");
}
if self.contains(StreamFlags::IDS_WRAPPED) {
let _d = write!(f, "IDS_WRAPPED ");
}
if self.contains(StreamFlags::HISTORY_DONE) {
let _d = write!(f, "HISTORY_DONE ");
}
if self.contains(StreamFlags::ROOT_CHANGED) {
let _d = write!(f, "ROOT_CHANGED ");
}
if self.contains(StreamFlags::MOUNT) {
let _d = write!(f, "MOUNT ");
}
if self.contains(StreamFlags::UNMOUNT) {
let _d = write!(f, "UNMOUNT ");
}
if self.contains(StreamFlags::ITEM_CREATED) {
let _d = write!(f, "ITEM_CREATED ");
}
if self.contains(StreamFlags::ITEM_REMOVED) {
let _d = write!(f, "ITEM_REMOVED ");
}
if self.contains(StreamFlags::INODE_META_MOD) {
let _d = write!(f, "INODE_META_MOD ");
}
if self.contains(StreamFlags::ITEM_RENAMED) {
let _d = write!(f, "ITEM_RENAMED ");
}
if self.contains(StreamFlags::ITEM_MODIFIED) {
let _d = write!(f, "ITEM_MODIFIED ");
}
if self.contains(StreamFlags::FINDER_INFO_MOD) {
let _d = write!(f, "FINDER_INFO_MOD ");
}
if self.contains(StreamFlags::ITEM_CHANGE_OWNER) {
let _d = write!(f, "ITEM_CHANGE_OWNER ");
}
if self.contains(StreamFlags::ITEM_XATTR_MOD) {
let _d = write!(f, "ITEM_XATTR_MOD ");
}
if self.contains(StreamFlags::IS_FILE) {
let _d = write!(f, "IS_FILE ");
}
if self.contains(StreamFlags::IS_DIR) {
let _d = write!(f, "IS_DIR ");
}
if self.contains(StreamFlags::IS_SYMLINK) {
let _d = write!(f, "IS_SYMLINK ");
}
if self.contains(StreamFlags::OWN_EVENT) {
let _d = write!(f, "OWN_EVENT ");
}
if self.contains(StreamFlags::IS_LAST_HARDLINK) {
let _d = write!(f, "IS_LAST_HARDLINK ");
}
if self.contains(StreamFlags::IS_HARDLINK) {
let _d = write!(f, "IS_HARDLINK ");
}
if self.contains(StreamFlags::ITEM_CLONED) {
let _d = write!(f, "ITEM_CLONED ");
}
write!(f, "")
}
}
#[link(name = "CoreServices", kind = "framework")]
extern "C" {
pub fn FSEventsGetCurrentEventId() -> u64;
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs, sync::mpsc, thread, time::Duration};
use tempdir::TempDir;
#[test]
fn test_event_stream_simple() {
for _ in 0..3 {
let dir = TempDir::new("test-event-stream").unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
}
flush_historical_events();
let (tx, rx) = mpsc::channel();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
thread::spawn(move || stream.run(move |events| tx.send(events.to_vec()).is_ok()));
fs::write(path.join("new-file"), "").unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("new-file"));
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
fs::remove_file(path.join("existing-file-5")).unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("existing-file-5"));
assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
drop(handle);
}
}
#[test]
fn test_event_stream_delayed_start() {
for _ in 0..3 {
let dir = TempDir::new("test-event-stream").unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
}
flush_historical_events();
let (tx, rx) = mpsc::channel();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
// Delay the call to `run` in order to make sure we don't miss any events that occur
// between creating the `EventStream` and calling `run`.
thread::spawn(move || {
thread::sleep(Duration::from_millis(100));
stream.run(move |events| tx.send(events.to_vec()).is_ok())
});
fs::write(path.join("new-file"), "").unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("new-file"));
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
fs::remove_file(path.join("existing-file-5")).unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("existing-file-5"));
assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
drop(handle);
}
}
#[test]
fn test_event_stream_shutdown_by_dropping_handle() {
let dir = TempDir::new("test-event-stream").unwrap();
let path = dir.path().canonicalize().unwrap();
flush_historical_events();
let (tx, rx) = mpsc::channel();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
thread::spawn(move || {
stream.run({
let tx = tx.clone();
move |_| {
tx.send("running").unwrap();
true
}
});
tx.send("stopped").unwrap();
});
fs::write(path.join("new-file"), "").unwrap();
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "running");
// Dropping the handle causes `EventStream::run` to return.
drop(handle);
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "stopped");
}
#[test]
fn test_event_stream_shutdown_before_run() {
let dir = TempDir::new("test-event-stream").unwrap();
let path = dir.path().canonicalize().unwrap();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
drop(handle);
// This returns immediately because the handle was already dropped.
stream.run(|_| true);
}
fn flush_historical_events() {
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
};
thread::sleep(duration);
}
}

6
crates/fuzzy/Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[package]
name = "fuzzy"
version = "0.1.0"
edition = "2018"
[dependencies]

View file

@ -0,0 +1,62 @@
use std::iter::FromIterator;
#[derive(Copy, Clone, Debug, Default)]
pub struct CharBag(u64);
impl CharBag {
pub fn is_superset(self, other: CharBag) -> bool {
self.0 & other.0 == other.0
}
fn insert(&mut self, c: char) {
if c >= 'a' && c <= 'z' {
let mut count = self.0;
let idx = c as u8 - 'a' as u8;
count = count >> (idx * 2);
count = ((count << 1) | 1) & 3;
count = count << idx * 2;
self.0 |= count;
} else if c >= '0' && c <= '9' {
let idx = c as u8 - '0' as u8;
self.0 |= 1 << (idx + 52);
} else if c == '-' {
self.0 |= 1 << 62;
}
}
}
impl Extend<char> for CharBag {
fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
for c in iter {
self.insert(c);
}
}
}
impl FromIterator<char> for CharBag {
fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
let mut result = Self::default();
result.extend(iter);
result
}
}
impl From<&str> for CharBag {
fn from(s: &str) -> Self {
let mut bag = Self(0);
for c in s.chars() {
bag.insert(c);
}
bag
}
}
impl From<&[char]> for CharBag {
fn from(chars: &[char]) -> Self {
let mut bag = Self(0);
for c in chars {
bag.insert(*c);
}
bag
}
}

614
crates/fuzzy/src/lib.rs Normal file
View file

@ -0,0 +1,614 @@
mod char_bag;
use std::{
borrow::Cow,
cmp::Ordering,
path::Path,
sync::atomic::{self, AtomicBool},
sync::Arc,
};
pub use char_bag::CharBag;
const BASE_DISTANCE_PENALTY: f64 = 0.6;
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
const MIN_DISTANCE_PENALTY: f64 = 0.2;
pub struct Matcher<'a> {
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
min_score: f64,
match_positions: Vec<usize>,
last_positions: Vec<usize>,
score_matrix: Vec<Option<f64>>,
best_position_matrix: Vec<usize>,
}
trait Match: Ord {
fn score(&self) -> f64;
fn set_positions(&mut self, positions: Vec<usize>);
}
trait MatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool;
fn to_string<'a>(&'a self) -> Cow<'a, str>;
}
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub path: &'a Arc<Path>,
pub char_bag: CharBag,
}
#[derive(Clone, Debug)]
pub struct PathMatch {
pub score: f64,
pub positions: Vec<usize>,
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
}
#[derive(Clone, Debug)]
pub struct StringMatchCandidate {
pub string: String,
pub char_bag: CharBag,
}
impl Match for PathMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.path.to_string_lossy()
}
}
impl<'a> MatchCandidate for &'a StringMatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.string.as_str().into()
}
}
#[derive(Clone, Debug)]
pub struct StringMatch {
pub score: f64,
pub positions: Vec<usize>,
pub string: String,
}
impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool {
self.score.eq(&other.score)
}
}
impl Eq for StringMatch {}
impl PartialOrd for StringMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StringMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.string.cmp(&other.string))
}
}
impl PartialEq for PathMatch {
fn eq(&self, other: &Self) -> bool {
self.score.eq(&other.score)
}
}
impl Eq for PathMatch {}
impl PartialOrd for PathMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
.then_with(|| Arc::as_ptr(&self.path).cmp(&Arc::as_ptr(&other.path)))
}
}
impl<'a> Matcher<'a> {
pub fn new(
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
) -> Self {
Self {
query,
lowercase_query,
query_char_bag,
min_score: 0.0,
last_positions: vec![0; query.len()],
match_positions: vec![0; query.len()],
score_matrix: Vec::new(),
best_position_matrix: Vec::new(),
smart_case,
max_results,
}
}
pub fn match_strings(
&mut self,
candidates: &[StringMatchCandidate],
results: &mut Vec<StringMatch>,
cancel_flag: &AtomicBool,
) {
self.match_internal(
&[],
&[],
candidates.iter(),
results,
cancel_flag,
|candidate, score| StringMatch {
score,
positions: Vec::new(),
string: candidate.string.to_string(),
},
)
}
pub fn match_paths(
&mut self,
tree_id: usize,
path_prefix: Arc<str>,
path_entries: impl Iterator<Item = PathMatchCandidate<'a>>,
results: &mut Vec<PathMatch>,
cancel_flag: &AtomicBool,
) {
let prefix = path_prefix.chars().collect::<Vec<_>>();
let lowercase_prefix = prefix
.iter()
.map(|c| c.to_ascii_lowercase())
.collect::<Vec<_>>();
self.match_internal(
&prefix,
&lowercase_prefix,
path_entries,
results,
cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id: tree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: path_prefix.clone(),
},
)
}
fn match_internal<C: MatchCandidate, R, F>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = C>,
results: &mut Vec<R>,
cancel_flag: &AtomicBool,
build_match: F,
) where
R: Match,
F: Fn(&C, f64) -> R,
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
for candidate in candidates {
if !candidate.has_chars(self.query_char_bag) {
continue;
}
if cancel_flag.load(atomic::Ordering::Relaxed) {
break;
}
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.to_string().chars() {
candidate_chars.push(c);
lowercase_candidate_chars.push(c.to_ascii_lowercase());
}
if !self.find_last_positions(&lowercase_prefix, &lowercase_candidate_chars) {
continue;
}
let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
self.score_matrix.clear();
self.score_matrix.resize(matrix_len, None);
self.best_position_matrix.clear();
self.best_position_matrix.resize(matrix_len, 0);
let score = self.score_match(
&candidate_chars,
&lowercase_candidate_chars,
&prefix,
&lowercase_prefix,
);
if score > 0.0 {
let mut mat = build_match(&candidate, score);
if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) {
if results.len() < self.max_results {
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
} else if i < results.len() {
results.pop();
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
}
if results.len() == self.max_results {
self.min_score = results.last().unwrap().score();
}
}
}
}
}
fn find_last_positions(&mut self, prefix: &[char], path: &[char]) -> bool {
let mut path = path.iter();
let mut prefix_iter = prefix.iter();
for (i, char) in self.query.iter().enumerate().rev() {
if let Some(j) = path.rposition(|c| c == char) {
self.last_positions[i] = j + prefix.len();
} else if let Some(j) = prefix_iter.rposition(|c| c == char) {
self.last_positions[i] = j;
} else {
return false;
}
}
true
}
fn score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
) -> f64 {
let score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
0,
0,
self.query.len() as f64,
) * self.query.len() as f64;
if score <= 0.0 {
return 0.0;
}
let path_len = prefix.len() + path.len();
let mut cur_start = 0;
let mut byte_ix = 0;
let mut char_ix = 0;
for i in 0..self.query.len() {
let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
while char_ix < match_char_ix {
let ch = prefix
.get(char_ix)
.or_else(|| path.get(char_ix - prefix.len()))
.unwrap();
byte_ix += ch.len_utf8();
char_ix += 1;
}
cur_start = match_char_ix + 1;
self.match_positions[i] = byte_ix;
}
score
}
fn recursive_score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
query_idx: usize,
path_idx: usize,
cur_score: f64,
) -> f64 {
if query_idx == self.query.len() {
return 1.0;
}
let path_len = prefix.len() + path.len();
if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
return memoized;
}
let mut score = 0.0;
let mut best_position = 0;
let query_char = self.lowercase_query[query_idx];
let limit = self.last_positions[query_idx];
let mut last_slash = 0;
for j in path_idx..=limit {
let path_char = if j < prefix.len() {
lowercase_prefix[j]
} else {
path_cased[j - prefix.len()]
};
let is_path_sep = path_char == '/' || path_char == '\\';
if query_idx == 0 && is_path_sep {
last_slash = j;
}
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
let curr = if j < prefix.len() {
prefix[j]
} else {
path[j - prefix.len()]
};
let mut char_score = 1.0;
if j > path_idx {
let last = if j - 1 < prefix.len() {
prefix[j - 1]
} else {
path[j - 1 - prefix.len()]
};
if last == '/' {
char_score = 0.9;
} else if last == '-' || last == '_' || last == ' ' || last.is_numeric() {
char_score = 0.8;
} else if last.is_lowercase() && curr.is_uppercase() {
char_score = 0.8;
} else if last == '.' {
char_score = 0.7;
} else if query_idx == 0 {
char_score = BASE_DISTANCE_PENALTY;
} else {
char_score = MIN_DISTANCE_PENALTY.max(
BASE_DISTANCE_PENALTY
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
);
}
}
// Apply a severe penalty if the case doesn't match.
// This will make the exact matches have higher score than the case-insensitive and the
// path insensitive matches.
if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
char_score *= 0.001;
}
let mut multiplier = char_score;
// Scale the score based on how deep within the path we found the match.
if query_idx == 0 {
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
}
let mut next_score = 1.0;
if self.min_score > 0.0 {
next_score = cur_score * multiplier;
// Scores only decrease. If we can't pass the previous best, bail
if next_score < self.min_score {
// Ensure that score is non-zero so we use it in the memo table.
if score == 0.0 {
score = 1e-18;
}
continue;
}
}
let new_score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
query_idx + 1,
j + 1,
next_score,
) * multiplier;
if new_score > score {
score = new_score;
best_position = j;
// Optimization: can't score better than 1.
if new_score == 1.0 {
break;
}
}
}
}
if best_position != 0 {
self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
}
self.score_matrix[query_idx * path_len + path_idx] = Some(score);
score
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_get_last_positions() {
let mut query: &[char] = &['d', 'c'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert_eq!(result, false);
query = &['c', 'd'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert_eq!(result, true);
assert_eq!(matcher.last_positions, vec![2, 4]);
query = &['z', '/', 'z', 'f'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
assert_eq!(result, true);
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
}
#[test]
fn test_match_path_entries() {
let paths = vec![
"",
"a",
"ab",
"abC",
"abcd",
"alphabravocharlie",
"AlphaBravoCharlie",
"thisisatestdir",
"/////ThisIsATestDir",
"/this/is/a/test/dir",
"/test/tiatd",
];
assert_eq!(
match_query("abc", false, &paths),
vec![
("abC", vec![0, 1, 2]),
("abcd", vec![0, 1, 2]),
("AlphaBravoCharlie", vec![0, 5, 10]),
("alphabravocharlie", vec![4, 5, 10]),
]
);
assert_eq!(
match_query("t/i/a/t/d", false, &paths),
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
);
assert_eq!(
match_query("tiatd", false, &paths),
vec![
("/test/tiatd", vec![6, 7, 8, 9, 10]),
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
("thisisatestdir", vec![0, 2, 6, 7, 11]),
]
);
}
#[test]
fn test_match_multibyte_path_entries() {
let paths = vec!["aαbβ/cγ", "αβγδ/bcde", "c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", "/d/🆒/h"];
assert_eq!("1".len(), 7);
assert_eq!(
match_query("bcd", false, &paths),
vec![
("αβγδ/bcde", vec![9, 10, 11]),
("aαbβ/cγ", vec![3, 7, 10]),
]
);
assert_eq!(
match_query("cde", false, &paths),
vec![
("αβγδ/bcde", vec![10, 11, 12]),
("c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", vec![0, 23, 46]),
]
);
}
fn match_query<'a>(
query: &str,
smart_case: bool,
paths: &Vec<&'a str>,
) -> Vec<(&'a str, Vec<usize>)> {
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let query_chars = CharBag::from(&lowercase_query[..]);
let path_arcs = paths
.iter()
.map(|path| Arc::from(PathBuf::from(path)))
.collect::<Vec<_>>();
let mut path_entries = Vec::new();
for (i, path) in paths.iter().enumerate() {
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
let char_bag = CharBag::from(lowercase_path.as_slice());
path_entries.push(PathMatchCandidate {
char_bag,
path: path_arcs.get(i).unwrap(),
});
}
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
let cancel_flag = AtomicBool::new(false);
let mut results = Vec::new();
matcher.match_paths(
0,
"".into(),
path_entries.into_iter(),
&mut results,
&cancel_flag,
);
results
.into_iter()
.map(|result| {
(
paths
.iter()
.copied()
.find(|p| result.path.as_ref() == Path::new(p))
.unwrap(),
result.positions,
)
})
.collect()
}
}

60
crates/gpui/Cargo.toml Normal file
View file

@ -0,0 +1,60 @@
[package]
authors = ["Nathan Sobo <nathansobo@gmail.com>"]
edition = "2018"
name = "gpui"
version = "0.1.0"
[features]
test-support = []
[dependencies]
async-task = "4.0.3"
backtrace = "0.3"
ctor = "0.1"
etagere = "0.2"
gpui_macros = { path = "../gpui_macros" }
image = "0.23"
lazy_static = "1.4.0"
log = "0.4"
num_cpus = "1.13"
ordered-float = "2.1.1"
parking = "2.0.0"
parking_lot = "0.11.1"
pathfinder_color = "0.5"
pathfinder_geometry = "0.5"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8.3"
resvg = "0.14"
seahash = "4.1"
serde = { version = "1.0.125", features = ["derive"] }
serde_json = "1.0.64"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
sum_tree = { path = "../sum_tree" }
time = { version = "0.3" }
tiny-skia = "0.5"
tree-sitter = "0.19"
usvg = "0.14"
waker-fn = "1.1.0"
[build-dependencies]
bindgen = "0.58.1"
cc = "1.0.67"
[dev-dependencies]
env_logger = "0.8"
png = "0.16"
simplelog = "0.9"
[target.'cfg(target_os = "macos")'.dependencies]
anyhow = "1"
block = "0.1"
cocoa = "0.24"
core-foundation = "0.9"
core-graphics = "0.22.2"
core-text = "19.2"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" }
foreign-types = "0.3"
log = "0.4"
metal = "0.21.0"
objc = "0.2"

108
crates/gpui/build.rs Normal file
View file

@ -0,0 +1,108 @@
use std::{
env,
path::PathBuf,
process::{self, Command},
};
fn main() {
generate_dispatch_bindings();
compile_context_predicate_parser();
compile_metal_shaders();
generate_shader_bindings();
}
fn generate_dispatch_bindings() {
println!("cargo:rustc-link-lib=framework=System");
println!("cargo:rerun-if-changed=src/platform/mac/dispatch.h");
let bindings = bindgen::Builder::default()
.header("src/platform/mac/dispatch.h")
.allowlist_var("_dispatch_main_q")
.allowlist_function("dispatch_async_f")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.layout_tests(false)
.generate()
.expect("unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("dispatch_sys.rs"))
.expect("couldn't write dispatch bindings");
}
fn compile_context_predicate_parser() {
let dir = PathBuf::from("./grammars/context-predicate/src");
let parser_c = dir.join("parser.c");
println!("cargo:rerun-if-changed={}", &parser_c.to_str().unwrap());
cc::Build::new()
.include(&dir)
.file(parser_c)
.compile("tree_sitter_context_predicate");
}
const SHADER_HEADER_PATH: &'static str = "./src/platform/mac/shaders/shaders.h";
fn compile_metal_shaders() {
let shader_path = "./src/platform/mac/shaders/shaders.metal";
let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
let metallib_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
println!("cargo:rerun-if-changed={}", SHADER_HEADER_PATH);
println!("cargo:rerun-if-changed={}", shader_path);
let output = Command::new("xcrun")
.args(&[
"-sdk",
"macosx",
"metal",
"-gline-tables-only",
"-mmacosx-version-min=10.14",
"-MO",
"-c",
shader_path,
"-o",
])
.arg(&air_output_path)
.output()
.unwrap();
if !output.status.success() {
eprintln!(
"metal shader compilation failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
process::exit(1);
}
let output = Command::new("xcrun")
.args(&["-sdk", "macosx", "metallib"])
.arg(air_output_path)
.arg("-o")
.arg(metallib_output_path)
.output()
.unwrap();
if !output.status.success() {
eprintln!(
"metallib compilation failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
process::exit(1);
}
}
fn generate_shader_bindings() {
let bindings = bindgen::Builder::default()
.header(SHADER_HEADER_PATH)
.allowlist_type("GPUI.*")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.layout_tests(false)
.generate()
.expect("unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("shaders.rs"))
.expect("couldn't write shader bindings");
}

View file

@ -0,0 +1,123 @@
use gpui::{
color::Color,
fonts::{Properties, Weight},
text_layout::RunStyle,
DebugContext, Element as _, Quad,
};
use log::LevelFilter;
use pathfinder_geometry::rect::RectF;
use simplelog::SimpleLogger;
fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
gpui::App::new(()).unwrap().run(|cx| {
cx.platform().activate(true);
cx.add_window(Default::default(), |_| TextView);
});
}
struct TextView;
struct TextElement;
impl gpui::Entity for TextView {
type Event = ();
}
impl gpui::View for TextView {
fn ui_name() -> &'static str {
"View"
}
fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
TextElement.boxed()
}
}
impl gpui::Element for TextElement {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
_: &mut gpui::LayoutContext,
) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) {
(constraint.max, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let font_size = 12.;
let family = cx.font_cache.load_family(&["SF Pro Display"]).unwrap();
let normal = RunStyle {
font_id: cx
.font_cache
.select_font(family, &Default::default())
.unwrap(),
color: Color::default(),
underline: false,
};
let bold = RunStyle {
font_id: cx
.font_cache
.select_font(
family,
&Properties {
weight: Weight::BOLD,
..Default::default()
},
)
.unwrap(),
color: Color::default(),
underline: false,
};
let text = "Hello world!";
let line = cx.text_layout_cache.layout_str(
text,
font_size,
&[
(1, normal.clone()),
(1, bold.clone()),
(1, normal.clone()),
(1, bold.clone()),
(text.len() - 4, normal.clone()),
],
);
cx.scene.push_quad(Quad {
bounds,
background: Some(Color::white()),
..Default::default()
});
line.paint(bounds.origin(), visible_bounds, bounds.height(), cx);
}
fn dispatch_event(
&mut self,
_: &gpui::Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut gpui::EventContext,
) -> bool {
false
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> gpui::json::Value {
todo!()
}
}

View file

@ -0,0 +1,2 @@
/node_modules
/build

View file

@ -0,0 +1,20 @@
[package]
name = "tree-sitter-context-predicate"
description = "context-predicate grammar for the tree-sitter parsing library"
version = "0.0.1"
keywords = ["incremental", "parsing", "context-predicate"]
categories = ["parsing", "text-editors"]
repository = "https://github.com/tree-sitter/tree-sitter-javascript"
edition = "2018"
license = "MIT"
build = "bindings/rust/build.rs"
include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"]
[lib]
path = "bindings/rust/lib.rs"
[dependencies]
tree-sitter = "0.19.3"
[build-dependencies]
cc = "1.0"

View file

@ -0,0 +1,18 @@
{
"targets": [
{
"target_name": "tree_sitter_context_predicate_binding",
"include_dirs": [
"<!(node -e \"require('nan')\")",
"src"
],
"sources": [
"src/parser.c",
"bindings/node/binding.cc"
],
"cflags_c": [
"-std=c99",
]
}
]
}

View file

@ -0,0 +1,28 @@
#include "tree_sitter/parser.h"
#include <node.h>
#include "nan.h"
using namespace v8;
extern "C" TSLanguage * tree_sitter_context_predicate();
namespace {
NAN_METHOD(New) {}
void Init(Local<Object> exports, Local<Object> module) {
Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
tpl->SetClassName(Nan::New("Language").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked();
Local<Object> instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
Nan::SetInternalFieldPointer(instance, 0, tree_sitter_context_predicate());
Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("context_predicate").ToLocalChecked());
Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance);
}
NODE_MODULE(tree_sitter_context_predicate_binding, Init)
} // namespace

View file

@ -0,0 +1,19 @@
try {
module.exports = require("../../build/Release/tree_sitter_context_predicate_binding");
} catch (error1) {
if (error1.code !== 'MODULE_NOT_FOUND') {
throw error1;
}
try {
module.exports = require("../../build/Debug/tree_sitter_context_predicate_binding");
} catch (error2) {
if (error2.code !== 'MODULE_NOT_FOUND') {
throw error2;
}
throw error1
}
}
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

View file

@ -0,0 +1,40 @@
fn main() {
let src_dir = std::path::Path::new("src");
let mut c_config = cc::Build::new();
c_config.include(&src_dir);
c_config
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable")
.flag_if_supported("-Wno-trigraphs");
let parser_path = src_dir.join("parser.c");
c_config.file(&parser_path);
// If your language uses an external scanner written in C,
// then include this block of code:
/*
let scanner_path = src_dir.join("scanner.c");
c_config.file(&scanner_path);
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
*/
c_config.compile("parser");
println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
// If your language uses an external scanner written in C++,
// then include this block of code:
/*
let mut cpp_config = cc::Build::new();
cpp_config.cpp(true);
cpp_config.include(&src_dir);
cpp_config
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable");
let scanner_path = src_dir.join("scanner.cc");
cpp_config.file(&scanner_path);
cpp_config.compile("scanner");
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
*/
}

View file

@ -0,0 +1,52 @@
//! This crate provides context_predicate language support for the [tree-sitter][] parsing library.
//!
//! Typically, you will use the [language][language func] function to add this language to a
//! tree-sitter [Parser][], and then use the parser to parse some code:
//!
//! ```
//! let code = "";
//! let mut parser = tree_sitter::Parser::new();
//! parser.set_language(tree_sitter_context_predicate::language()).expect("Error loading context_predicate grammar");
//! let tree = parser.parse(code, None).unwrap();
//! ```
//!
//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
//! [language func]: fn.language.html
//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html
//! [tree-sitter]: https://tree-sitter.github.io/
use tree_sitter::Language;
extern "C" {
fn tree_sitter_context_predicate() -> Language;
}
/// Get the tree-sitter [Language][] for this grammar.
///
/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
pub fn language() -> Language {
unsafe { tree_sitter_context_predicate() }
}
/// The content of the [`node-types.json`][] file for this grammar.
///
/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types
pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json");
// Uncomment these to include any queries that this grammar contains
// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm");
// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm");
// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm");
// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm");
#[cfg(test)]
mod tests {
#[test]
fn test_can_load_grammar() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(super::language())
.expect("Error loading context_predicate language");
}
}

View file

@ -0,0 +1,49 @@
==================
Identifiers
==================
abc12
---
(source (identifier))
==================
Negation
==================
!abc
---
(source (not (identifier)))
==================
And/Or
==================
a || b && c && d
---
(source
(or
(identifier)
(and
(and (identifier) (identifier))
(identifier))))
==================
Expressions
==================
a && (b == c || d != e)
---
(source
(and
(identifier)
(parenthesized (or
(equal (identifier) (identifier))
(not_equal (identifier) (identifier))))))

View file

@ -0,0 +1,31 @@
module.exports = grammar({
name: 'context_predicate',
rules: {
source: $ => $._expression,
_expression: $ => choice(
$.identifier,
$.not,
$.and,
$.or,
$.equal,
$.not_equal,
$.parenthesized,
),
identifier: $ => /[A-Za-z0-9_-]+/,
not: $ => prec(3, seq("!", field("expression", $._expression))),
and: $ => prec.left(2, seq(field("left", $._expression), "&&", field("right", $._expression))),
or: $ => prec.left(1, seq(field("left", $._expression), "||", field("right", $._expression))),
equal: $ => seq(field("left", $.identifier), "==", field("right", $.identifier)),
not_equal: $ => seq(field("left", $.identifier), "!=", field("right", $.identifier)),
parenthesized: $ => seq("(", field("expression", $._expression), ")"),
}
});

View file

@ -0,0 +1,44 @@
{
"name": "tree-sitter-context-predicate",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "tree-sitter-context-predicate",
"dependencies": {
"nan": "^2.14.0"
},
"devDependencies": {
"tree-sitter-cli": "^0.19.5"
}
},
"node_modules/nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"node_modules/tree-sitter-cli": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.19.5.tgz",
"integrity": "sha512-kRzKrUAwpDN9AjA3b0tPBwT1hd8N2oQvvvHup2OEsX6mdsSMLmAvR+NSqK9fe05JrRbVvG8mbteNUQsxlMQohQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
"tree-sitter": "cli.js"
}
}
},
"dependencies": {
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"tree-sitter-cli": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.19.5.tgz",
"integrity": "sha512-kRzKrUAwpDN9AjA3b0tPBwT1hd8N2oQvvvHup2OEsX6mdsSMLmAvR+NSqK9fe05JrRbVvG8mbteNUQsxlMQohQ==",
"dev": true
}
}
}

View file

@ -0,0 +1,10 @@
{
"name": "tree-sitter-context-predicate",
"main": "bindings/node",
"devDependencies": {
"tree-sitter-cli": "^0.19.5"
},
"dependencies": {
"nan": "^2.14.0"
}
}

View file

@ -0,0 +1,208 @@
{
"name": "context_predicate",
"rules": {
"source": {
"type": "SYMBOL",
"name": "_expression"
},
"_expression": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "not"
},
{
"type": "SYMBOL",
"name": "and"
},
{
"type": "SYMBOL",
"name": "or"
},
{
"type": "SYMBOL",
"name": "equal"
},
{
"type": "SYMBOL",
"name": "not_equal"
},
{
"type": "SYMBOL",
"name": "parenthesized"
}
]
},
"identifier": {
"type": "PATTERN",
"value": "[A-Za-z0-9_-]+"
},
"not": {
"type": "PREC",
"value": 3,
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "!"
},
{
"type": "FIELD",
"name": "expression",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"and": {
"type": "PREC_LEFT",
"value": 2,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "&&"
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"or": {
"type": "PREC_LEFT",
"value": 1,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "||"
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"equal": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
},
{
"type": "STRING",
"value": "=="
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
]
},
"not_equal": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
},
{
"type": "STRING",
"value": "!="
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
]
},
"parenthesized": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "("
},
{
"type": "FIELD",
"name": "expression",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": ")"
}
]
}
},
"extras": [
{
"type": "PATTERN",
"value": "\\s"
}
],
"conflicts": [],
"precedences": [],
"externals": [],
"inline": [],
"supertypes": []
}

View file

@ -0,0 +1,353 @@
[
{
"type": "and",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "equal",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{
"type": "not",
"named": true,
"fields": {
"expression": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "not_equal",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{
"type": "or",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "parenthesized",
"named": true,
"fields": {
"expression": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "source",
"named": true,
"fields": {},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
},
{
"type": "!",
"named": false
},
{
"type": "!=",
"named": false
},
{
"type": "&&",
"named": false
},
{
"type": "(",
"named": false
},
{
"type": ")",
"named": false
},
{
"type": "==",
"named": false
},
{
"type": "identifier",
"named": true
},
{
"type": "||",
"named": false
}
]

View file

@ -0,0 +1,529 @@
#include <tree_sitter/parser.h>
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#endif
#define LANGUAGE_VERSION 13
#define STATE_COUNT 18
#define LARGE_STATE_COUNT 6
#define SYMBOL_COUNT 17
#define ALIAS_COUNT 0
#define TOKEN_COUNT 9
#define EXTERNAL_TOKEN_COUNT 0
#define FIELD_COUNT 3
#define MAX_ALIAS_SEQUENCE_LENGTH 3
#define PRODUCTION_ID_COUNT 3
enum {
sym_identifier = 1,
anon_sym_BANG = 2,
anon_sym_AMP_AMP = 3,
anon_sym_PIPE_PIPE = 4,
anon_sym_EQ_EQ = 5,
anon_sym_BANG_EQ = 6,
anon_sym_LPAREN = 7,
anon_sym_RPAREN = 8,
sym_source = 9,
sym__expression = 10,
sym_not = 11,
sym_and = 12,
sym_or = 13,
sym_equal = 14,
sym_not_equal = 15,
sym_parenthesized = 16,
};
static const char * const ts_symbol_names[] = {
[ts_builtin_sym_end] = "end",
[sym_identifier] = "identifier",
[anon_sym_BANG] = "!",
[anon_sym_AMP_AMP] = "&&",
[anon_sym_PIPE_PIPE] = "||",
[anon_sym_EQ_EQ] = "==",
[anon_sym_BANG_EQ] = "!=",
[anon_sym_LPAREN] = "(",
[anon_sym_RPAREN] = ")",
[sym_source] = "source",
[sym__expression] = "_expression",
[sym_not] = "not",
[sym_and] = "and",
[sym_or] = "or",
[sym_equal] = "equal",
[sym_not_equal] = "not_equal",
[sym_parenthesized] = "parenthesized",
};
static const TSSymbol ts_symbol_map[] = {
[ts_builtin_sym_end] = ts_builtin_sym_end,
[sym_identifier] = sym_identifier,
[anon_sym_BANG] = anon_sym_BANG,
[anon_sym_AMP_AMP] = anon_sym_AMP_AMP,
[anon_sym_PIPE_PIPE] = anon_sym_PIPE_PIPE,
[anon_sym_EQ_EQ] = anon_sym_EQ_EQ,
[anon_sym_BANG_EQ] = anon_sym_BANG_EQ,
[anon_sym_LPAREN] = anon_sym_LPAREN,
[anon_sym_RPAREN] = anon_sym_RPAREN,
[sym_source] = sym_source,
[sym__expression] = sym__expression,
[sym_not] = sym_not,
[sym_and] = sym_and,
[sym_or] = sym_or,
[sym_equal] = sym_equal,
[sym_not_equal] = sym_not_equal,
[sym_parenthesized] = sym_parenthesized,
};
static const TSSymbolMetadata ts_symbol_metadata[] = {
[ts_builtin_sym_end] = {
.visible = false,
.named = true,
},
[sym_identifier] = {
.visible = true,
.named = true,
},
[anon_sym_BANG] = {
.visible = true,
.named = false,
},
[anon_sym_AMP_AMP] = {
.visible = true,
.named = false,
},
[anon_sym_PIPE_PIPE] = {
.visible = true,
.named = false,
},
[anon_sym_EQ_EQ] = {
.visible = true,
.named = false,
},
[anon_sym_BANG_EQ] = {
.visible = true,
.named = false,
},
[anon_sym_LPAREN] = {
.visible = true,
.named = false,
},
[anon_sym_RPAREN] = {
.visible = true,
.named = false,
},
[sym_source] = {
.visible = true,
.named = true,
},
[sym__expression] = {
.visible = false,
.named = true,
},
[sym_not] = {
.visible = true,
.named = true,
},
[sym_and] = {
.visible = true,
.named = true,
},
[sym_or] = {
.visible = true,
.named = true,
},
[sym_equal] = {
.visible = true,
.named = true,
},
[sym_not_equal] = {
.visible = true,
.named = true,
},
[sym_parenthesized] = {
.visible = true,
.named = true,
},
};
enum {
field_expression = 1,
field_left = 2,
field_right = 3,
};
static const char * const ts_field_names[] = {
[0] = NULL,
[field_expression] = "expression",
[field_left] = "left",
[field_right] = "right",
};
static const TSFieldMapSlice ts_field_map_slices[PRODUCTION_ID_COUNT] = {
[1] = {.index = 0, .length = 1},
[2] = {.index = 1, .length = 2},
};
static const TSFieldMapEntry ts_field_map_entries[] = {
[0] =
{field_expression, 1},
[1] =
{field_left, 0},
{field_right, 2},
};
static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT][MAX_ALIAS_SEQUENCE_LENGTH] = {
[0] = {0},
};
static const uint16_t ts_non_terminal_alias_map[] = {
0,
};
static bool ts_lex(TSLexer *lexer, TSStateId state) {
START_LEXER();
eof = lexer->eof(lexer);
switch (state) {
case 0:
if (eof) ADVANCE(7);
if (lookahead == '!') ADVANCE(10);
if (lookahead == '&') ADVANCE(2);
if (lookahead == '(') ADVANCE(15);
if (lookahead == ')') ADVANCE(16);
if (lookahead == '=') ADVANCE(4);
if (lookahead == '|') ADVANCE(5);
if (lookahead == '\t' ||
lookahead == '\n' ||
lookahead == '\r' ||
lookahead == ' ') SKIP(0)
if (lookahead == '-' ||
('0' <= lookahead && lookahead <= '9') ||
('A' <= lookahead && lookahead <= 'Z') ||
lookahead == '_' ||
('a' <= lookahead && lookahead <= 'z')) ADVANCE(8);
END_STATE();
case 1:
if (lookahead == '!') ADVANCE(9);
if (lookahead == '(') ADVANCE(15);
if (lookahead == '\t' ||
lookahead == '\n' ||
lookahead == '\r' ||
lookahead == ' ') SKIP(1)
if (lookahead == '-' ||
('0' <= lookahead && lookahead <= '9') ||
('A' <= lookahead && lookahead <= 'Z') ||
lookahead == '_' ||
('a' <= lookahead && lookahead <= 'z')) ADVANCE(8);
END_STATE();
case 2:
if (lookahead == '&') ADVANCE(11);
END_STATE();
case 3:
if (lookahead == '=') ADVANCE(14);
END_STATE();
case 4:
if (lookahead == '=') ADVANCE(13);
END_STATE();
case 5:
if (lookahead == '|') ADVANCE(12);
END_STATE();
case 6:
if (eof) ADVANCE(7);
if (lookahead == '!') ADVANCE(3);
if (lookahead == '&') ADVANCE(2);
if (lookahead == ')') ADVANCE(16);
if (lookahead == '=') ADVANCE(4);
if (lookahead == '|') ADVANCE(5);
if (lookahead == '\t' ||
lookahead == '\n' ||
lookahead == '\r' ||
lookahead == ' ') SKIP(6)
END_STATE();
case 7:
ACCEPT_TOKEN(ts_builtin_sym_end);
END_STATE();
case 8:
ACCEPT_TOKEN(sym_identifier);
if (lookahead == '-' ||
('0' <= lookahead && lookahead <= '9') ||
('A' <= lookahead && lookahead <= 'Z') ||
lookahead == '_' ||
('a' <= lookahead && lookahead <= 'z')) ADVANCE(8);
END_STATE();
case 9:
ACCEPT_TOKEN(anon_sym_BANG);
END_STATE();
case 10:
ACCEPT_TOKEN(anon_sym_BANG);
if (lookahead == '=') ADVANCE(14);
END_STATE();
case 11:
ACCEPT_TOKEN(anon_sym_AMP_AMP);
END_STATE();
case 12:
ACCEPT_TOKEN(anon_sym_PIPE_PIPE);
END_STATE();
case 13:
ACCEPT_TOKEN(anon_sym_EQ_EQ);
END_STATE();
case 14:
ACCEPT_TOKEN(anon_sym_BANG_EQ);
END_STATE();
case 15:
ACCEPT_TOKEN(anon_sym_LPAREN);
END_STATE();
case 16:
ACCEPT_TOKEN(anon_sym_RPAREN);
END_STATE();
default:
return false;
}
}
static const TSLexMode ts_lex_modes[STATE_COUNT] = {
[0] = {.lex_state = 0},
[1] = {.lex_state = 1},
[2] = {.lex_state = 1},
[3] = {.lex_state = 1},
[4] = {.lex_state = 1},
[5] = {.lex_state = 1},
[6] = {.lex_state = 6},
[7] = {.lex_state = 0},
[8] = {.lex_state = 0},
[9] = {.lex_state = 0},
[10] = {.lex_state = 0},
[11] = {.lex_state = 0},
[12] = {.lex_state = 0},
[13] = {.lex_state = 0},
[14] = {.lex_state = 0},
[15] = {.lex_state = 0},
[16] = {.lex_state = 0},
[17] = {.lex_state = 0},
};
static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = {
[0] = {
[ts_builtin_sym_end] = ACTIONS(1),
[sym_identifier] = ACTIONS(1),
[anon_sym_BANG] = ACTIONS(1),
[anon_sym_AMP_AMP] = ACTIONS(1),
[anon_sym_PIPE_PIPE] = ACTIONS(1),
[anon_sym_EQ_EQ] = ACTIONS(1),
[anon_sym_BANG_EQ] = ACTIONS(1),
[anon_sym_LPAREN] = ACTIONS(1),
[anon_sym_RPAREN] = ACTIONS(1),
},
[1] = {
[sym_source] = STATE(15),
[sym__expression] = STATE(13),
[sym_not] = STATE(13),
[sym_and] = STATE(13),
[sym_or] = STATE(13),
[sym_equal] = STATE(13),
[sym_not_equal] = STATE(13),
[sym_parenthesized] = STATE(13),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[2] = {
[sym__expression] = STATE(7),
[sym_not] = STATE(7),
[sym_and] = STATE(7),
[sym_or] = STATE(7),
[sym_equal] = STATE(7),
[sym_not_equal] = STATE(7),
[sym_parenthesized] = STATE(7),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[3] = {
[sym__expression] = STATE(14),
[sym_not] = STATE(14),
[sym_and] = STATE(14),
[sym_or] = STATE(14),
[sym_equal] = STATE(14),
[sym_not_equal] = STATE(14),
[sym_parenthesized] = STATE(14),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[4] = {
[sym__expression] = STATE(11),
[sym_not] = STATE(11),
[sym_and] = STATE(11),
[sym_or] = STATE(11),
[sym_equal] = STATE(11),
[sym_not_equal] = STATE(11),
[sym_parenthesized] = STATE(11),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[5] = {
[sym__expression] = STATE(12),
[sym_not] = STATE(12),
[sym_and] = STATE(12),
[sym_or] = STATE(12),
[sym_equal] = STATE(12),
[sym_not_equal] = STATE(12),
[sym_parenthesized] = STATE(12),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
};
static const uint16_t ts_small_parse_table[] = {
[0] = 3,
ACTIONS(11), 1,
anon_sym_EQ_EQ,
ACTIONS(13), 1,
anon_sym_BANG_EQ,
ACTIONS(9), 4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[13] = 1,
ACTIONS(15), 4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[20] = 1,
ACTIONS(17), 4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[27] = 1,
ACTIONS(19), 4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[34] = 1,
ACTIONS(21), 4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[41] = 1,
ACTIONS(23), 4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[48] = 2,
ACTIONS(27), 1,
anon_sym_AMP_AMP,
ACTIONS(25), 3,
ts_builtin_sym_end,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[57] = 3,
ACTIONS(27), 1,
anon_sym_AMP_AMP,
ACTIONS(29), 1,
ts_builtin_sym_end,
ACTIONS(31), 1,
anon_sym_PIPE_PIPE,
[67] = 3,
ACTIONS(27), 1,
anon_sym_AMP_AMP,
ACTIONS(31), 1,
anon_sym_PIPE_PIPE,
ACTIONS(33), 1,
anon_sym_RPAREN,
[77] = 1,
ACTIONS(35), 1,
ts_builtin_sym_end,
[81] = 1,
ACTIONS(37), 1,
sym_identifier,
[85] = 1,
ACTIONS(39), 1,
sym_identifier,
};
static const uint32_t ts_small_parse_table_map[] = {
[SMALL_STATE(6)] = 0,
[SMALL_STATE(7)] = 13,
[SMALL_STATE(8)] = 20,
[SMALL_STATE(9)] = 27,
[SMALL_STATE(10)] = 34,
[SMALL_STATE(11)] = 41,
[SMALL_STATE(12)] = 48,
[SMALL_STATE(13)] = 57,
[SMALL_STATE(14)] = 67,
[SMALL_STATE(15)] = 77,
[SMALL_STATE(16)] = 81,
[SMALL_STATE(17)] = 85,
};
static const TSParseActionEntry ts_parse_actions[] = {
[0] = {.entry = {.count = 0, .reusable = false}},
[1] = {.entry = {.count = 1, .reusable = false}}, RECOVER(),
[3] = {.entry = {.count = 1, .reusable = true}}, SHIFT(6),
[5] = {.entry = {.count = 1, .reusable = true}}, SHIFT(2),
[7] = {.entry = {.count = 1, .reusable = true}}, SHIFT(3),
[9] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym__expression, 1),
[11] = {.entry = {.count = 1, .reusable = true}}, SHIFT(16),
[13] = {.entry = {.count = 1, .reusable = true}}, SHIFT(17),
[15] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_not, 2, .production_id = 1),
[17] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_equal, 3, .production_id = 2),
[19] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_not_equal, 3, .production_id = 2),
[21] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_parenthesized, 3, .production_id = 1),
[23] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_and, 3, .production_id = 2),
[25] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_or, 3, .production_id = 2),
[27] = {.entry = {.count = 1, .reusable = true}}, SHIFT(4),
[29] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_source, 1),
[31] = {.entry = {.count = 1, .reusable = true}}, SHIFT(5),
[33] = {.entry = {.count = 1, .reusable = true}}, SHIFT(10),
[35] = {.entry = {.count = 1, .reusable = true}}, ACCEPT_INPUT(),
[37] = {.entry = {.count = 1, .reusable = true}}, SHIFT(8),
[39] = {.entry = {.count = 1, .reusable = true}}, SHIFT(9),
};
#ifdef __cplusplus
extern "C" {
#endif
#ifdef _WIN32
#define extern __declspec(dllexport)
#endif
extern const TSLanguage *tree_sitter_context_predicate(void) {
static const TSLanguage language = {
.version = LANGUAGE_VERSION,
.symbol_count = SYMBOL_COUNT,
.alias_count = ALIAS_COUNT,
.token_count = TOKEN_COUNT,
.external_token_count = EXTERNAL_TOKEN_COUNT,
.state_count = STATE_COUNT,
.large_state_count = LARGE_STATE_COUNT,
.production_id_count = PRODUCTION_ID_COUNT,
.field_count = FIELD_COUNT,
.max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH,
.parse_table = &ts_parse_table[0][0],
.small_parse_table = ts_small_parse_table,
.small_parse_table_map = ts_small_parse_table_map,
.parse_actions = ts_parse_actions,
.symbol_names = ts_symbol_names,
.field_names = ts_field_names,
.field_map_slices = ts_field_map_slices,
.field_map_entries = ts_field_map_entries,
.symbol_metadata = ts_symbol_metadata,
.public_symbol_map = ts_symbol_map,
.alias_map = ts_non_terminal_alias_map,
.alias_sequences = &ts_alias_sequences[0][0],
.lex_modes = ts_lex_modes,
.lex_fn = ts_lex,
};
return &language;
}
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,223 @@
#ifndef TREE_SITTER_PARSER_H_
#define TREE_SITTER_PARSER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define ts_builtin_sym_error ((TSSymbol)-1)
#define ts_builtin_sym_end 0
#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
typedef uint16_t TSStateId;
#ifndef TREE_SITTER_API_H_
typedef uint16_t TSSymbol;
typedef uint16_t TSFieldId;
typedef struct TSLanguage TSLanguage;
#endif
typedef struct {
TSFieldId field_id;
uint8_t child_index;
bool inherited;
} TSFieldMapEntry;
typedef struct {
uint16_t index;
uint16_t length;
} TSFieldMapSlice;
typedef struct {
bool visible;
bool named;
bool supertype;
} TSSymbolMetadata;
typedef struct TSLexer TSLexer;
struct TSLexer {
int32_t lookahead;
TSSymbol result_symbol;
void (*advance)(TSLexer *, bool);
void (*mark_end)(TSLexer *);
uint32_t (*get_column)(TSLexer *);
bool (*is_at_included_range_start)(const TSLexer *);
bool (*eof)(const TSLexer *);
};
typedef enum {
TSParseActionTypeShift,
TSParseActionTypeReduce,
TSParseActionTypeAccept,
TSParseActionTypeRecover,
} TSParseActionType;
typedef union {
struct {
uint8_t type;
TSStateId state;
bool extra;
bool repetition;
} shift;
struct {
uint8_t type;
uint8_t child_count;
TSSymbol symbol;
int16_t dynamic_precedence;
uint16_t production_id;
} reduce;
uint8_t type;
} TSParseAction;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
} TSLexMode;
typedef union {
TSParseAction action;
struct {
uint8_t count;
bool reusable;
} entry;
} TSParseActionEntry;
struct TSLanguage {
uint32_t version;
uint32_t symbol_count;
uint32_t alias_count;
uint32_t token_count;
uint32_t external_token_count;
uint32_t state_count;
uint32_t large_state_count;
uint32_t production_id_count;
uint32_t field_count;
uint16_t max_alias_sequence_length;
const uint16_t *parse_table;
const uint16_t *small_parse_table;
const uint32_t *small_parse_table_map;
const TSParseActionEntry *parse_actions;
const char * const *symbol_names;
const char * const *field_names;
const TSFieldMapSlice *field_map_slices;
const TSFieldMapEntry *field_map_entries;
const TSSymbolMetadata *symbol_metadata;
const TSSymbol *public_symbol_map;
const uint16_t *alias_map;
const TSSymbol *alias_sequences;
const TSLexMode *lex_modes;
bool (*lex_fn)(TSLexer *, TSStateId);
bool (*keyword_lex_fn)(TSLexer *, TSStateId);
TSSymbol keyword_capture_token;
struct {
const bool *states;
const TSSymbol *symbol_map;
void *(*create)(void);
void (*destroy)(void *);
bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
unsigned (*serialize)(void *, char *);
void (*deserialize)(void *, const char *, unsigned);
} external_scanner;
};
/*
* Lexer Macros
*/
#define START_LEXER() \
bool result = false; \
bool skip = false; \
bool eof = false; \
int32_t lookahead; \
goto start; \
next_state: \
lexer->advance(lexer, skip); \
start: \
skip = false; \
lookahead = lexer->lookahead;
#define ADVANCE(state_value) \
{ \
state = state_value; \
goto next_state; \
}
#define SKIP(state_value) \
{ \
skip = true; \
state = state_value; \
goto next_state; \
}
#define ACCEPT_TOKEN(symbol_value) \
result = true; \
lexer->result_symbol = symbol_value; \
lexer->mark_end(lexer);
#define END_STATE() return result;
/*
* Parse Table Macros
*/
#define SMALL_STATE(id) id - LARGE_STATE_COUNT
#define STATE(id) id
#define ACTIONS(id) id
#define SHIFT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value \
} \
}}
#define SHIFT_REPEAT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value, \
.repetition = true \
} \
}}
#define SHIFT_EXTRA() \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.extra = true \
} \
}}
#define REDUCE(symbol_val, child_count_val, ...) \
{{ \
.reduce = { \
.type = TSParseActionTypeReduce, \
.symbol = symbol_val, \
.child_count = child_count_val, \
__VA_ARGS__ \
}, \
}}
#define RECOVER() \
{{ \
.type = TSParseActionTypeRecover \
}}
#define ACCEPT_INPUT() \
{{ \
.type = TSParseActionTypeAccept \
}}
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_PARSER_H_

4224
crates/gpui/src/app.rs Normal file

File diff suppressed because it is too large Load diff

46
crates/gpui/src/assets.rs Normal file
View file

@ -0,0 +1,46 @@
use anyhow::{anyhow, Result};
use std::{borrow::Cow, cell::RefCell, collections::HashMap};
pub trait AssetSource: 'static + Send + Sync {
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
fn list(&self, path: &str) -> Vec<Cow<'static, str>>;
}
impl AssetSource for () {
fn load(&self, path: &str) -> Result<Cow<[u8]>> {
Err(anyhow!(
"get called on empty asset provider with \"{}\"",
path
))
}
fn list(&self, _: &str) -> Vec<Cow<'static, str>> {
vec![]
}
}
pub struct AssetCache {
source: Box<dyn AssetSource>,
svgs: RefCell<HashMap<String, usvg::Tree>>,
}
impl AssetCache {
pub fn new(source: impl AssetSource) -> Self {
Self {
source: Box::new(source),
svgs: RefCell::new(HashMap::new()),
}
}
pub fn svg(&self, path: &str) -> Result<usvg::Tree> {
let mut svgs = self.svgs.borrow_mut();
if let Some(svg) = svgs.get(path) {
Ok(svg.clone())
} else {
let bytes = self.source.load(path)?;
let svg = usvg::Tree::from_data(&bytes, &usvg::Options::default())?;
svgs.insert(path.to_string(), svg.clone());
Ok(svg)
}
}
}

View file

@ -0,0 +1,42 @@
use seahash::SeaHasher;
use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardItem {
pub(crate) text: String,
pub(crate) metadata: Option<String>,
}
impl ClipboardItem {
pub fn new(text: String) -> Self {
Self {
text,
metadata: None,
}
}
pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
self.metadata = Some(serde_json::to_string(&metadata).unwrap());
self
}
pub fn text(&self) -> &String {
&self.text
}
pub fn metadata<T>(&self) -> Option<T>
where
T: for<'a> Deserialize<'a>,
{
self.metadata
.as_ref()
.and_then(|m| serde_json::from_str(m).ok())
}
pub(crate) fn text_hash(text: &str) -> u64 {
let mut hasher = SeaHasher::new();
text.hash(&mut hasher);
hasher.finish()
}
}

101
crates/gpui/src/color.rs Normal file
View file

@ -0,0 +1,101 @@
use std::{
borrow::Cow,
fmt,
ops::{Deref, DerefMut},
};
use crate::json::ToJson;
use pathfinder_color::ColorU;
use serde::{
de::{self, Unexpected},
Deserialize, Deserializer,
};
use serde_json::json;
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct Color(ColorU);
impl Color {
pub fn transparent_black() -> Self {
Self(ColorU::transparent_black())
}
pub fn black() -> Self {
Self(ColorU::black())
}
pub fn white() -> Self {
Self(ColorU::white())
}
pub fn red() -> Self {
Self(ColorU::from_u32(0xff0000ff))
}
pub fn green() -> Self {
Self(ColorU::from_u32(0x00ff00ff))
}
pub fn blue() -> Self {
Self(ColorU::from_u32(0x0000ffff))
}
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self(ColorU::new(r, g, b, a))
}
pub fn from_u32(rgba: u32) -> Self {
Self(ColorU::from_u32(rgba))
}
}
impl<'de> Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let literal: Cow<str> = Deserialize::deserialize(deserializer)?;
if let Some(digits) = literal.strip_prefix('#') {
if let Ok(value) = u32::from_str_radix(digits, 16) {
if digits.len() == 6 {
return Ok(Color::from_u32((value << 8) | 0xFF));
} else if digits.len() == 8 {
return Ok(Color::from_u32(value));
}
}
}
Err(de::Error::invalid_value(
Unexpected::Str(literal.as_ref()),
&"#RRGGBB[AA]",
))
}
}
impl ToJson for Color {
fn to_json(&self) -> serde_json::Value {
json!(format!(
"0x{:x}{:x}{:x}{:x}",
self.0.r, self.0.g, self.0.b, self.0.a
))
}
}
impl Deref for Color {
type Target = ColorU;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Color {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl fmt::Debug for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

401
crates/gpui/src/elements.rs Normal file
View file

@ -0,0 +1,401 @@
mod align;
mod canvas;
mod constrained_box;
mod container;
mod empty;
mod event_handler;
mod flex;
mod hook;
mod image;
mod label;
mod list;
mod mouse_event_handler;
mod overlay;
mod stack;
mod svg;
mod text;
mod uniform_list;
pub use self::{
align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*,
text::*, uniform_list::*,
};
pub use crate::presenter::ChildView;
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use core::panic;
use json::ToJson;
use std::{
any::Any,
borrow::Cow,
cell::RefCell,
mem,
ops::{Deref, DerefMut},
rc::Rc,
};
trait AnyElement {
fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F;
fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext);
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool;
fn debug(&self, cx: &DebugContext) -> serde_json::Value;
fn size(&self) -> Vector2F;
fn metadata(&self) -> Option<&dyn Any>;
}
pub trait Element {
type LayoutState;
type PaintState;
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState);
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState;
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
layout: &mut Self::LayoutState,
paint: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool;
fn metadata(&self) -> Option<&dyn Any> {
None
}
fn debug(
&self,
bounds: RectF,
layout: &Self::LayoutState,
paint: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value;
fn boxed(self) -> ElementBox
where
Self: 'static + Sized,
{
ElementBox(ElementRc {
name: None,
element: Rc::new(RefCell::new(Lifecycle::Init { element: self })),
})
}
fn named(self, name: impl Into<Cow<'static, str>>) -> ElementBox
where
Self: 'static + Sized,
{
ElementBox(ElementRc {
name: Some(name.into()),
element: Rc::new(RefCell::new(Lifecycle::Init { element: self })),
})
}
fn constrained(self) -> ConstrainedBox
where
Self: 'static + Sized,
{
ConstrainedBox::new(self.boxed())
}
fn aligned(self) -> Align
where
Self: 'static + Sized,
{
Align::new(self.boxed())
}
fn contained(self) -> Container
where
Self: 'static + Sized,
{
Container::new(self.boxed())
}
fn expanded(self, flex: f32) -> Expanded
where
Self: 'static + Sized,
{
Expanded::new(flex, self.boxed())
}
}
pub enum Lifecycle<T: Element> {
Empty,
Init {
element: T,
},
PostLayout {
element: T,
constraint: SizeConstraint,
size: Vector2F,
layout: T::LayoutState,
},
PostPaint {
element: T,
constraint: SizeConstraint,
bounds: RectF,
layout: T::LayoutState,
paint: T::PaintState,
},
}
pub struct ElementBox(ElementRc);
#[derive(Clone)]
pub struct ElementRc {
name: Option<Cow<'static, str>>,
element: Rc<RefCell<dyn AnyElement>>,
}
impl<T: Element> AnyElement for Lifecycle<T> {
fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F {
let result;
*self = match mem::take(self) {
Lifecycle::Empty => unreachable!(),
Lifecycle::Init { mut element }
| Lifecycle::PostLayout { mut element, .. }
| Lifecycle::PostPaint { mut element, .. } => {
let (size, layout) = element.layout(constraint, cx);
debug_assert!(size.x().is_finite());
debug_assert!(size.y().is_finite());
result = size;
Lifecycle::PostLayout {
element,
constraint,
size,
layout,
}
}
};
result
}
fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext) {
*self = match mem::take(self) {
Lifecycle::PostLayout {
mut element,
constraint,
size,
mut layout,
} => {
let bounds = RectF::new(origin, size);
let visible_bounds = visible_bounds
.intersection(bounds)
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
Lifecycle::PostPaint {
element,
constraint,
bounds,
layout,
paint,
}
}
Lifecycle::PostPaint {
mut element,
constraint,
bounds,
mut layout,
..
} => {
let bounds = RectF::new(origin, bounds.size());
let visible_bounds = visible_bounds
.intersection(bounds)
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
Lifecycle::PostPaint {
element,
constraint,
bounds,
layout,
paint,
}
}
_ => panic!("invalid element lifecycle state"),
}
}
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
if let Lifecycle::PostPaint {
element,
bounds,
layout,
paint,
..
} = self
{
element.dispatch_event(event, *bounds, layout, paint, cx)
} else {
panic!("invalid element lifecycle state");
}
}
fn size(&self) -> Vector2F {
match self {
Lifecycle::Empty | Lifecycle::Init { .. } => panic!("invalid element lifecycle state"),
Lifecycle::PostLayout { size, .. } => *size,
Lifecycle::PostPaint { bounds, .. } => bounds.size(),
}
}
fn metadata(&self) -> Option<&dyn Any> {
match self {
Lifecycle::Empty => unreachable!(),
Lifecycle::Init { element }
| Lifecycle::PostLayout { element, .. }
| Lifecycle::PostPaint { element, .. } => element.metadata(),
}
}
fn debug(&self, cx: &DebugContext) -> serde_json::Value {
match self {
Lifecycle::PostPaint {
element,
constraint,
bounds,
layout,
paint,
} => {
let mut value = element.debug(*bounds, layout, paint, cx);
if let json::Value::Object(map) = &mut value {
let mut new_map: crate::json::Map<String, serde_json::Value> =
Default::default();
if let Some(typ) = map.remove("type") {
new_map.insert("type".into(), typ);
}
new_map.insert("constraint".into(), constraint.to_json());
new_map.append(map);
json::Value::Object(new_map)
} else {
value
}
}
_ => panic!("invalid element lifecycle state"),
}
}
}
impl<T: Element> Default for Lifecycle<T> {
fn default() -> Self {
Self::Empty
}
}
impl ElementBox {
pub fn metadata<T: 'static>(&self) -> Option<&T> {
let element = unsafe { &*self.0.element.as_ptr() };
element.metadata().and_then(|m| m.downcast_ref())
}
}
impl Into<ElementRc> for ElementBox {
fn into(self) -> ElementRc {
self.0
}
}
impl Deref for ElementBox {
type Target = ElementRc;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ElementBox {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl ElementRc {
pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F {
self.element.borrow_mut().layout(constraint, cx)
}
pub fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext) {
self.element.borrow_mut().paint(origin, visible_bounds, cx);
}
pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
self.element.borrow_mut().dispatch_event(event, cx)
}
pub fn size(&self) -> Vector2F {
self.element.borrow().size()
}
pub fn debug(&self, cx: &DebugContext) -> json::Value {
let mut value = self.element.borrow().debug(cx);
if let Some(name) = &self.name {
if let json::Value::Object(map) = &mut value {
let mut new_map: crate::json::Map<String, serde_json::Value> = Default::default();
new_map.insert("name".into(), json::Value::String(name.to_string()));
new_map.append(map);
return json::Value::Object(new_map);
}
}
value
}
pub fn with_metadata<T, F, R>(&self, f: F) -> R
where
T: 'static,
F: FnOnce(Option<&T>) -> R,
{
let element = self.element.borrow();
f(element.metadata().and_then(|m| m.downcast_ref()))
}
}
pub trait ParentElement<'a>: Extend<ElementBox> + Sized {
fn add_children(&mut self, children: impl IntoIterator<Item = ElementBox>) {
self.extend(children);
}
fn add_child(&mut self, child: ElementBox) {
self.add_children(Some(child));
}
fn with_children(mut self, children: impl IntoIterator<Item = ElementBox>) -> Self {
self.add_children(children);
self
}
fn with_child(self, child: ElementBox) -> Self {
self.with_children(Some(child))
}
}
impl<'a, T> ParentElement<'a> for T where T: Extend<ElementBox> {}
fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
if max_size.x().is_infinite() && max_size.y().is_infinite() {
size
} else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() {
vec2f(size.x() * max_size.y() / size.y(), max_size.y())
} else {
vec2f(max_size.x(), size.y() * max_size.x() / size.x())
}
}

View file

@ -0,0 +1,105 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
use json::ToJson;
use serde_json::json;
pub struct Align {
child: ElementBox,
alignment: Vector2F,
}
impl Align {
pub fn new(child: ElementBox) -> Self {
Self {
child,
alignment: Vector2F::zero(),
}
}
pub fn top(mut self) -> Self {
self.alignment.set_y(-1.0);
self
}
pub fn left(mut self) -> Self {
self.alignment.set_x(-1.0);
self
}
pub fn right(mut self) -> Self {
self.alignment.set_x(1.0);
self
}
}
impl Element for Align {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.max;
constraint.min = Vector2F::zero();
let child_size = self.child.layout(constraint, cx);
if size.x().is_infinite() {
size.set_x(child_size.x());
}
if size.y().is_infinite() {
size.set_y(child_size.y());
}
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let my_center = bounds.size() / 2.;
let my_target = my_center + my_center * self.alignment;
let child_center = self.child.size() / 2.;
let child_target = child_center + child_center * self.alignment;
self.child.paint(
bounds.origin() - (child_target - my_target),
visible_bounds,
cx,
);
}
fn dispatch_event(
&mut self,
event: &Event,
_: pathfinder_geometry::rect::RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
bounds: pathfinder_geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> json::Value {
json!({
"type": "Align",
"bounds": bounds.to_json(),
"alignment": self.alignment.to_json(),
"child": self.child.debug(cx),
})
}
}

View file

@ -0,0 +1,78 @@
use super::Element;
use crate::{
json::{self, json},
DebugContext, PaintContext,
};
use json::ToJson;
use pathfinder_geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
};
pub struct Canvas<F>(F);
impl<F> Canvas<F>
where
F: FnMut(RectF, RectF, &mut PaintContext),
{
pub fn new(f: F) -> Self {
Self(f)
}
}
impl<F> Element for Canvas<F>
where
F: FnMut(RectF, RectF, &mut PaintContext),
{
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: crate::SizeConstraint,
_: &mut crate::LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let x = if constraint.max.x().is_finite() {
constraint.max.x()
} else {
constraint.min.x()
};
let y = if constraint.max.y().is_finite() {
constraint.max.y()
} else {
constraint.min.y()
};
(vec2f(x, y), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.0(bounds, visible_bounds, cx)
}
fn dispatch_event(
&mut self,
_: &crate::Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut crate::EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> json::Value {
json!({"type": "Canvas", "bounds": bounds.to_json()})
}
}

View file

@ -0,0 +1,100 @@
use json::ToJson;
use serde_json::json;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
pub struct ConstrainedBox {
child: ElementBox,
constraint: SizeConstraint,
}
impl ConstrainedBox {
pub fn new(child: ElementBox) -> Self {
Self {
child,
constraint: SizeConstraint {
min: Vector2F::zero(),
max: Vector2F::splat(f32::INFINITY),
},
}
}
pub fn with_min_width(mut self, min_width: f32) -> Self {
self.constraint.min.set_x(min_width);
self
}
pub fn with_max_width(mut self, max_width: f32) -> Self {
self.constraint.max.set_x(max_width);
self
}
pub fn with_max_height(mut self, max_height: f32) -> Self {
self.constraint.max.set_y(max_height);
self
}
pub fn with_width(mut self, width: f32) -> Self {
self.constraint.min.set_x(width);
self.constraint.max.set_x(width);
self
}
pub fn with_height(mut self, height: f32) -> Self {
self.constraint.min.set_y(height);
self.constraint.max.set_y(height);
self
}
}
impl Element for ConstrainedBox {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
constraint.min = constraint.min.max(self.constraint.min);
constraint.max = constraint.max.min(self.constraint.max);
constraint.max = constraint.max.max(constraint.min);
let size = self.child.layout(constraint, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> json::Value {
json!({"type": "ConstrainedBox", "set_constraint": self.constraint.to_json(), "child": self.child.debug(cx)})
}
}

View file

@ -0,0 +1,435 @@
use pathfinder_geometry::rect::RectF;
use serde::Deserialize;
use serde_json::json;
use crate::{
color::Color,
geometry::{
deserialize_vec2f,
vector::{vec2f, Vector2F},
},
json::ToJson,
scene::{self, Border, Quad},
Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
#[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct ContainerStyle {
#[serde(default)]
pub margin: Margin,
#[serde(default)]
pub padding: Padding,
#[serde(rename = "background")]
pub background_color: Option<Color>,
#[serde(default)]
pub border: Border,
#[serde(default)]
pub corner_radius: f32,
#[serde(default)]
pub shadow: Option<Shadow>,
}
pub struct Container {
child: ElementBox,
style: ContainerStyle,
}
impl Container {
pub fn new(child: ElementBox) -> Self {
Self {
child,
style: Default::default(),
}
}
pub fn with_style(mut self, style: ContainerStyle) -> Self {
self.style = style;
self
}
pub fn with_margin_top(mut self, margin: f32) -> Self {
self.style.margin.top = margin;
self
}
pub fn with_margin_left(mut self, margin: f32) -> Self {
self.style.margin.left = margin;
self
}
pub fn with_margin_right(mut self, margin: f32) -> Self {
self.style.margin.right = margin;
self
}
pub fn with_horizontal_padding(mut self, padding: f32) -> Self {
self.style.padding.left = padding;
self.style.padding.right = padding;
self
}
pub fn with_vertical_padding(mut self, padding: f32) -> Self {
self.style.padding.top = padding;
self.style.padding.bottom = padding;
self
}
pub fn with_uniform_padding(mut self, padding: f32) -> Self {
self.style.padding = Padding {
top: padding,
left: padding,
bottom: padding,
right: padding,
};
self
}
pub fn with_padding_left(mut self, padding: f32) -> Self {
self.style.padding.left = padding;
self
}
pub fn with_padding_right(mut self, padding: f32) -> Self {
self.style.padding.right = padding;
self
}
pub fn with_padding_bottom(mut self, padding: f32) -> Self {
self.style.padding.bottom = padding;
self
}
pub fn with_background_color(mut self, color: Color) -> Self {
self.style.background_color = Some(color);
self
}
pub fn with_border(mut self, border: Border) -> Self {
self.style.border = border;
self
}
pub fn with_corner_radius(mut self, radius: f32) -> Self {
self.style.corner_radius = radius;
self
}
pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: Color) -> Self {
self.style.shadow = Some(Shadow {
offset,
blur,
color,
});
self
}
fn margin_size(&self) -> Vector2F {
vec2f(
self.style.margin.left + self.style.margin.right,
self.style.margin.top + self.style.margin.bottom,
)
}
fn padding_size(&self) -> Vector2F {
vec2f(
self.style.padding.left + self.style.padding.right,
self.style.padding.top + self.style.padding.bottom,
)
}
fn border_size(&self) -> Vector2F {
let mut x = 0.0;
if self.style.border.left {
x += self.style.border.width;
}
if self.style.border.right {
x += self.style.border.width;
}
let mut y = 0.0;
if self.style.border.top {
y += self.style.border.width;
}
if self.style.border.bottom {
y += self.style.border.width;
}
vec2f(x, y)
}
}
impl Element for Container {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let mut size_buffer = self.margin_size() + self.padding_size();
if !self.style.border.overlay {
size_buffer += self.border_size();
}
let child_constraint = SizeConstraint {
min: (constraint.min - size_buffer).max(Vector2F::zero()),
max: (constraint.max - size_buffer).max(Vector2F::zero()),
};
let child_size = self.child.layout(child_constraint, cx);
(child_size + size_buffer, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let quad_bounds = RectF::from_points(
bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top),
bounds.lower_right() - vec2f(self.style.margin.right, self.style.margin.bottom),
);
if let Some(shadow) = self.style.shadow.as_ref() {
cx.scene.push_shadow(scene::Shadow {
bounds: quad_bounds + shadow.offset,
corner_radius: self.style.corner_radius,
sigma: shadow.blur,
color: shadow.color,
});
}
let child_origin =
quad_bounds.origin() + vec2f(self.style.padding.left, self.style.padding.top);
if self.style.border.overlay {
cx.scene.push_quad(Quad {
bounds: quad_bounds,
background: self.style.background_color,
border: Default::default(),
corner_radius: self.style.corner_radius,
});
self.child.paint(child_origin, visible_bounds, cx);
cx.scene.push_layer(None);
cx.scene.push_quad(Quad {
bounds: quad_bounds,
background: Default::default(),
border: self.style.border,
corner_radius: self.style.corner_radius,
});
cx.scene.pop_layer();
} else {
cx.scene.push_quad(Quad {
bounds: quad_bounds,
background: self.style.background_color,
border: self.style.border,
corner_radius: self.style.corner_radius,
});
let child_origin = child_origin
+ vec2f(
self.style.border.left_width(),
self.style.border.top_width(),
);
self.child.paint(child_origin, visible_bounds, cx);
}
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &crate::DebugContext,
) -> serde_json::Value {
json!({
"type": "Container",
"bounds": bounds.to_json(),
"details": self.style.to_json(),
"child": self.child.debug(cx),
})
}
}
impl ToJson for ContainerStyle {
fn to_json(&self) -> serde_json::Value {
json!({
"margin": self.margin.to_json(),
"padding": self.padding.to_json(),
"background_color": self.background_color.to_json(),
"border": self.border.to_json(),
"corner_radius": self.corner_radius,
"shadow": self.shadow.to_json(),
})
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Margin {
pub top: f32,
pub left: f32,
pub bottom: f32,
pub right: f32,
}
impl ToJson for Margin {
fn to_json(&self) -> serde_json::Value {
let mut value = json!({});
if self.top > 0. {
value["top"] = json!(self.top);
}
if self.right > 0. {
value["right"] = json!(self.right);
}
if self.bottom > 0. {
value["bottom"] = json!(self.bottom);
}
if self.left > 0. {
value["left"] = json!(self.left);
}
value
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Padding {
pub top: f32,
pub left: f32,
pub bottom: f32,
pub right: f32,
}
impl<'de> Deserialize<'de> for Padding {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let spacing = Spacing::deserialize(deserializer)?;
Ok(match spacing {
Spacing::Uniform(size) => Padding {
top: size,
left: size,
bottom: size,
right: size,
},
Spacing::Specific {
top,
left,
bottom,
right,
} => Padding {
top,
left,
bottom,
right,
},
})
}
}
impl<'de> Deserialize<'de> for Margin {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let spacing = Spacing::deserialize(deserializer)?;
Ok(match spacing {
Spacing::Uniform(size) => Margin {
top: size,
left: size,
bottom: size,
right: size,
},
Spacing::Specific {
top,
left,
bottom,
right,
} => Margin {
top,
left,
bottom,
right,
},
})
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Spacing {
Uniform(f32),
Specific {
#[serde(default)]
top: f32,
#[serde(default)]
left: f32,
#[serde(default)]
bottom: f32,
#[serde(default)]
right: f32,
},
}
impl Padding {
pub fn uniform(padding: f32) -> Self {
Self {
top: padding,
left: padding,
bottom: padding,
right: padding,
}
}
}
impl ToJson for Padding {
fn to_json(&self) -> serde_json::Value {
let mut value = json!({});
if self.top > 0. {
value["top"] = json!(self.top);
}
if self.right > 0. {
value["right"] = json!(self.right);
}
if self.bottom > 0. {
value["bottom"] = json!(self.bottom);
}
if self.left > 0. {
value["left"] = json!(self.left);
}
value
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct Shadow {
#[serde(default, deserialize_with = "deserialize_vec2f")]
offset: Vector2F,
#[serde(default)]
blur: f32,
#[serde(default)]
color: Color,
}
impl ToJson for Shadow {
fn to_json(&self) -> serde_json::Value {
json!({
"offset": self.offset.to_json(),
"blur": self.blur,
"color": self.color.to_json()
})
}
}

View file

@ -0,0 +1,74 @@
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
DebugContext,
};
use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
pub struct Empty;
impl Empty {
pub fn new() -> Self {
Self
}
}
impl Element for Empty {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
_: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let x = if constraint.max.x().is_finite() {
constraint.max.x()
} else {
constraint.min.x()
};
let y = if constraint.max.y().is_finite() {
constraint.max.y()
} else {
constraint.min.y()
};
(vec2f(x, y), ())
}
fn paint(
&mut self,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut PaintContext,
) -> Self::PaintState {
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "Empty",
"bounds": bounds.to_json(),
})
}
}

View file

@ -0,0 +1,91 @@
use pathfinder_geometry::rect::RectF;
use serde_json::json;
use crate::{
geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext,
LayoutContext, PaintContext, SizeConstraint,
};
pub struct EventHandler {
child: ElementBox,
mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
}
impl EventHandler {
pub fn new(child: ElementBox) -> Self {
Self {
child,
mouse_down: None,
}
}
pub fn on_mouse_down<F>(mut self, callback: F) -> Self
where
F: 'static + FnMut(&mut EventContext) -> bool,
{
self.mouse_down = Some(Box::new(callback));
self
}
}
impl Element for EventHandler {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
if self.child.dispatch_event(event, cx) {
true
} else {
match event {
Event::LeftMouseDown { position, .. } => {
if let Some(callback) = self.mouse_down.as_mut() {
if bounds.contains_point(*position) {
return callback(cx);
}
}
false
}
_ => false,
}
}
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value {
json!({
"type": "EventHandler",
"child": self.child.debug(cx),
})
}
}

View file

@ -0,0 +1,369 @@
use std::{any::Any, f32::INFINITY};
use crate::{
json::{self, ToJson, Value},
Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint, Vector2FExt,
};
use pathfinder_geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
};
use serde_json::json;
pub struct Flex {
axis: Axis,
children: Vec<ElementBox>,
}
impl Flex {
pub fn new(axis: Axis) -> Self {
Self {
axis,
children: Default::default(),
}
}
pub fn row() -> Self {
Self::new(Axis::Horizontal)
}
pub fn column() -> Self {
Self::new(Axis::Vertical)
}
fn layout_flex_children(
&mut self,
expanded: bool,
constraint: SizeConstraint,
remaining_space: &mut f32,
remaining_flex: &mut f32,
cross_axis_max: &mut f32,
cx: &mut LayoutContext,
) {
let cross_axis = self.axis.invert();
for child in &mut self.children {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if metadata.expanded != expanded {
continue;
}
let flex = metadata.flex;
let child_max = if *remaining_flex == 0.0 {
*remaining_space
} else {
let space_per_flex = *remaining_space / *remaining_flex;
space_per_flex * flex
};
let child_min = if expanded { child_max } else { 0. };
let child_constraint = match self.axis {
Axis::Horizontal => SizeConstraint::new(
vec2f(child_min, constraint.min.y()),
vec2f(child_max, constraint.max.y()),
),
Axis::Vertical => SizeConstraint::new(
vec2f(constraint.min.x(), child_min),
vec2f(constraint.max.x(), child_max),
),
};
let child_size = child.layout(child_constraint, cx);
*remaining_space -= child_size.along(self.axis);
*remaining_flex -= flex;
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
}
}
}
}
impl Extend<ElementBox> for Flex {
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
self.children.extend(children);
}
}
impl Element for Flex {
type LayoutState = bool;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let mut total_flex = None;
let mut fixed_space = 0.0;
let cross_axis = self.axis.invert();
let mut cross_axis_max: f32 = 0.0;
for child in &mut self.children {
if let Some(metadata) = child.metadata::<FlexParentData>() {
*total_flex.get_or_insert(0.) += metadata.flex;
} else {
let child_constraint = match self.axis {
Axis::Horizontal => SizeConstraint::new(
vec2f(0.0, constraint.min.y()),
vec2f(INFINITY, constraint.max.y()),
),
Axis::Vertical => SizeConstraint::new(
vec2f(constraint.min.x(), 0.0),
vec2f(constraint.max.x(), INFINITY),
),
};
let size = child.layout(child_constraint, cx);
fixed_space += size.along(self.axis);
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
}
}
let mut size = if let Some(mut remaining_flex) = total_flex {
if constraint.max_along(self.axis).is_infinite() {
panic!("flex contains flexible children but has an infinite constraint along the flex axis");
}
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
self.layout_flex_children(
false,
constraint,
&mut remaining_space,
&mut remaining_flex,
&mut cross_axis_max,
cx,
);
self.layout_flex_children(
true,
constraint,
&mut remaining_space,
&mut remaining_flex,
&mut cross_axis_max,
cx,
);
match self.axis {
Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
}
} else {
match self.axis {
Axis::Horizontal => vec2f(fixed_space, cross_axis_max),
Axis::Vertical => vec2f(cross_axis_max, fixed_space),
}
};
if constraint.min.x().is_finite() {
size.set_x(size.x().max(constraint.min.x()));
}
if constraint.min.y().is_finite() {
size.set_y(size.y().max(constraint.min.y()));
}
let mut overflowing = false;
if size.x() > constraint.max.x() {
size.set_x(constraint.max.x());
overflowing = true;
}
if size.y() > constraint.max.y() {
size.set_y(constraint.max.y());
overflowing = true;
}
(size, overflowing)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
overflowing: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
if *overflowing {
cx.scene.push_layer(Some(bounds));
}
let mut child_origin = bounds.origin();
for child in &mut self.children {
child.paint(child_origin, visible_bounds, cx);
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
}
}
if *overflowing {
cx.scene.pop_layer();
}
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
let mut handled = false;
for child in &mut self.children {
handled = child.dispatch_event(event, cx) || handled;
}
handled
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> json::Value {
json!({
"type": "Flex",
"bounds": bounds.to_json(),
"axis": self.axis.to_json(),
"children": self.children.iter().map(|child| child.debug(cx)).collect::<Vec<json::Value>>()
})
}
}
struct FlexParentData {
flex: f32,
expanded: bool,
}
pub struct Expanded {
metadata: FlexParentData,
child: ElementBox,
}
impl Expanded {
pub fn new(flex: f32, child: ElementBox) -> Self {
Expanded {
metadata: FlexParentData {
flex,
expanded: true,
},
child,
}
}
}
impl Element for Expanded {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx)
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn metadata(&self) -> Option<&dyn Any> {
Some(&self.metadata)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> Value {
json!({
"type": "Expanded",
"flex": self.metadata.flex,
"child": self.child.debug(cx)
})
}
}
pub struct Flexible {
metadata: FlexParentData,
child: ElementBox,
}
impl Flexible {
pub fn new(flex: f32, child: ElementBox) -> Self {
Flexible {
metadata: FlexParentData {
flex,
expanded: false,
},
child,
}
}
}
impl Element for Flexible {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx)
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn metadata(&self) -> Option<&dyn Any> {
Some(&self.metadata)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> Value {
json!({
"type": "Flexible",
"flex": self.metadata.flex,
"child": self.child.debug(cx)
})
}
}

View file

@ -0,0 +1,79 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::json,
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
pub struct Hook {
child: ElementBox,
after_layout: Option<Box<dyn FnMut(Vector2F, &mut LayoutContext)>>,
}
impl Hook {
pub fn new(child: ElementBox) -> Self {
Self {
child,
after_layout: None,
}
}
pub fn on_after_layout(
mut self,
f: impl 'static + FnMut(Vector2F, &mut LayoutContext),
) -> Self {
self.after_layout = Some(Box::new(f));
self
}
}
impl Element for Hook {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
if let Some(handler) = self.after_layout.as_mut() {
handler(size, cx);
}
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value {
json!({
"type": "Hooks",
"child": self.child.debug(cx),
})
}
}

View file

@ -0,0 +1,103 @@
use super::constrain_size_preserving_aspect_ratio;
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext,
PaintContext, SizeConstraint,
};
use serde::Deserialize;
use std::sync::Arc;
pub struct Image {
data: Arc<ImageData>,
style: ImageStyle,
}
#[derive(Copy, Clone, Default, Deserialize)]
pub struct ImageStyle {
#[serde(default)]
pub border: Border,
#[serde(default)]
pub corner_radius: f32,
#[serde(default)]
pub height: Option<f32>,
#[serde(default)]
pub width: Option<f32>,
}
impl Image {
pub fn new(data: Arc<ImageData>) -> Self {
Self {
data,
style: Default::default(),
}
}
pub fn with_style(mut self, style: ImageStyle) -> Self {
self.style = style;
self
}
}
impl Element for Image {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
_: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let desired_size = vec2f(
self.style.width.unwrap_or(constraint.max.x()),
self.style.height.unwrap_or(constraint.max.y()),
);
let size = constrain_size_preserving_aspect_ratio(
constraint.constrain(desired_size),
self.data.size().to_f32(),
);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
_: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_image(scene::Image {
bounds,
border: self.style.border,
corner_radius: self.style.corner_radius,
data: self.data.clone(),
});
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "Image",
"bounds": bounds.to_json(),
})
}
}

View file

@ -0,0 +1,263 @@
use crate::{
fonts::TextStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{ToJson, Value},
text_layout::{Line, RunStyle},
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use serde::Deserialize;
use serde_json::json;
use smallvec::{smallvec, SmallVec};
pub struct Label {
text: String,
style: LabelStyle,
highlight_indices: Vec<usize>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct LabelStyle {
pub text: TextStyle,
pub highlight_text: Option<TextStyle>,
}
impl From<TextStyle> for LabelStyle {
fn from(text: TextStyle) -> Self {
LabelStyle {
text,
highlight_text: None,
}
}
}
impl Label {
pub fn new(text: String, style: impl Into<LabelStyle>) -> Self {
Self {
text,
highlight_indices: Default::default(),
style: style.into(),
}
}
pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
self.highlight_indices = indices;
self
}
fn compute_runs(&self) -> SmallVec<[(usize, RunStyle); 8]> {
let font_id = self.style.text.font_id;
if self.highlight_indices.is_empty() {
return smallvec![(
self.text.len(),
RunStyle {
font_id,
color: self.style.text.color,
underline: self.style.text.underline,
}
)];
}
let highlight_font_id = self
.style
.highlight_text
.as_ref()
.map_or(font_id, |style| style.font_id);
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
let mut runs = SmallVec::new();
let highlight_style = self
.style
.highlight_text
.as_ref()
.unwrap_or(&self.style.text);
for (char_ix, c) in self.text.char_indices() {
let mut font_id = font_id;
let mut color = self.style.text.color;
let mut underline = self.style.text.underline;
if let Some(highlight_ix) = highlight_indices.peek() {
if char_ix == *highlight_ix {
font_id = highlight_font_id;
color = highlight_style.color;
underline = highlight_style.underline;
highlight_indices.next();
}
}
let last_run: Option<&mut (usize, RunStyle)> = runs.last_mut();
let push_new_run = if let Some((last_len, last_style)) = last_run {
if font_id == last_style.font_id
&& color == last_style.color
&& underline == last_style.underline
{
*last_len += c.len_utf8();
false
} else {
true
}
} else {
true
};
if push_new_run {
runs.push((
c.len_utf8(),
RunStyle {
font_id,
color,
underline,
},
));
}
}
runs
}
}
impl Element for Label {
type LayoutState = Line;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let runs = self.compute_runs();
let line = cx.text_layout_cache.layout_str(
self.text.as_str(),
self.style.text.font_size,
runs.as_slice(),
);
let size = vec2f(
line.width()
.ceil()
.max(constraint.min.x())
.min(constraint.max.x()),
cx.font_cache
.line_height(self.style.text.font_id, self.style.text.font_size),
);
(size, line)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
line: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> Value {
json!({
"type": "Label",
"bounds": bounds.to_json(),
"text": &self.text,
"highlight_indices": self.highlight_indices,
"style": self.style.to_json(),
})
}
}
impl ToJson for LabelStyle {
fn to_json(&self) -> Value {
json!({
"text": self.text.to_json(),
"highlight_text": self.highlight_text
.as_ref()
.map_or(serde_json::Value::Null, |style| style.to_json())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
use crate::fonts::{Properties as FontProperties, Weight};
#[crate::test(self)]
fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) {
let default_style = TextStyle::new(
"Menlo",
12.,
Default::default(),
false,
Color::black(),
cx.font_cache(),
)
.unwrap();
let highlight_style = TextStyle::new(
"Menlo",
12.,
*FontProperties::new().weight(Weight::BOLD),
false,
Color::new(255, 0, 0, 255),
cx.font_cache(),
)
.unwrap();
let label = Label::new(
".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(),
LabelStyle {
text: default_style.clone(),
highlight_text: Some(highlight_style.clone()),
},
)
.with_highlights(vec![
".α".len(),
".αβ".len(),
".αβγδ".len(),
".αβγδε.ⓐ".len(),
".αβγδε.ⓐⓑ".len(),
]);
let default_run_style = RunStyle {
font_id: default_style.font_id,
color: default_style.color,
underline: default_style.underline,
};
let highlight_run_style = RunStyle {
font_id: highlight_style.font_id,
color: highlight_style.color,
underline: highlight_style.underline,
};
let runs = label.compute_runs();
assert_eq!(
runs.as_slice(),
&[
(".α".len(), default_run_style),
("βγ".len(), highlight_run_style),
("δ".len(), default_run_style),
("ε".len(), highlight_run_style),
(".ⓐ".len(), default_run_style),
("ⓑⓒ".len(), highlight_run_style),
("ⓓⓔ.abcde.".len(), default_run_style),
]
);
}
}

View file

@ -0,0 +1,890 @@
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::json,
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
use sum_tree::{self, Bias, SumTree};
pub struct List {
state: ListState,
invalidated_elements: Vec<ElementRc>,
}
#[derive(Clone)]
pub struct ListState(Rc<RefCell<StateInner>>);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Orientation {
Top,
Bottom,
}
struct StateInner {
last_layout_width: Option<f32>,
render_item: Box<dyn FnMut(usize, &mut LayoutContext) -> ElementBox>,
rendered_range: Range<usize>,
items: SumTree<ListItem>,
logical_scroll_top: Option<ListOffset>,
orientation: Orientation,
overdraw: f32,
scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ListOffset {
item_ix: usize,
offset_in_item: f32,
}
#[derive(Clone)]
enum ListItem {
Unrendered,
Rendered(ElementRc),
Removed(f32),
}
impl std::fmt::Debug for ListItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unrendered => write!(f, "Unrendered"),
Self::Rendered(_) => f.debug_tuple("Rendered").finish(),
Self::Removed(height) => f.debug_tuple("Removed").field(height).finish(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
struct ListItemSummary {
count: usize,
rendered_count: usize,
unrendered_count: usize,
height: f32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct Count(usize);
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct RenderedCount(usize);
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct UnrenderedCount(usize);
#[derive(Clone, Debug, Default)]
struct Height(f32);
impl List {
pub fn new(state: ListState) -> Self {
Self {
state,
invalidated_elements: Default::default(),
}
}
}
impl Element for List {
type LayoutState = ListOffset;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let state = &mut *self.state.0.borrow_mut();
let size = constraint.max;
let mut item_constraint = constraint;
item_constraint.min.set_y(0.);
item_constraint.max.set_y(f32::INFINITY);
if cx.refreshing || state.last_layout_width != Some(size.x()) {
state.rendered_range = 0..0;
state.items = SumTree::from_iter(
(0..state.items.summary().count).map(|_| ListItem::Unrendered),
&(),
)
}
let old_items = state.items.clone();
let mut new_items = SumTree::new();
let mut rendered_items = VecDeque::new();
let mut rendered_height = 0.;
let mut scroll_top = state
.logical_scroll_top
.unwrap_or_else(|| match state.orientation {
Orientation::Top => ListOffset {
item_ix: 0,
offset_in_item: 0.,
},
Orientation::Bottom => ListOffset {
item_ix: state.items.summary().count,
offset_in_item: 0.,
},
});
// Render items after the scroll top, including those in the trailing overdraw.
let mut cursor = old_items.cursor::<Count>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
for (ix, item) in cursor.by_ref().enumerate() {
if rendered_height - scroll_top.offset_in_item >= size.y() + state.overdraw {
break;
}
let element = state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx);
rendered_height += element.size().y();
rendered_items.push_back(ListItem::Rendered(element));
}
// Prepare to start walking upward from the item at the scroll top.
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
// If the rendered items do not fill the visible region, then adjust
// the scroll top upward.
if rendered_height - scroll_top.offset_in_item < size.y() {
while rendered_height < size.y() {
cursor.prev(&());
if let Some(item) = cursor.item() {
let element = state.render_item(cursor.start().0, item, item_constraint, cx);
rendered_height += element.size().y();
rendered_items.push_front(ListItem::Rendered(element));
} else {
break;
}
}
scroll_top = ListOffset {
item_ix: cursor.start().0,
offset_in_item: rendered_height - size.y(),
};
match state.orientation {
Orientation::Top => {
scroll_top.offset_in_item = scroll_top.offset_in_item.max(0.);
state.logical_scroll_top = Some(scroll_top);
}
Orientation::Bottom => {
scroll_top = ListOffset {
item_ix: cursor.start().0,
offset_in_item: rendered_height - size.y(),
};
state.logical_scroll_top = None;
}
};
}
// Render items in the leading overdraw.
let mut leading_overdraw = scroll_top.offset_in_item;
while leading_overdraw < state.overdraw {
cursor.prev(&());
if let Some(item) = cursor.item() {
let element = state.render_item(cursor.start().0, item, item_constraint, cx);
leading_overdraw += element.size().y();
rendered_items.push_front(ListItem::Rendered(element));
} else {
break;
}
}
let new_rendered_range = cursor.start().0..(cursor.start().0 + rendered_items.len());
let mut cursor = old_items.cursor::<Count>();
if state.rendered_range.start < new_rendered_range.start {
new_items.push_tree(
cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
&(),
);
let remove_to = state.rendered_range.end.min(new_rendered_range.start);
while cursor.start().0 < remove_to {
new_items.push(cursor.item().unwrap().remove(), &());
cursor.next(&());
}
}
new_items.push_tree(
cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
&(),
);
new_items.extend(rendered_items, &());
cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
if new_rendered_range.end < state.rendered_range.start {
new_items.push_tree(
cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
&(),
);
}
while cursor.start().0 < state.rendered_range.end {
new_items.push(cursor.item().unwrap().remove(), &());
cursor.next(&());
}
new_items.push_tree(cursor.suffix(&()), &());
state.items = new_items;
state.rendered_range = new_rendered_range;
state.last_layout_width = Some(size.x());
(size, scroll_top)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
scroll_top: &mut ListOffset,
cx: &mut PaintContext,
) {
cx.scene.push_layer(Some(bounds));
let state = &mut *self.state.0.borrow_mut();
for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
element.paint(origin, visible_bounds, cx);
}
cx.scene.pop_layer();
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
scroll_top: &mut ListOffset,
_: &mut (),
cx: &mut EventContext,
) -> bool {
let mut handled = false;
let mut state = self.state.0.borrow_mut();
let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
let mut cursor = state.items.cursor::<Count>();
let mut new_items = cursor.slice(&Count(scroll_top.item_ix), Bias::Right, &());
while let Some(item) = cursor.item() {
if item_origin.y() > bounds.max_y() {
break;
}
if let ListItem::Rendered(element) = item {
let prev_notify_count = cx.notify_count();
let mut element = element.clone();
handled = element.dispatch_event(event, cx) || handled;
item_origin.set_y(item_origin.y() + element.size().y());
if cx.notify_count() > prev_notify_count {
new_items.push(ListItem::Unrendered, &());
self.invalidated_elements.push(element);
} else {
new_items.push(item.clone(), &());
}
cursor.next(&());
} else {
unreachable!();
}
}
new_items.push_tree(cursor.suffix(&()), &());
drop(cursor);
state.items = new_items;
match event {
Event::ScrollWheel {
position,
delta,
precise,
} => {
if bounds.contains_point(*position) {
if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
handled = true;
}
}
}
_ => {}
}
handled
}
fn debug(
&self,
bounds: RectF,
scroll_top: &Self::LayoutState,
_: &(),
cx: &DebugContext,
) -> serde_json::Value {
let state = self.state.0.borrow_mut();
let visible_elements = state
.visible_elements(bounds, scroll_top)
.map(|e| e.0.debug(cx))
.collect::<Vec<_>>();
let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
json!({
"visible_range": visible_range,
"visible_elements": visible_elements,
"scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
})
}
}
impl ListState {
pub fn new<F>(
element_count: usize,
orientation: Orientation,
overdraw: f32,
render_item: F,
) -> Self
where
F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox,
{
let mut items = SumTree::new();
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
Self(Rc::new(RefCell::new(StateInner {
last_layout_width: None,
render_item: Box::new(render_item),
rendered_range: 0..0,
items,
logical_scroll_top: None,
orientation,
overdraw,
scroll_handler: None,
})))
}
pub fn reset(&self, element_count: usize) {
let state = &mut *self.0.borrow_mut();
state.rendered_range = 0..0;
state.logical_scroll_top = None;
state.items = SumTree::new();
state
.items
.extend((0..element_count).map(|_| ListItem::Unrendered), &());
}
pub fn splice(&self, old_range: Range<usize>, count: usize) {
let state = &mut *self.0.borrow_mut();
if let Some(ListOffset {
item_ix,
offset_in_item,
}) = state.logical_scroll_top.as_mut()
{
if old_range.contains(item_ix) {
*item_ix = old_range.start;
*offset_in_item = 0.;
} else if old_range.end <= *item_ix {
*item_ix = *item_ix - (old_range.end - old_range.start) + count;
}
}
let new_end = old_range.start + count;
if old_range.start < state.rendered_range.start {
state.rendered_range.start =
new_end + state.rendered_range.start.saturating_sub(old_range.end);
}
if old_range.start < state.rendered_range.end {
state.rendered_range.end =
new_end + state.rendered_range.end.saturating_sub(old_range.end);
}
let mut old_heights = state.items.cursor::<Count>();
let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
new_heights.push_tree(old_heights.suffix(&()), &());
drop(old_heights);
state.items = new_heights;
}
pub fn set_scroll_handler(
&mut self,
handler: impl FnMut(Range<usize>, &mut EventContext) + 'static,
) {
self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
}
}
impl StateInner {
fn render_item(
&mut self,
ix: usize,
existing_item: &ListItem,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> ElementRc {
if let ListItem::Rendered(element) = existing_item {
element.clone()
} else {
let mut element = (self.render_item)(ix, cx);
element.layout(constraint, cx);
element.into()
}
}
fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
let start_y = cursor.start().height + scroll_top.offset_in_item;
cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
scroll_top.item_ix..cursor.start().count + 1
}
fn visible_elements<'a>(
&'a self,
bounds: RectF,
scroll_top: &ListOffset,
) -> impl Iterator<Item = (ElementRc, Vector2F)> + 'a {
let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
let mut cursor = self.items.cursor::<Count>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
std::iter::from_fn(move || {
while let Some(item) = cursor.item() {
if item_origin.y() > bounds.max_y() {
break;
}
if let ListItem::Rendered(element) = item {
let result = (element.clone(), item_origin);
item_origin.set_y(item_origin.y() + element.size().y());
cursor.next(&());
return Some(result);
}
cursor.next(&());
}
None
})
}
fn scroll(
&mut self,
scroll_top: &ListOffset,
height: f32,
mut delta: Vector2F,
precise: bool,
cx: &mut EventContext,
) -> bool {
if !precise {
delta *= 20.;
}
let scroll_max = (self.items.summary().height - height).max(0.);
let new_scroll_top = (self.scroll_top(scroll_top) - delta.y())
.max(0.)
.min(scroll_max);
if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
self.logical_scroll_top = None;
} else {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
let item_ix = cursor.start().count;
let offset_in_item = new_scroll_top - cursor.start().height;
self.logical_scroll_top = Some(ListOffset {
item_ix,
offset_in_item,
});
}
if self.scroll_handler.is_some() {
let visible_range = self.visible_range(height, scroll_top);
self.scroll_handler.as_mut().unwrap()(visible_range, cx);
}
cx.notify();
true
}
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
cursor.start().height + logical_scroll_top.offset_in_item
}
}
impl ListItem {
fn remove(&self) -> Self {
match self {
ListItem::Unrendered => ListItem::Unrendered,
ListItem::Rendered(element) => ListItem::Removed(element.size().y()),
ListItem::Removed(height) => ListItem::Removed(*height),
}
}
}
impl sum_tree::Item for ListItem {
type Summary = ListItemSummary;
fn summary(&self) -> Self::Summary {
match self {
ListItem::Unrendered => ListItemSummary {
count: 1,
rendered_count: 0,
unrendered_count: 1,
height: 0.,
},
ListItem::Rendered(element) => ListItemSummary {
count: 1,
rendered_count: 1,
unrendered_count: 0,
height: element.size().y(),
},
ListItem::Removed(height) => ListItemSummary {
count: 1,
rendered_count: 0,
unrendered_count: 1,
height: *height,
},
}
}
}
impl sum_tree::Summary for ListItemSummary {
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &()) {
self.count += summary.count;
self.rendered_count += summary.rendered_count;
self.unrendered_count += summary.unrendered_count;
self.height += summary.height;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.rendered_count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.unrendered_count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.height;
}
}
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
self.0.partial_cmp(&other.count).unwrap()
}
}
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
self.0.partial_cmp(&other.height).unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::vector::vec2f;
use rand::prelude::*;
use std::env;
#[crate::test(self)]
fn test_layout(cx: &mut crate::MutableAppContext) {
let mut presenter = cx.build_presenter(0, 0.);
let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
let elements = elements.clone();
move |ix, _| {
let (id, height) = elements.borrow()[ix];
TestElement::new(id, height).boxed()
}
});
let mut list = List::new(state.clone());
let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(false, cx));
assert_eq!(size, vec2f(100., 40.));
assert_eq!(
state.0.borrow().items.summary(),
ListItemSummary {
count: 3,
rendered_count: 3,
unrendered_count: 0,
height: 150.
}
);
state.0.borrow_mut().scroll(
&ListOffset {
item_ix: 0,
offset_in_item: 0.,
},
40.,
vec2f(0., -54.),
true,
&mut presenter.build_event_context(cx),
);
let (_, logical_scroll_top) =
list.layout(constraint, &mut presenter.build_layout_context(false, cx));
assert_eq!(
logical_scroll_top,
ListOffset {
item_ix: 2,
offset_in_item: 4.
}
);
assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
elements.borrow_mut().push((5, 60.));
state.splice(1..2, 2);
state.splice(4..4, 1);
assert_eq!(
state.0.borrow().items.summary(),
ListItemSummary {
count: 5,
rendered_count: 2,
unrendered_count: 3,
height: 120.
}
);
let (size, logical_scroll_top) =
list.layout(constraint, &mut presenter.build_layout_context(false, cx));
assert_eq!(size, vec2f(100., 40.));
assert_eq!(
state.0.borrow().items.summary(),
ListItemSummary {
count: 5,
rendered_count: 5,
unrendered_count: 0,
height: 270.
}
);
assert_eq!(
logical_scroll_top,
ListOffset {
item_ix: 3,
offset_in_item: 4.
}
);
assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
}
#[crate::test(self, iterations = 10, seed = 0)]
fn test_random(cx: &mut crate::MutableAppContext, mut rng: StdRng) {
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let mut presenter = cx.build_presenter(0, 0.);
let mut next_id = 0;
let elements = Rc::new(RefCell::new(
(0..rng.gen_range(0..=20))
.map(|_| {
let id = next_id;
next_id += 1;
(id, rng.gen_range(0..=200) as f32 / 2.0)
})
.collect::<Vec<_>>(),
));
let orientation = *[Orientation::Top, Orientation::Bottom]
.choose(&mut rng)
.unwrap();
let overdraw = rng.gen_range(1..=100) as f32;
let state = ListState::new(elements.borrow().len(), orientation, overdraw, {
let elements = elements.clone();
move |ix, _| {
let (id, height) = elements.borrow()[ix];
TestElement::new(id, height).boxed()
}
});
let mut width = rng.gen_range(0..=2000) as f32 / 2.;
let mut height = rng.gen_range(0..=2000) as f32 / 2.;
log::info!("orientation: {:?}", orientation);
log::info!("overdraw: {}", overdraw);
log::info!("elements: {:?}", elements.borrow());
log::info!("size: ({:?}, {:?})", width, height);
log::info!("==================");
let mut last_logical_scroll_top = None;
for _ in 0..operations {
match rng.gen_range(0..=100) {
0..=29 if last_logical_scroll_top.is_some() => {
let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
log::info!(
"Scrolling by {:?}, previous scroll top: {:?}",
delta,
last_logical_scroll_top.unwrap()
);
state.0.borrow_mut().scroll(
last_logical_scroll_top.as_ref().unwrap(),
height,
delta,
true,
&mut presenter.build_event_context(cx),
);
}
30..=34 => {
width = rng.gen_range(0..=2000) as f32 / 2.;
log::info!("changing width: {:?}", width);
}
35..=54 => {
height = rng.gen_range(0..=1000) as f32 / 2.;
log::info!("changing height: {:?}", height);
}
_ => {
let mut elements = elements.borrow_mut();
let end_ix = rng.gen_range(0..=elements.len());
let start_ix = rng.gen_range(0..=end_ix);
let new_elements = (0..rng.gen_range(0..10))
.map(|_| {
let id = next_id;
next_id += 1;
(id, rng.gen_range(0..=200) as f32 / 2.)
})
.collect::<Vec<_>>();
log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
state.splice(start_ix..end_ix, new_elements.len());
elements.splice(start_ix..end_ix, new_elements);
for (ix, item) in state.0.borrow().items.cursor::<()>().enumerate() {
if let ListItem::Rendered(element) = item {
let (expected_id, _) = elements[ix];
element.with_metadata(|metadata: Option<&usize>| {
assert_eq!(*metadata.unwrap(), expected_id);
});
}
}
}
}
let mut list = List::new(state.clone());
let (size, logical_scroll_top) = list.layout(
SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)),
&mut presenter.build_layout_context(false, cx),
);
assert_eq!(size, vec2f(width, height));
last_logical_scroll_top = Some(logical_scroll_top);
let state = state.0.borrow();
log::info!("items {:?}", state.items.items(&()));
let scroll_top = state.scroll_top(&logical_scroll_top);
let rendered_top = (scroll_top - overdraw).max(0.);
let rendered_bottom = scroll_top + height + overdraw;
let mut item_top = 0.;
log::info!(
"rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
rendered_top,
rendered_bottom,
scroll_top,
);
let mut first_rendered_element_top = None;
let mut last_rendered_element_bottom = None;
assert_eq!(state.items.summary().count, elements.borrow().len());
for (ix, item) in state.items.cursor::<()>().enumerate() {
match item {
ListItem::Unrendered => {
let item_bottom = item_top;
assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
item_top = item_bottom;
}
ListItem::Removed(height) => {
let (id, expected_height) = elements.borrow()[ix];
assert_eq!(
*height, expected_height,
"element {} height didn't match",
id
);
let item_bottom = item_top + height;
assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
item_top = item_bottom;
}
ListItem::Rendered(element) => {
let (expected_id, expected_height) = elements.borrow()[ix];
element.with_metadata(|metadata: Option<&usize>| {
assert_eq!(*metadata.unwrap(), expected_id);
});
assert_eq!(element.size().y(), expected_height);
let item_bottom = item_top + element.size().y();
first_rendered_element_top.get_or_insert(item_top);
last_rendered_element_bottom = Some(item_bottom);
assert!(item_bottom > rendered_top || item_top < rendered_bottom);
item_top = item_bottom;
}
}
}
match orientation {
Orientation::Top => {
if let Some(first_rendered_element_top) = first_rendered_element_top {
assert!(first_rendered_element_top <= scroll_top);
}
}
Orientation::Bottom => {
if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
assert!(last_rendered_element_bottom >= scroll_top + height);
}
}
}
}
}
struct TestElement {
id: usize,
size: Vector2F,
}
impl TestElement {
fn new(id: usize, height: f32) -> Self {
Self {
id,
size: vec2f(100., height),
}
}
}
impl Element for TestElement {
type LayoutState = ();
type PaintState = ();
fn layout(&mut self, _: SizeConstraint, _: &mut LayoutContext) -> (Vector2F, ()) {
(self.size, ())
}
fn paint(&mut self, _: RectF, _: RectF, _: &mut (), _: &mut PaintContext) {
todo!()
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut (),
_: &mut (),
_: &mut EventContext,
) -> bool {
todo!()
}
fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value {
self.id.into()
}
fn metadata(&self) -> Option<&dyn std::any::Any> {
Some(&self.id)
}
}
}

View file

@ -0,0 +1,208 @@
use super::Padding;
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
platform::CursorStyle,
CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, ElementStateId,
Event, EventContext, LayoutContext, MutableAppContext, PaintContext, SizeConstraint,
};
use serde_json::json;
use std::ops::DerefMut;
pub struct MouseEventHandler {
state: ElementStateHandle<MouseState>,
child: ElementBox,
cursor_style: Option<CursorStyle>,
mouse_down_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
click_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
drag_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
padding: Padding,
}
#[derive(Default)]
pub struct MouseState {
pub hovered: bool,
pub clicked: bool,
prev_drag_position: Option<Vector2F>,
cursor_style_handle: Option<CursorStyleHandle>,
}
impl MouseEventHandler {
pub fn new<Tag, F, C, Id>(id: Id, cx: &mut C, render_child: F) -> Self
where
Tag: 'static,
F: FnOnce(&MouseState, &mut C) -> ElementBox,
C: DerefMut<Target = MutableAppContext>,
Id: Into<ElementStateId>,
{
let state_handle = cx.element_state::<Tag, _>(id.into());
let child = state_handle.update(cx, |state, cx| render_child(state, cx));
Self {
state: state_handle,
child,
cursor_style: None,
mouse_down_handler: None,
click_handler: None,
drag_handler: None,
padding: Default::default(),
}
}
pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
self.cursor_style = Some(cursor);
self
}
pub fn on_mouse_down(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
self.mouse_down_handler = Some(Box::new(handler));
self
}
pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
self.click_handler = Some(Box::new(handler));
self
}
pub fn on_drag(mut self, handler: impl FnMut(Vector2F, &mut EventContext) + 'static) -> Self {
self.drag_handler = Some(Box::new(handler));
self
}
pub fn with_padding(mut self, padding: Padding) -> Self {
self.padding = padding;
self
}
}
impl Element for MouseEventHandler {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, cx), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
let cursor_style = self.cursor_style;
let mouse_down_handler = self.mouse_down_handler.as_mut();
let click_handler = self.click_handler.as_mut();
let drag_handler = self.drag_handler.as_mut();
let handled_in_child = self.child.dispatch_event(event, cx);
let hit_bounds = RectF::from_points(
bounds.origin() - vec2f(self.padding.left, self.padding.top),
bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
)
.round_out();
self.state.update(cx, |state, cx| match event {
Event::MouseMoved {
position,
left_mouse_down,
} => {
if !left_mouse_down {
let mouse_in = hit_bounds.contains_point(*position);
if state.hovered != mouse_in {
state.hovered = mouse_in;
if let Some(cursor_style) = cursor_style {
if !state.clicked {
if state.hovered {
state.cursor_style_handle =
Some(cx.set_cursor_style(cursor_style));
} else {
state.cursor_style_handle = None;
}
}
}
cx.notify();
return true;
}
}
handled_in_child
}
Event::LeftMouseDown { position, .. } => {
if !handled_in_child && hit_bounds.contains_point(*position) {
state.clicked = true;
state.prev_drag_position = Some(*position);
cx.notify();
if let Some(handler) = mouse_down_handler {
handler(cx);
}
true
} else {
handled_in_child
}
}
Event::LeftMouseUp { position, .. } => {
state.prev_drag_position = None;
if !handled_in_child && state.clicked {
state.clicked = false;
if !state.hovered {
state.cursor_style_handle = None;
}
cx.notify();
if let Some(handler) = click_handler {
if hit_bounds.contains_point(*position) {
handler(cx);
}
}
true
} else {
handled_in_child
}
}
Event::LeftMouseDragged { position, .. } => {
if !handled_in_child && state.clicked {
let prev_drag_position = state.prev_drag_position.replace(*position);
if let Some((handler, prev_position)) = drag_handler.zip(prev_drag_position) {
let delta = *position - prev_position;
if !delta.is_zero() {
(handler)(delta, cx);
}
}
true
} else {
handled_in_child
}
}
_ => handled_in_child,
})
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value {
json!({
"type": "MouseEventHandler",
"child": self.child.debug(cx),
})
}
}

View file

@ -0,0 +1,63 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
pub struct Overlay {
child: ElementBox,
}
impl Overlay {
pub fn new(child: ElementBox) -> Self {
Self { child }
}
}
impl Element for Overlay {
type LayoutState = Vector2F;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
(Vector2F::zero(), size)
}
fn paint(
&mut self,
bounds: RectF,
_: RectF,
size: &mut Self::LayoutState,
cx: &mut PaintContext,
) {
let bounds = RectF::new(bounds.origin(), *size);
cx.scene.push_stacking_context(None);
self.child.paint(bounds.origin(), bounds, cx);
cx.scene.pop_stacking_context();
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value {
self.child.debug(cx)
}
}

View file

@ -0,0 +1,85 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::{self, json, ToJson},
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
pub struct Stack {
children: Vec<ElementBox>,
}
impl Stack {
pub fn new() -> Self {
Stack {
children: Vec::new(),
}
}
}
impl Element for Stack {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.min;
for child in &mut self.children {
size = size.max(child.layout(constraint, cx));
}
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
for child in &mut self.children {
cx.scene.push_layer(None);
child.paint(bounds.origin(), visible_bounds, cx);
cx.scene.pop_layer();
}
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
for child in self.children.iter_mut().rev() {
if child.dispatch_event(event, cx) {
return true;
}
}
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> json::Value {
json!({
"type": "Stack",
"bounds": bounds.to_json(),
"children": self.children.iter().map(|child| child.debug(cx)).collect::<Vec<json::Value>>()
})
}
}
impl Extend<ElementBox> for Stack {
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
self.children.extend(children)
}
}

View file

@ -0,0 +1,111 @@
use std::borrow::Cow;
use serde_json::json;
use crate::{
color::Color,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
scene, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
pub struct Svg {
path: Cow<'static, str>,
color: Color,
}
impl Svg {
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
Self {
path: path.into(),
color: Color::black(),
}
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
}
impl Element for Svg {
type LayoutState = Option<usvg::Tree>;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
match cx.asset_cache.svg(&self.path) {
Ok(tree) => {
let size = constrain_size_preserving_aspect_ratio(
constraint.max,
from_usvg_rect(tree.svg_node().view_box.rect).size(),
);
(size, Some(tree))
}
Err(_error) => {
#[cfg(not(any(test, feature = "test-support")))]
log::error!("{}", _error);
(constraint.min, None)
}
}
}
fn paint(
&mut self,
bounds: RectF,
_visible_bounds: RectF,
svg: &mut Self::LayoutState,
cx: &mut PaintContext,
) {
if let Some(svg) = svg.clone() {
cx.scene.push_icon(scene::Icon {
bounds,
svg,
path: self.path.clone(),
color: self.color,
});
}
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "Svg",
"bounds": bounds.to_json(),
"path": self.path,
"color": self.color.to_json(),
})
}
}
use crate::json::ToJson;
use super::constrain_size_preserving_aspect_ratio;
fn from_usvg_rect(rect: usvg::Rect) -> RectF {
RectF::new(
vec2f(rect.x() as f32, rect.y() as f32),
vec2f(rect.width() as f32, rect.height() as f32),
)
}

View file

@ -0,0 +1,131 @@
use crate::{
color::Color,
fonts::TextStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{ToJson, Value},
text_layout::{Line, ShapedBoundary},
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use serde_json::json;
pub struct Text {
text: String,
style: TextStyle,
}
pub struct LayoutState {
lines: Vec<(Line, Vec<ShapedBoundary>)>,
line_height: f32,
}
impl Text {
pub fn new(text: String, style: TextStyle) -> Self {
Self { text, style }
}
pub fn with_default_color(mut self, color: Color) -> Self {
self.style.color = color;
self
}
}
impl Element for Text {
type LayoutState = LayoutState;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let font_id = self.style.font_id;
let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
let mut lines = Vec::new();
let mut line_count = 0;
let mut max_line_width = 0_f32;
for line in self.text.lines() {
let shaped_line = cx.text_layout_cache.layout_str(
line,
self.style.font_size,
&[(line.len(), self.style.to_run())],
);
let wrap_boundaries = wrapper
.wrap_shaped_line(line, &shaped_line, constraint.max.x())
.collect::<Vec<_>>();
max_line_width = max_line_width.max(shaped_line.width());
line_count += wrap_boundaries.len() + 1;
lines.push((shaped_line, wrap_boundaries));
}
let size = vec2f(
max_line_width
.ceil()
.max(constraint.min.x())
.min(constraint.max.x()),
(line_height * line_count as f32).ceil(),
);
(size, LayoutState { lines, line_height })
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let mut origin = bounds.origin();
for (line, wrap_boundaries) in &layout.lines {
let wrapped_line_boundaries = RectF::new(
origin,
vec2f(
bounds.width(),
(wrap_boundaries.len() + 1) as f32 * layout.line_height,
),
);
if wrapped_line_boundaries.intersects(visible_bounds) {
line.paint_wrapped(
origin,
visible_bounds,
layout.line_height,
wrap_boundaries.iter().copied(),
cx,
);
}
origin.set_y(wrapped_line_boundaries.max_y());
}
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> Value {
json!({
"type": "Text",
"bounds": bounds.to_json(),
"text": &self.text,
"style": self.style.to_json(),
})
}
}

View file

@ -0,0 +1,256 @@
use super::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{self, json},
ElementBox,
};
use json::ToJson;
use parking_lot::Mutex;
use std::{cmp, ops::Range, sync::Arc};
#[derive(Clone, Default)]
pub struct UniformListState(Arc<Mutex<StateInner>>);
impl UniformListState {
pub fn scroll_to(&self, item_ix: usize) {
self.0.lock().scroll_to = Some(item_ix);
}
pub fn scroll_top(&self) -> f32 {
self.0.lock().scroll_top
}
}
#[derive(Default)]
struct StateInner {
scroll_top: f32,
scroll_to: Option<usize>,
}
pub struct LayoutState {
scroll_max: f32,
item_height: f32,
items: Vec<ElementBox>,
}
pub struct UniformList<F>
where
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
{
state: UniformListState,
item_count: usize,
append_items: F,
padding_top: f32,
padding_bottom: f32,
}
impl<F> UniformList<F>
where
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
{
pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self {
Self {
state,
item_count,
append_items,
padding_top: 0.,
padding_bottom: 0.,
}
}
pub fn with_padding_top(mut self, padding: f32) -> Self {
self.padding_top = padding;
self
}
pub fn with_padding_bottom(mut self, padding: f32) -> Self {
self.padding_bottom = padding;
self
}
fn scroll(
&self,
_: Vector2F,
mut delta: Vector2F,
precise: bool,
scroll_max: f32,
cx: &mut EventContext,
) -> bool {
if !precise {
delta *= 20.;
}
let mut state = self.state.0.lock();
state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
cx.notify();
true
}
fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
let mut state = self.state.0.lock();
if state.scroll_top > scroll_max {
state.scroll_top = scroll_max;
}
if let Some(item_ix) = state.scroll_to.take() {
let item_top = self.padding_top + item_ix as f32 * item_height;
let item_bottom = item_top + item_height;
if item_top < state.scroll_top {
state.scroll_top = item_top;
} else if item_bottom > (state.scroll_top + list_height) {
state.scroll_top = item_bottom - list_height;
}
}
}
fn scroll_top(&self) -> f32 {
self.state.0.lock().scroll_top
}
}
impl<F> Element for UniformList<F>
where
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
{
type LayoutState = LayoutState;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
if constraint.max.y().is_infinite() {
unimplemented!(
"UniformList does not support being rendered with an unconstrained height"
);
}
let mut size = constraint.max;
let mut item_constraint =
SizeConstraint::new(vec2f(size.x(), 0.0), vec2f(size.x(), f32::INFINITY));
let mut item_height = 0.;
let mut scroll_max = 0.;
let mut items = Vec::new();
(self.append_items)(0..1, &mut items, cx);
if let Some(first_item) = items.first_mut() {
let mut item_size = first_item.layout(item_constraint, cx);
item_size.set_x(size.x());
item_constraint.min = item_size;
item_constraint.max = item_size;
item_height = item_size.y();
let scroll_height = self.item_count as f32 * item_height;
if scroll_height < size.y() {
size.set_y(size.y().min(scroll_height).max(constraint.min.y()));
}
let scroll_height =
item_height * self.item_count as f32 + self.padding_top + self.padding_bottom;
scroll_max = (scroll_height - size.y()).max(0.);
self.autoscroll(scroll_max, size.y(), item_height);
items.clear();
let start = cmp::min(
((self.scroll_top() - self.padding_top) / item_height) as usize,
self.item_count,
);
let end = cmp::min(
self.item_count,
start + (size.y() / item_height).ceil() as usize + 1,
);
(self.append_items)(start..end, &mut items, cx);
for item in &mut items {
item.layout(item_constraint, cx);
}
} else {
size = constraint.min;
}
(
size,
LayoutState {
item_height,
scroll_max,
items,
},
)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_layer(Some(bounds));
let mut item_origin = bounds.origin()
- vec2f(
0.,
(self.state.scroll_top() - self.padding_top) % layout.item_height,
);
for item in &mut layout.items {
item.paint(item_origin, visible_bounds, cx);
item_origin += vec2f(0.0, layout.item_height);
}
cx.scene.pop_layer();
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
layout: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
let mut handled = false;
for item in &mut layout.items {
handled = item.dispatch_event(event, cx) || handled;
}
match event {
Event::ScrollWheel {
position,
delta,
precise,
} => {
if bounds.contains_point(*position) {
if self.scroll(*position, *delta, *precise, layout.scroll_max, cx) {
handled = true;
}
}
}
_ => {}
}
handled
}
fn debug(
&self,
bounds: RectF,
layout: &Self::LayoutState,
_: &Self::PaintState,
cx: &crate::DebugContext,
) -> json::Value {
json!({
"type": "UniformList",
"bounds": bounds.to_json(),
"scroll_max": layout.scroll_max,
"item_height": layout.item_height,
"items": layout.items.iter().map(|item| item.debug(cx)).collect::<Vec<json::Value>>()
})
}
}

656
crates/gpui/src/executor.rs Normal file
View file

@ -0,0 +1,656 @@
use anyhow::{anyhow, Result};
use async_task::Runnable;
use backtrace::{Backtrace, BacktraceFmt, BytesOrWideString};
use parking_lot::Mutex;
use postage::{barrier, prelude::Stream as _};
use rand::prelude::*;
use smol::{channel, prelude::*, Executor, Timer};
use std::{
any::Any,
fmt::{self, Debug},
marker::PhantomData,
mem,
ops::RangeInclusive,
pin::Pin,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
task::{Context, Poll},
thread,
time::{Duration, Instant},
};
use waker_fn::waker_fn;
use crate::{
platform::{self, Dispatcher},
util,
};
pub enum Foreground {
Platform {
dispatcher: Arc<dyn platform::Dispatcher>,
_not_send_or_sync: PhantomData<Rc<()>>,
},
Test(smol::LocalExecutor<'static>),
Deterministic(Arc<Deterministic>),
}
pub enum Background {
Deterministic(Arc<Deterministic>),
Production {
executor: Arc<smol::Executor<'static>>,
_stop: channel::Sender<()>,
},
}
type AnyLocalFuture = Pin<Box<dyn 'static + Future<Output = Box<dyn Any + 'static>>>>;
type AnyFuture = Pin<Box<dyn 'static + Send + Future<Output = Box<dyn Any + Send + 'static>>>>;
type AnyTask = async_task::Task<Box<dyn Any + Send + 'static>>;
type AnyLocalTask = async_task::Task<Box<dyn Any + 'static>>;
pub enum Task<T> {
Local {
any_task: AnyLocalTask,
result_type: PhantomData<T>,
},
Send {
any_task: AnyTask,
result_type: PhantomData<T>,
},
}
unsafe impl<T: Send> Send for Task<T> {}
struct DeterministicState {
rng: StdRng,
seed: u64,
scheduled_from_foreground: Vec<(Runnable, Backtrace)>,
scheduled_from_background: Vec<(Runnable, Backtrace)>,
spawned_from_foreground: Vec<(Runnable, Backtrace)>,
forbid_parking: bool,
block_on_ticks: RangeInclusive<usize>,
now: Instant,
pending_timers: Vec<(Instant, barrier::Sender)>,
}
pub struct Deterministic {
state: Arc<Mutex<DeterministicState>>,
parker: Mutex<parking::Parker>,
}
impl Deterministic {
fn new(seed: u64) -> Self {
Self {
state: Arc::new(Mutex::new(DeterministicState {
rng: StdRng::seed_from_u64(seed),
seed,
scheduled_from_foreground: Default::default(),
scheduled_from_background: Default::default(),
spawned_from_foreground: Default::default(),
forbid_parking: false,
block_on_ticks: 0..=1000,
now: Instant::now(),
pending_timers: Default::default(),
})),
parker: Default::default(),
}
}
fn spawn_from_foreground(&self, future: AnyLocalFuture) -> AnyLocalTask {
let backtrace = Backtrace::new_unresolved();
let scheduled_once = AtomicBool::new(false);
let state = self.state.clone();
let unparker = self.parker.lock().unparker();
let (runnable, task) = async_task::spawn_local(future, move |runnable| {
let mut state = state.lock();
let backtrace = backtrace.clone();
if scheduled_once.fetch_or(true, SeqCst) {
state.scheduled_from_foreground.push((runnable, backtrace));
} else {
state.spawned_from_foreground.push((runnable, backtrace));
}
unparker.unpark();
});
runnable.schedule();
task
}
fn spawn(&self, future: AnyFuture) -> AnyTask {
let backtrace = Backtrace::new_unresolved();
let state = self.state.clone();
let unparker = self.parker.lock().unparker();
let (runnable, task) = async_task::spawn(future, move |runnable| {
let mut state = state.lock();
state
.scheduled_from_background
.push((runnable, backtrace.clone()));
unparker.unpark();
});
runnable.schedule();
task
}
fn run(&self, mut future: AnyLocalFuture) -> Box<dyn Any> {
let woken = Arc::new(AtomicBool::new(false));
loop {
if let Some(result) = self.run_internal(woken.clone(), &mut future) {
return result;
}
if !woken.load(SeqCst) && self.state.lock().forbid_parking {
panic!("deterministic executor parked after a call to forbid_parking");
}
woken.store(false, SeqCst);
self.parker.lock().park();
}
}
fn run_until_parked(&self) {
let woken = Arc::new(AtomicBool::new(false));
let mut future = any_local_future(std::future::pending::<()>());
self.run_internal(woken, &mut future);
}
fn run_internal(
&self,
woken: Arc<AtomicBool>,
future: &mut AnyLocalFuture,
) -> Option<Box<dyn Any>> {
let unparker = self.parker.lock().unparker();
let waker = waker_fn(move || {
woken.store(true, SeqCst);
unparker.unpark();
});
let mut cx = Context::from_waker(&waker);
let mut trace = Trace::default();
loop {
let mut state = self.state.lock();
let runnable_count = state.scheduled_from_foreground.len()
+ state.scheduled_from_background.len()
+ state.spawned_from_foreground.len();
let ix = state.rng.gen_range(0..=runnable_count);
if ix < state.scheduled_from_foreground.len() {
let (_, backtrace) = &state.scheduled_from_foreground[ix];
trace.record(&state, backtrace.clone());
let runnable = state.scheduled_from_foreground.remove(ix).0;
drop(state);
runnable.run();
} else if ix - state.scheduled_from_foreground.len()
< state.scheduled_from_background.len()
{
let ix = ix - state.scheduled_from_foreground.len();
let (_, backtrace) = &state.scheduled_from_background[ix];
trace.record(&state, backtrace.clone());
let runnable = state.scheduled_from_background.remove(ix).0;
drop(state);
runnable.run();
} else if ix < runnable_count {
let (_, backtrace) = &state.spawned_from_foreground[0];
trace.record(&state, backtrace.clone());
let runnable = state.spawned_from_foreground.remove(0).0;
drop(state);
runnable.run();
} else {
drop(state);
if let Poll::Ready(result) = future.poll(&mut cx) {
return Some(result);
}
let state = self.state.lock();
if state.scheduled_from_foreground.is_empty()
&& state.scheduled_from_background.is_empty()
&& state.spawned_from_foreground.is_empty()
{
return None;
}
}
}
}
fn block_on(&self, future: &mut AnyLocalFuture) -> Option<Box<dyn Any>> {
let unparker = self.parker.lock().unparker();
let waker = waker_fn(move || {
unparker.unpark();
});
let max_ticks = {
let mut state = self.state.lock();
let range = state.block_on_ticks.clone();
state.rng.gen_range(range)
};
let mut cx = Context::from_waker(&waker);
let mut trace = Trace::default();
for _ in 0..max_ticks {
let mut state = self.state.lock();
let runnable_count = state.scheduled_from_background.len();
let ix = state.rng.gen_range(0..=runnable_count);
if ix < state.scheduled_from_background.len() {
let (_, backtrace) = &state.scheduled_from_background[ix];
trace.record(&state, backtrace.clone());
let runnable = state.scheduled_from_background.remove(ix).0;
drop(state);
runnable.run();
} else {
drop(state);
if let Poll::Ready(result) = future.as_mut().poll(&mut cx) {
return Some(result);
}
let state = self.state.lock();
if state.scheduled_from_background.is_empty() {
if state.forbid_parking {
panic!("deterministic executor parked after a call to forbid_parking");
}
drop(state);
self.parker.lock().park();
}
continue;
}
}
None
}
}
#[derive(Default)]
struct Trace {
executed: Vec<Backtrace>,
scheduled: Vec<Vec<Backtrace>>,
spawned_from_foreground: Vec<Vec<Backtrace>>,
}
impl Trace {
fn record(&mut self, state: &DeterministicState, executed: Backtrace) {
self.scheduled.push(
state
.scheduled_from_foreground
.iter()
.map(|(_, backtrace)| backtrace.clone())
.collect(),
);
self.spawned_from_foreground.push(
state
.spawned_from_foreground
.iter()
.map(|(_, backtrace)| backtrace.clone())
.collect(),
);
self.executed.push(executed);
}
fn resolve(&mut self) {
for backtrace in &mut self.executed {
backtrace.resolve();
}
for backtraces in &mut self.scheduled {
for backtrace in backtraces {
backtrace.resolve();
}
}
for backtraces in &mut self.spawned_from_foreground {
for backtrace in backtraces {
backtrace.resolve();
}
}
}
}
impl Debug for Trace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct FirstCwdFrameInBacktrace<'a>(&'a Backtrace);
impl<'a> Debug for FirstCwdFrameInBacktrace<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
let cwd = std::env::current_dir().unwrap();
let mut print_path = |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
fmt::Display::fmt(&path, fmt)
};
let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path);
for frame in self.0.frames() {
let mut formatted_frame = fmt.frame();
if frame
.symbols()
.iter()
.any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd)))
{
formatted_frame.backtrace_frame(frame)?;
break;
}
}
fmt.finish()
}
}
for ((backtrace, scheduled), spawned_from_foreground) in self
.executed
.iter()
.zip(&self.scheduled)
.zip(&self.spawned_from_foreground)
{
writeln!(f, "Scheduled")?;
for backtrace in scheduled {
writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?;
}
if scheduled.is_empty() {
writeln!(f, "None")?;
}
writeln!(f, "==========")?;
writeln!(f, "Spawned from foreground")?;
for backtrace in spawned_from_foreground {
writeln!(f, "- {:?}", FirstCwdFrameInBacktrace(backtrace))?;
}
if spawned_from_foreground.is_empty() {
writeln!(f, "None")?;
}
writeln!(f, "==========")?;
writeln!(f, "Run: {:?}", FirstCwdFrameInBacktrace(backtrace))?;
writeln!(f, "+++++++++++++++++++")?;
}
Ok(())
}
}
impl Drop for Trace {
fn drop(&mut self) {
let trace_on_panic = if let Ok(trace_on_panic) = std::env::var("EXECUTOR_TRACE_ON_PANIC") {
trace_on_panic == "1" || trace_on_panic == "true"
} else {
false
};
let trace_always = if let Ok(trace_always) = std::env::var("EXECUTOR_TRACE_ALWAYS") {
trace_always == "1" || trace_always == "true"
} else {
false
};
if trace_always || (trace_on_panic && thread::panicking()) {
self.resolve();
dbg!(self);
}
}
}
impl Foreground {
pub fn platform(dispatcher: Arc<dyn platform::Dispatcher>) -> Result<Self> {
if dispatcher.is_main_thread() {
Ok(Self::Platform {
dispatcher,
_not_send_or_sync: PhantomData,
})
} else {
Err(anyhow!("must be constructed on main thread"))
}
}
pub fn test() -> Self {
Self::Test(smol::LocalExecutor::new())
}
pub fn spawn<T: 'static>(&self, future: impl Future<Output = T> + 'static) -> Task<T> {
let future = any_local_future(future);
let any_task = match self {
Self::Deterministic(executor) => executor.spawn_from_foreground(future),
Self::Platform { dispatcher, .. } => {
fn spawn_inner(
future: AnyLocalFuture,
dispatcher: &Arc<dyn Dispatcher>,
) -> AnyLocalTask {
let dispatcher = dispatcher.clone();
let schedule =
move |runnable: Runnable| dispatcher.run_on_main_thread(runnable);
let (runnable, task) = async_task::spawn_local(future, schedule);
runnable.schedule();
task
}
spawn_inner(future, dispatcher)
}
Self::Test(executor) => executor.spawn(future),
};
Task::local(any_task)
}
pub fn run<T: 'static>(&self, future: impl 'static + Future<Output = T>) -> T {
let future = any_local_future(future);
let any_value = match self {
Self::Deterministic(executor) => executor.run(future),
Self::Platform { .. } => panic!("you can't call run on a platform foreground executor"),
Self::Test(executor) => smol::block_on(executor.run(future)),
};
*any_value.downcast().unwrap()
}
pub fn forbid_parking(&self) {
match self {
Self::Deterministic(executor) => {
let mut state = executor.state.lock();
state.forbid_parking = true;
state.rng = StdRng::seed_from_u64(state.seed);
}
_ => panic!("this method can only be called on a deterministic executor"),
}
}
pub async fn timer(&self, duration: Duration) {
match self {
Self::Deterministic(executor) => {
let (tx, mut rx) = barrier::channel();
{
let mut state = executor.state.lock();
let wakeup_at = state.now + duration;
state.pending_timers.push((wakeup_at, tx));
}
rx.recv().await;
}
_ => {
Timer::after(duration).await;
}
}
}
pub fn advance_clock(&self, duration: Duration) {
match self {
Self::Deterministic(executor) => {
executor.run_until_parked();
let mut state = executor.state.lock();
state.now += duration;
let now = state.now;
let mut pending_timers = mem::take(&mut state.pending_timers);
drop(state);
pending_timers.retain(|(wakeup, _)| *wakeup > now);
executor.state.lock().pending_timers.extend(pending_timers);
}
_ => panic!("this method can only be called on a deterministic executor"),
}
}
pub fn set_block_on_ticks(&self, range: RangeInclusive<usize>) {
match self {
Self::Deterministic(executor) => executor.state.lock().block_on_ticks = range,
_ => panic!("this method can only be called on a deterministic executor"),
}
}
}
impl Background {
pub fn new() -> Self {
let executor = Arc::new(Executor::new());
let stop = channel::unbounded::<()>();
for i in 0..2 * num_cpus::get() {
let executor = executor.clone();
let stop = stop.1.clone();
thread::Builder::new()
.name(format!("background-executor-{}", i))
.spawn(move || smol::block_on(executor.run(stop.recv())))
.unwrap();
}
Self::Production {
executor,
_stop: stop.0,
}
}
pub fn num_cpus(&self) -> usize {
num_cpus::get()
}
pub fn spawn<T, F>(&self, future: F) -> Task<T>
where
T: 'static + Send,
F: Send + Future<Output = T> + 'static,
{
let future = any_future(future);
let any_task = match self {
Self::Production { executor, .. } => executor.spawn(future),
Self::Deterministic(executor) => executor.spawn(future),
};
Task::send(any_task)
}
pub fn block_with_timeout<F, T>(
&self,
timeout: Duration,
future: F,
) -> Result<T, impl Future<Output = T>>
where
T: 'static,
F: 'static + Unpin + Future<Output = T>,
{
let mut future = any_local_future(future);
if !timeout.is_zero() {
let output = match self {
Self::Production { .. } => smol::block_on(util::timeout(timeout, &mut future)).ok(),
Self::Deterministic(executor) => executor.block_on(&mut future),
};
if let Some(output) = output {
return Ok(*output.downcast().unwrap());
}
}
Err(async { *future.await.downcast().unwrap() })
}
pub async fn scoped<'scope, F>(&self, scheduler: F)
where
F: FnOnce(&mut Scope<'scope>),
{
let mut scope = Scope {
futures: Default::default(),
_phantom: PhantomData,
};
(scheduler)(&mut scope);
let spawned = scope
.futures
.into_iter()
.map(|f| self.spawn(f))
.collect::<Vec<_>>();
for task in spawned {
task.await;
}
}
}
pub struct Scope<'a> {
futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
_phantom: PhantomData<&'a ()>,
}
impl<'a> Scope<'a> {
pub fn spawn<F>(&mut self, f: F)
where
F: Future<Output = ()> + Send + 'a,
{
let f = unsafe {
mem::transmute::<
Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
>(Box::pin(f))
};
self.futures.push(f);
}
}
pub fn deterministic(seed: u64) -> (Rc<Foreground>, Arc<Background>) {
let executor = Arc::new(Deterministic::new(seed));
(
Rc::new(Foreground::Deterministic(executor.clone())),
Arc::new(Background::Deterministic(executor)),
)
}
impl<T> Task<T> {
fn local(any_task: AnyLocalTask) -> Self {
Self::Local {
any_task,
result_type: PhantomData,
}
}
pub fn detach(self) {
match self {
Task::Local { any_task, .. } => any_task.detach(),
Task::Send { any_task, .. } => any_task.detach(),
}
}
}
impl<T: Send> Task<T> {
fn send(any_task: AnyTask) -> Self {
Self::Send {
any_task,
result_type: PhantomData,
}
}
}
impl<T: fmt::Debug> fmt::Debug for Task<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Task::Local { any_task, .. } => any_task.fmt(f),
Task::Send { any_task, .. } => any_task.fmt(f),
}
}
}
impl<T: 'static> Future for Task<T> {
type Output = T;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match unsafe { self.get_unchecked_mut() } {
Task::Local { any_task, .. } => {
any_task.poll(cx).map(|value| *value.downcast().unwrap())
}
Task::Send { any_task, .. } => {
any_task.poll(cx).map(|value| *value.downcast().unwrap())
}
}
}
}
fn any_future<T, F>(future: F) -> AnyFuture
where
T: 'static + Send,
F: Future<Output = T> + Send + 'static,
{
async { Box::new(future.await) as Box<dyn Any + Send> }.boxed()
}
fn any_local_future<T, F>(future: F) -> AnyLocalFuture
where
T: 'static,
F: Future<Output = T> + 'static,
{
async { Box::new(future.await) as Box<dyn Any> }.boxed_local()
}

View file

@ -0,0 +1,259 @@
use crate::{
fonts::{FontId, Metrics, Properties},
geometry::vector::{vec2f, Vector2F},
platform,
text_layout::LineWrapper,
};
use anyhow::{anyhow, Result};
use ordered_float::OrderedFloat;
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use std::{
collections::HashMap,
ops::{Deref, DerefMut},
sync::Arc,
};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct FamilyId(usize);
struct Family {
name: Arc<str>,
font_ids: Vec<FontId>,
}
pub struct FontCache(RwLock<FontCacheState>);
pub struct FontCacheState {
fonts: Arc<dyn platform::FontSystem>,
families: Vec<Family>,
font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
metrics: HashMap<FontId, Metrics>,
wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
}
pub struct LineWrapperHandle {
wrapper: Option<LineWrapper>,
font_cache: Arc<FontCache>,
}
unsafe impl Send for FontCache {}
impl FontCache {
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
Self(RwLock::new(FontCacheState {
fonts,
families: Default::default(),
font_selections: Default::default(),
metrics: Default::default(),
wrapper_pool: Default::default(),
}))
}
pub fn family_name(&self, family_id: FamilyId) -> Result<Arc<str>> {
self.0
.read()
.families
.get(family_id.0)
.ok_or_else(|| anyhow!("invalid family id"))
.map(|family| family.name.clone())
}
pub fn load_family(&self, names: &[&str]) -> Result<FamilyId> {
for name in names {
let state = self.0.upgradable_read();
if let Some(ix) = state.families.iter().position(|f| f.name.as_ref() == *name) {
return Ok(FamilyId(ix));
}
let mut state = RwLockUpgradableReadGuard::upgrade(state);
if let Ok(font_ids) = state.fonts.load_family(name) {
if font_ids.is_empty() {
continue;
}
let family_id = FamilyId(state.families.len());
for font_id in &font_ids {
if state.fonts.glyph_for_char(*font_id, 'm').is_none() {
return Err(anyhow!("font must contain a glyph for the 'm' character"));
}
}
state.families.push(Family {
name: Arc::from(*name),
font_ids,
});
return Ok(family_id);
}
}
Err(anyhow!(
"could not find a non-empty font family matching one of the given names"
))
}
pub fn default_font(&self, family_id: FamilyId) -> FontId {
self.select_font(family_id, &Properties::default()).unwrap()
}
pub fn select_font(&self, family_id: FamilyId, properties: &Properties) -> Result<FontId> {
let inner = self.0.upgradable_read();
if let Some(font_id) = inner
.font_selections
.get(&family_id)
.and_then(|f| f.get(properties))
{
Ok(*font_id)
} else {
let mut inner = RwLockUpgradableReadGuard::upgrade(inner);
let family = &inner.families[family_id.0];
let font_id = inner
.fonts
.select_font(&family.font_ids, properties)
.unwrap_or(family.font_ids[0]);
inner
.font_selections
.entry(family_id)
.or_default()
.insert(properties.clone(), font_id);
Ok(font_id)
}
}
pub fn metric<F, T>(&self, font_id: FontId, f: F) -> T
where
F: FnOnce(&Metrics) -> T,
T: 'static,
{
let state = self.0.upgradable_read();
if let Some(metrics) = state.metrics.get(&font_id) {
f(metrics)
} else {
let metrics = state.fonts.font_metrics(font_id);
let metric = f(&metrics);
let mut state = RwLockUpgradableReadGuard::upgrade(state);
state.metrics.insert(font_id, metrics);
metric
}
}
pub fn bounding_box(&self, font_id: FontId, font_size: f32) -> Vector2F {
let bounding_box = self.metric(font_id, |m| m.bounding_box);
let width = bounding_box.width() * self.em_scale(font_id, font_size);
let height = bounding_box.height() * self.em_scale(font_id, font_size);
vec2f(width, height)
}
pub fn em_width(&self, font_id: FontId, font_size: f32) -> f32 {
let glyph_id;
let bounds;
{
let state = self.0.read();
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
}
bounds.width() * self.em_scale(font_id, font_size)
}
pub fn line_height(&self, font_id: FontId, font_size: f32) -> f32 {
let height = self.metric(font_id, |m| m.bounding_box.height());
(height * self.em_scale(font_id, font_size)).ceil()
}
pub fn cap_height(&self, font_id: FontId, font_size: f32) -> f32 {
self.metric(font_id, |m| m.cap_height) * self.em_scale(font_id, font_size)
}
pub fn x_height(&self, font_id: FontId, font_size: f32) -> f32 {
self.metric(font_id, |m| m.x_height) * self.em_scale(font_id, font_size)
}
pub fn ascent(&self, font_id: FontId, font_size: f32) -> f32 {
self.metric(font_id, |m| m.ascent) * self.em_scale(font_id, font_size)
}
pub fn descent(&self, font_id: FontId, font_size: f32) -> f32 {
self.metric(font_id, |m| -m.descent) * self.em_scale(font_id, font_size)
}
pub fn em_scale(&self, font_id: FontId, font_size: f32) -> f32 {
font_size / self.metric(font_id, |m| m.units_per_em as f32)
}
pub fn baseline_offset(&self, font_id: FontId, font_size: f32) -> f32 {
let line_height = self.line_height(font_id, font_size);
let ascent = self.ascent(font_id, font_size);
let descent = self.descent(font_id, font_size);
let padding_top = (line_height - ascent - descent) / 2.;
padding_top + ascent
}
pub fn line_wrapper(self: &Arc<Self>, font_id: FontId, font_size: f32) -> LineWrapperHandle {
let mut state = self.0.write();
let wrappers = state
.wrapper_pool
.entry((font_id, OrderedFloat(font_size)))
.or_default();
let wrapper = wrappers
.pop()
.unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.fonts.clone()));
LineWrapperHandle {
wrapper: Some(wrapper),
font_cache: self.clone(),
}
}
}
impl Drop for LineWrapperHandle {
fn drop(&mut self) {
let mut state = self.font_cache.0.write();
let wrapper = self.wrapper.take().unwrap();
state
.wrapper_pool
.get_mut(&(wrapper.font_id, OrderedFloat(wrapper.font_size)))
.unwrap()
.push(wrapper);
}
}
impl Deref for LineWrapperHandle {
type Target = LineWrapper;
fn deref(&self) -> &Self::Target {
self.wrapper.as_ref().unwrap()
}
}
impl DerefMut for LineWrapperHandle {
fn deref_mut(&mut self) -> &mut Self::Target {
self.wrapper.as_mut().unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
fonts::{Style, Weight},
platform::{test, Platform as _},
};
#[test]
fn test_select_font() {
let platform = test::platform();
let fonts = FontCache::new(platform.fonts());
let arial = fonts.load_family(&["Arial"]).unwrap();
let arial_regular = fonts.select_font(arial, &Properties::new()).unwrap();
let arial_italic = fonts
.select_font(arial, &Properties::new().style(Style::Italic))
.unwrap();
let arial_bold = fonts
.select_font(arial, &Properties::new().weight(Weight::BOLD))
.unwrap();
assert_ne!(arial_regular, arial_italic);
assert_ne!(arial_regular, arial_bold);
assert_ne!(arial_italic, arial_bold);
}
}

309
crates/gpui/src/fonts.rs Normal file
View file

@ -0,0 +1,309 @@
use crate::{
color::Color,
font_cache::FamilyId,
json::{json, ToJson},
text_layout::RunStyle,
FontCache,
};
use anyhow::anyhow;
pub use font_kit::{
metrics::Metrics,
properties::{Properties, Stretch, Style, Weight},
};
use serde::{de, Deserialize};
use serde_json::Value;
use std::{cell::RefCell, sync::Arc};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct FontId(pub usize);
pub type GlyphId = u32;
#[derive(Clone, Debug)]
pub struct TextStyle {
pub color: Color,
pub font_family_name: Arc<str>,
pub font_family_id: FamilyId,
pub font_id: FontId,
pub font_size: f32,
pub font_properties: Properties,
pub underline: bool,
}
#[derive(Clone, Debug, Default)]
pub struct HighlightStyle {
pub color: Color,
pub font_properties: Properties,
pub underline: bool,
}
#[allow(non_camel_case_types)]
#[derive(Deserialize)]
enum WeightJson {
thin,
extra_light,
light,
normal,
medium,
semibold,
bold,
extra_bold,
black,
}
thread_local! {
static FONT_CACHE: RefCell<Option<Arc<FontCache>>> = Default::default();
}
#[derive(Deserialize)]
struct TextStyleJson {
color: Color,
family: String,
weight: Option<WeightJson>,
size: f32,
#[serde(default)]
italic: bool,
#[serde(default)]
underline: bool,
}
#[derive(Deserialize)]
struct HighlightStyleJson {
color: Color,
weight: Option<WeightJson>,
#[serde(default)]
italic: bool,
#[serde(default)]
underline: bool,
}
impl TextStyle {
pub fn new(
font_family_name: impl Into<Arc<str>>,
font_size: f32,
font_properties: Properties,
underline: bool,
color: Color,
font_cache: &FontCache,
) -> anyhow::Result<Self> {
let font_family_name = font_family_name.into();
let font_family_id = font_cache.load_family(&[&font_family_name])?;
let font_id = font_cache.select_font(font_family_id, &font_properties)?;
Ok(Self {
color,
font_family_name,
font_family_id,
font_id,
font_size,
font_properties,
underline,
})
}
pub fn to_run(&self) -> RunStyle {
RunStyle {
font_id: self.font_id,
color: self.color,
underline: self.underline,
}
}
fn from_json(json: TextStyleJson) -> anyhow::Result<Self> {
FONT_CACHE.with(|font_cache| {
if let Some(font_cache) = font_cache.borrow().as_ref() {
let font_properties = properties_from_json(json.weight, json.italic);
Self::new(
json.family,
json.size,
font_properties,
json.underline,
json.color,
font_cache,
)
} else {
Err(anyhow!(
"TextStyle can only be deserialized within a call to with_font_cache"
))
}
})
}
pub fn line_height(&self, font_cache: &FontCache) -> f32 {
font_cache.line_height(self.font_id, self.font_size)
}
pub fn cap_height(&self, font_cache: &FontCache) -> f32 {
font_cache.cap_height(self.font_id, self.font_size)
}
pub fn x_height(&self, font_cache: &FontCache) -> f32 {
font_cache.x_height(self.font_id, self.font_size)
}
pub fn em_width(&self, font_cache: &FontCache) -> f32 {
font_cache.em_width(self.font_id, self.font_size)
}
pub fn descent(&self, font_cache: &FontCache) -> f32 {
font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
}
pub fn baseline_offset(&self, font_cache: &FontCache) -> f32 {
font_cache.baseline_offset(self.font_id, self.font_size)
}
fn em_scale(&self, font_cache: &FontCache) -> f32 {
font_cache.em_scale(self.font_id, self.font_size)
}
}
impl From<TextStyle> for HighlightStyle {
fn from(other: TextStyle) -> Self {
Self {
color: other.color,
font_properties: other.font_properties,
underline: other.underline,
}
}
}
impl HighlightStyle {
fn from_json(json: HighlightStyleJson) -> Self {
let font_properties = properties_from_json(json.weight, json.italic);
Self {
color: json.color,
font_properties,
underline: json.underline,
}
}
}
impl From<Color> for HighlightStyle {
fn from(color: Color) -> Self {
Self {
color,
font_properties: Default::default(),
underline: false,
}
}
}
impl<'de> Deserialize<'de> for TextStyle {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Self::from_json(TextStyleJson::deserialize(deserializer)?)
.map_err(|e| de::Error::custom(e))?)
}
}
impl ToJson for TextStyle {
fn to_json(&self) -> Value {
json!({
"color": self.color.to_json(),
"font_family": self.font_family_name.as_ref(),
"font_properties": self.font_properties.to_json(),
})
}
}
impl<'de> Deserialize<'de> for HighlightStyle {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let json = serde_json::Value::deserialize(deserializer)?;
if json.is_object() {
Ok(Self::from_json(
serde_json::from_value(json).map_err(de::Error::custom)?,
))
} else {
Ok(Self {
color: serde_json::from_value(json).map_err(de::Error::custom)?,
font_properties: Properties::new(),
underline: false,
})
}
}
}
fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
let weight = match weight.unwrap_or(WeightJson::normal) {
WeightJson::thin => Weight::THIN,
WeightJson::extra_light => Weight::EXTRA_LIGHT,
WeightJson::light => Weight::LIGHT,
WeightJson::normal => Weight::NORMAL,
WeightJson::medium => Weight::MEDIUM,
WeightJson::semibold => Weight::SEMIBOLD,
WeightJson::bold => Weight::BOLD,
WeightJson::extra_bold => Weight::EXTRA_BOLD,
WeightJson::black => Weight::BLACK,
};
let style = if italic { Style::Italic } else { Style::Normal };
*Properties::new().weight(weight).style(style)
}
impl ToJson for Properties {
fn to_json(&self) -> crate::json::Value {
json!({
"style": self.style.to_json(),
"weight": self.weight.to_json(),
"stretch": self.stretch.to_json(),
})
}
}
impl ToJson for Style {
fn to_json(&self) -> crate::json::Value {
match self {
Style::Normal => json!("normal"),
Style::Italic => json!("italic"),
Style::Oblique => json!("oblique"),
}
}
}
impl ToJson for Weight {
fn to_json(&self) -> crate::json::Value {
if self.0 == Weight::THIN.0 {
json!("thin")
} else if self.0 == Weight::EXTRA_LIGHT.0 {
json!("extra light")
} else if self.0 == Weight::LIGHT.0 {
json!("light")
} else if self.0 == Weight::NORMAL.0 {
json!("normal")
} else if self.0 == Weight::MEDIUM.0 {
json!("medium")
} else if self.0 == Weight::SEMIBOLD.0 {
json!("semibold")
} else if self.0 == Weight::BOLD.0 {
json!("bold")
} else if self.0 == Weight::EXTRA_BOLD.0 {
json!("extra bold")
} else if self.0 == Weight::BLACK.0 {
json!("black")
} else {
json!(self.0)
}
}
}
impl ToJson for Stretch {
fn to_json(&self) -> serde_json::Value {
json!(self.0)
}
}
pub fn with_font_cache<F, T>(font_cache: Arc<FontCache>, callback: F) -> T
where
F: FnOnce() -> T,
{
FONT_CACHE.with(|cache| {
*cache.borrow_mut() = Some(font_cache);
let result = callback();
cache.borrow_mut().take();
result
})
}

130
crates/gpui/src/geometry.rs Normal file
View file

@ -0,0 +1,130 @@
use super::scene::{Path, PathVertex};
use crate::{color::Color, json::ToJson};
pub use pathfinder_geometry::*;
use rect::RectF;
use serde::{Deserialize, Deserializer};
use serde_json::json;
use vector::{vec2f, Vector2F};
pub struct PathBuilder {
vertices: Vec<PathVertex>,
start: Vector2F,
current: Vector2F,
contour_count: usize,
bounds: RectF,
}
enum PathVertexKind {
Solid,
Quadratic,
}
impl PathBuilder {
pub fn new() -> Self {
Self {
vertices: Vec::new(),
start: vec2f(0., 0.),
current: vec2f(0., 0.),
contour_count: 0,
bounds: RectF::default(),
}
}
pub fn reset(&mut self, point: Vector2F) {
self.vertices.clear();
self.start = point;
self.current = point;
self.contour_count = 0;
}
pub fn line_to(&mut self, point: Vector2F) {
self.contour_count += 1;
if self.contour_count > 1 {
self.push_triangle(self.start, self.current, point, PathVertexKind::Solid);
}
self.current = point;
}
pub fn curve_to(&mut self, point: Vector2F, ctrl: Vector2F) {
self.contour_count += 1;
if self.contour_count > 1 {
self.push_triangle(self.start, self.current, point, PathVertexKind::Solid);
}
self.push_triangle(self.current, ctrl, point, PathVertexKind::Quadratic);
self.current = point;
}
pub fn build(mut self, color: Color, clip_bounds: Option<RectF>) -> Path {
if let Some(clip_bounds) = clip_bounds {
self.bounds = self
.bounds
.intersection(clip_bounds)
.unwrap_or(RectF::default());
}
Path {
bounds: self.bounds,
color,
vertices: self.vertices,
}
}
fn push_triangle(&mut self, a: Vector2F, b: Vector2F, c: Vector2F, kind: PathVertexKind) {
if self.vertices.is_empty() {
self.bounds = RectF::new(a, Vector2F::zero());
}
self.bounds = self.bounds.union_point(a).union_point(b).union_point(c);
match kind {
PathVertexKind::Solid => {
self.vertices.push(PathVertex {
xy_position: a,
st_position: vec2f(0., 1.),
});
self.vertices.push(PathVertex {
xy_position: b,
st_position: vec2f(0., 1.),
});
self.vertices.push(PathVertex {
xy_position: c,
st_position: vec2f(0., 1.),
});
}
PathVertexKind::Quadratic => {
self.vertices.push(PathVertex {
xy_position: a,
st_position: vec2f(0., 0.),
});
self.vertices.push(PathVertex {
xy_position: b,
st_position: vec2f(0.5, 0.),
});
self.vertices.push(PathVertex {
xy_position: c,
st_position: vec2f(1., 1.),
});
}
}
}
}
pub fn deserialize_vec2f<'de, D>(deserializer: D) -> Result<Vector2F, D::Error>
where
D: Deserializer<'de>,
{
let [x, y]: [f32; 2] = Deserialize::deserialize(deserializer)?;
Ok(vec2f(x, y))
}
impl ToJson for Vector2F {
fn to_json(&self) -> serde_json::Value {
json!([self.x(), self.y()])
}
}
impl ToJson for RectF {
fn to_json(&self) -> serde_json::Value {
json!({"origin": self.origin().to_json(), "size": self.size().to_json()})
}
}

View file

@ -0,0 +1,43 @@
use crate::geometry::vector::{vec2i, Vector2I};
use image::{Bgra, ImageBuffer};
use std::{
fmt,
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
};
pub struct ImageData {
pub id: usize,
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
}
impl ImageData {
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Arc<Self> {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Arc::new(Self {
id: NEXT_ID.fetch_add(1, SeqCst),
data,
})
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn size(&self) -> Vector2I {
let (width, height) = self.data.dimensions();
vec2i(width as i32, height as i32)
}
}
impl fmt::Debug for ImageData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageData")
.field("id", &self.id)
.field("size", &self.data.dimensions())
.finish()
}
}

15
crates/gpui/src/json.rs Normal file
View file

@ -0,0 +1,15 @@
pub use serde_json::*;
pub trait ToJson {
fn to_json(&self) -> Value;
}
impl<T: ToJson> ToJson for Option<T> {
fn to_json(&self) -> Value {
if let Some(value) = self.as_ref() {
value.to_json()
} else {
json!(null)
}
}
}

494
crates/gpui/src/keymap.rs Normal file
View file

@ -0,0 +1,494 @@
use anyhow::anyhow;
use std::{
any::Any,
collections::{HashMap, HashSet},
fmt::Debug,
};
use tree_sitter::{Language, Node, Parser};
use crate::{Action, AnyAction};
extern "C" {
fn tree_sitter_context_predicate() -> Language;
}
pub struct Matcher {
pending: HashMap<usize, Pending>,
keymap: Keymap,
}
#[derive(Default)]
struct Pending {
keystrokes: Vec<Keystroke>,
context: Option<Context>,
}
pub struct Keymap(Vec<Binding>);
pub struct Binding {
keystrokes: Vec<Keystroke>,
action: Box<dyn AnyAction>,
context: Option<ContextPredicate>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Keystroke {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub cmd: bool,
pub key: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Context {
pub set: HashSet<String>,
pub map: HashMap<String, String>,
}
#[derive(Debug, Eq, PartialEq)]
enum ContextPredicate {
Identifier(String),
Equal(String, String),
NotEqual(String, String),
Not(Box<ContextPredicate>),
And(Box<ContextPredicate>, Box<ContextPredicate>),
Or(Box<ContextPredicate>, Box<ContextPredicate>),
}
trait ActionArg {
fn boxed_clone(&self) -> Box<dyn Any>;
}
impl<T> ActionArg for T
where
T: 'static + Any + Clone,
{
fn boxed_clone(&self) -> Box<dyn Any> {
Box::new(self.clone())
}
}
pub enum MatchResult {
None,
Pending,
Action(Box<dyn AnyAction>),
}
impl Matcher {
pub fn new(keymap: Keymap) -> Self {
Self {
pending: HashMap::new(),
keymap,
}
}
pub fn set_keymap(&mut self, keymap: Keymap) {
self.pending.clear();
self.keymap = keymap;
}
pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
self.pending.clear();
self.keymap.add_bindings(bindings);
}
pub fn push_keystroke(
&mut self,
keystroke: Keystroke,
view_id: usize,
cx: &Context,
) -> MatchResult {
let pending = self.pending.entry(view_id).or_default();
if let Some(pending_ctx) = pending.context.as_ref() {
if pending_ctx != cx {
pending.keystrokes.clear();
}
}
pending.keystrokes.push(keystroke);
let mut retain_pending = false;
for binding in self.keymap.0.iter().rev() {
if binding.keystrokes.starts_with(&pending.keystrokes)
&& binding.context.as_ref().map(|c| c.eval(cx)).unwrap_or(true)
{
if binding.keystrokes.len() == pending.keystrokes.len() {
self.pending.remove(&view_id);
return MatchResult::Action(binding.action.boxed_clone());
} else {
retain_pending = true;
pending.context = Some(cx.clone());
}
}
}
if retain_pending {
MatchResult::Pending
} else {
self.pending.remove(&view_id);
MatchResult::None
}
}
}
impl Default for Matcher {
fn default() -> Self {
Self::new(Keymap::default())
}
}
impl Keymap {
pub fn new(bindings: Vec<Binding>) -> Self {
Self(bindings)
}
fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
self.0.extend(bindings.into_iter());
}
}
pub mod menu {
use crate::action;
action!(SelectPrev);
action!(SelectNext);
}
impl Default for Keymap {
fn default() -> Self {
Self(vec![
Binding::new("up", menu::SelectPrev, Some("menu")),
Binding::new("ctrl-p", menu::SelectPrev, Some("menu")),
Binding::new("down", menu::SelectNext, Some("menu")),
Binding::new("ctrl-n", menu::SelectNext, Some("menu")),
])
}
}
impl Binding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
let context = if let Some(context) = context {
Some(ContextPredicate::parse(context).unwrap())
} else {
None
};
Self {
keystrokes: keystrokes
.split_whitespace()
.map(|key| Keystroke::parse(key).unwrap())
.collect(),
action: Box::new(action),
context,
}
}
}
impl Keystroke {
pub fn parse(source: &str) -> anyhow::Result<Self> {
let mut ctrl = false;
let mut alt = false;
let mut shift = false;
let mut cmd = false;
let mut key = None;
let mut components = source.split("-").peekable();
while let Some(component) = components.next() {
match component {
"ctrl" => ctrl = true,
"alt" => alt = true,
"shift" => shift = true,
"cmd" => cmd = true,
_ => {
if let Some(component) = components.peek() {
if component.is_empty() && source.ends_with('-') {
key = Some(String::from("-"));
break;
} else {
return Err(anyhow!("Invalid keystroke `{}`", source));
}
} else {
key = Some(String::from(component));
}
}
}
}
Ok(Keystroke {
ctrl,
alt,
shift,
cmd,
key: key.unwrap(),
})
}
}
impl Context {
pub fn extend(&mut self, other: Context) {
for v in other.set {
self.set.insert(v);
}
for (k, v) in other.map {
self.map.insert(k, v);
}
}
}
impl ContextPredicate {
fn parse(source: &str) -> anyhow::Result<Self> {
let mut parser = Parser::new();
let language = unsafe { tree_sitter_context_predicate() };
parser.set_language(language).unwrap();
let source = source.as_bytes();
let tree = parser.parse(source, None).unwrap();
Self::from_node(tree.root_node(), source)
}
fn from_node(node: Node, source: &[u8]) -> anyhow::Result<Self> {
let parse_error = "error parsing context predicate";
let kind = node.kind();
match kind {
"source" => Self::from_node(node.child(0).ok_or(anyhow!(parse_error))?, source),
"identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())),
"not" => {
let child = Self::from_node(
node.child_by_field_name("expression")
.ok_or(anyhow!(parse_error))?,
source,
)?;
Ok(Self::Not(Box::new(child)))
}
"and" | "or" => {
let left = Box::new(Self::from_node(
node.child_by_field_name("left")
.ok_or(anyhow!(parse_error))?,
source,
)?);
let right = Box::new(Self::from_node(
node.child_by_field_name("right")
.ok_or(anyhow!(parse_error))?,
source,
)?);
if kind == "and" {
Ok(Self::And(left, right))
} else {
Ok(Self::Or(left, right))
}
}
"equal" | "not_equal" => {
let left = node
.child_by_field_name("left")
.ok_or(anyhow!(parse_error))?
.utf8_text(source)?
.into();
let right = node
.child_by_field_name("right")
.ok_or(anyhow!(parse_error))?
.utf8_text(source)?
.into();
if kind == "equal" {
Ok(Self::Equal(left, right))
} else {
Ok(Self::NotEqual(left, right))
}
}
"parenthesized" => Self::from_node(
node.child_by_field_name("expression")
.ok_or(anyhow!(parse_error))?,
source,
),
_ => Err(anyhow!(parse_error)),
}
}
fn eval(&self, cx: &Context) -> bool {
match self {
Self::Identifier(name) => cx.set.contains(name.as_str()),
Self::Equal(left, right) => cx
.map
.get(left)
.map(|value| value == right)
.unwrap_or(false),
Self::NotEqual(left, right) => {
cx.map.get(left).map(|value| value != right).unwrap_or(true)
}
Self::Not(pred) => !pred.eval(cx),
Self::And(left, right) => left.eval(cx) && right.eval(cx),
Self::Or(left, right) => left.eval(cx) || right.eval(cx),
}
}
}
#[cfg(test)]
mod tests {
use crate::action;
use super::*;
#[test]
fn test_keystroke_parsing() -> anyhow::Result<()> {
assert_eq!(
Keystroke::parse("ctrl-p")?,
Keystroke {
key: "p".into(),
ctrl: true,
alt: false,
shift: false,
cmd: false,
}
);
assert_eq!(
Keystroke::parse("alt-shift-down")?,
Keystroke {
key: "down".into(),
ctrl: false,
alt: true,
shift: true,
cmd: false,
}
);
assert_eq!(
Keystroke::parse("shift-cmd--")?,
Keystroke {
key: "-".into(),
ctrl: false,
alt: false,
shift: true,
cmd: true,
}
);
Ok(())
}
#[test]
fn test_context_predicate_parsing() -> anyhow::Result<()> {
use ContextPredicate::*;
assert_eq!(
ContextPredicate::parse("a && (b == c || d != e)")?,
And(
Box::new(Identifier("a".into())),
Box::new(Or(
Box::new(Equal("b".into(), "c".into())),
Box::new(NotEqual("d".into(), "e".into())),
))
)
);
assert_eq!(
ContextPredicate::parse("!a")?,
Not(Box::new(Identifier("a".into())),)
);
Ok(())
}
#[test]
fn test_context_predicate_eval() -> anyhow::Result<()> {
let predicate = ContextPredicate::parse("a && b || c == d")?;
let mut context = Context::default();
context.set.insert("a".into());
assert!(!predicate.eval(&context));
context.set.insert("b".into());
assert!(predicate.eval(&context));
context.set.remove("b");
context.map.insert("c".into(), "x".into());
assert!(!predicate.eval(&context));
context.map.insert("c".into(), "d".into());
assert!(predicate.eval(&context));
let predicate = ContextPredicate::parse("!a")?;
assert!(predicate.eval(&Context::default()));
Ok(())
}
#[test]
fn test_matcher() -> anyhow::Result<()> {
action!(A, &'static str);
action!(B);
action!(Ab);
impl PartialEq for A {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for A {}
impl Debug for A {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "A({:?})", &self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ActionArg {
a: &'static str,
}
let keymap = Keymap(vec![
Binding::new("a", A("x"), Some("a")),
Binding::new("b", B, Some("a")),
Binding::new("a b", Ab, Some("a || b")),
]);
let mut ctx_a = Context::default();
ctx_a.set.insert("a".into());
let mut ctx_b = Context::default();
ctx_b.set.insert("b".into());
let mut matcher = Matcher::new(keymap);
// Basic match
assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
// Multi-keystroke match
assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
// Failed matches don't interfere with matching subsequent keys
assert_eq!(matcher.test_keystroke::<A>("x", 1, &ctx_a), None);
assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
// Pending keystrokes are cleared when the context changes
assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
assert_eq!(matcher.test_keystroke("b", 1, &ctx_a), Some(B));
let mut ctx_c = Context::default();
ctx_c.set.insert("c".into());
// Pending keystrokes are maintained per-view
assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
assert_eq!(matcher.test_keystroke::<A>("a", 2, &ctx_c), None);
assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
Ok(())
}
impl Matcher {
fn test_keystroke<A>(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option<A>
where
A: Action + Debug + Eq,
{
if let MatchResult::Action(action) =
self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx)
{
Some(*action.boxed_clone_as_any().downcast().unwrap())
} else {
None
}
}
}
}

35
crates/gpui/src/lib.rs Normal file
View file

@ -0,0 +1,35 @@
mod app;
pub use app::*;
mod assets;
#[cfg(test)]
mod test;
pub use assets::*;
pub mod elements;
pub mod font_cache;
mod image_data;
pub use crate::image_data::ImageData;
pub mod views;
pub use font_cache::FontCache;
mod clipboard;
pub use clipboard::ClipboardItem;
pub mod fonts;
pub mod geometry;
mod presenter;
mod scene;
pub use scene::{Border, Quad, Scene};
pub mod text_layout;
pub use text_layout::TextLayoutCache;
mod util;
pub use elements::{Element, ElementBox, ElementRc};
pub mod executor;
pub use executor::Task;
pub mod color;
pub mod json;
pub mod keymap;
pub mod platform;
pub use gpui_macros::test;
pub use platform::FontSystem;
pub use platform::{Event, PathPromptOptions, Platform, PromptLevel};
pub use presenter::{
Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
};

163
crates/gpui/src/platform.rs Normal file
View file

@ -0,0 +1,163 @@
mod event;
#[cfg(target_os = "macos")]
pub mod mac;
pub mod test;
pub mod current {
#[cfg(target_os = "macos")]
pub use super::mac::*;
}
use crate::{
executor,
fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties},
geometry::{
rect::{RectF, RectI},
vector::{vec2f, Vector2F},
},
text_layout::{LineLayout, RunStyle},
AnyAction, ClipboardItem, Menu, Scene,
};
use anyhow::Result;
use async_task::Runnable;
pub use event::Event;
use std::{
any::Any,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use time::UtcOffset;
pub trait Platform: Send + Sync {
fn dispatcher(&self) -> Arc<dyn Dispatcher>;
fn fonts(&self) -> Arc<dyn FontSystem>;
fn activate(&self, ignoring_other_apps: bool);
fn open_window(
&self,
id: usize,
options: WindowOptions,
executor: Rc<executor::Foreground>,
) -> Box<dyn Window>;
fn key_window_id(&self) -> Option<usize>;
fn quit(&self);
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn open_url(&self, url: &str);
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
fn delete_credentials(&self, url: &str) -> Result<()>;
fn set_cursor_style(&self, style: CursorStyle);
fn local_timezone(&self) -> UtcOffset;
}
pub(crate) trait ForegroundPlatform {
fn on_become_active(&self, callback: Box<dyn FnMut()>);
fn on_resign_active(&self, callback: Box<dyn FnMut()>);
fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>);
fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>);
fn set_menus(&self, menus: Vec<Menu>);
fn prompt_for_paths(
&self,
options: PathPromptOptions,
done_fn: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
);
fn prompt_for_new_path(
&self,
directory: &Path,
done_fn: Box<dyn FnOnce(Option<std::path::PathBuf>)>,
);
}
pub trait Dispatcher: Send + Sync {
fn is_main_thread(&self) -> bool;
fn run_on_main_thread(&self, task: Runnable);
}
pub trait Window: WindowContext {
fn as_any_mut(&mut self) -> &mut dyn Any;
fn on_event(&mut self, callback: Box<dyn FnMut(Event)>);
fn on_resize(&mut self, callback: Box<dyn FnMut()>);
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
fn prompt(
&self,
level: PromptLevel,
msg: &str,
answers: &[&str],
done_fn: Box<dyn FnOnce(usize)>,
);
}
pub trait WindowContext {
fn size(&self) -> Vector2F;
fn scale_factor(&self) -> f32;
fn titlebar_height(&self) -> f32;
fn present_scene(&mut self, scene: Scene);
}
pub struct WindowOptions<'a> {
pub bounds: RectF,
pub title: Option<&'a str>,
pub titlebar_appears_transparent: bool,
pub traffic_light_position: Option<Vector2F>,
}
pub struct PathPromptOptions {
pub files: bool,
pub directories: bool,
pub multiple: bool,
}
pub enum PromptLevel {
Info,
Warning,
Critical,
}
#[derive(Copy, Clone, Debug)]
pub enum CursorStyle {
Arrow,
ResizeLeftRight,
PointingHand,
}
pub trait FontSystem: Send + Sync {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
fn select_font(
&self,
font_ids: &[FontId],
properties: &FontProperties,
) -> anyhow::Result<FontId>;
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF>;
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
fn rasterize_glyph(
&self,
font_id: FontId,
font_size: f32,
glyph_id: GlyphId,
subpixel_shift: Vector2F,
scale_factor: f32,
) -> Option<(RectI, Vec<u8>)>;
fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout;
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize>;
}
impl<'a> Default for WindowOptions<'a> {
fn default() -> Self {
Self {
bounds: RectF::new(Default::default(), vec2f(1024.0, 768.0)),
title: Default::default(),
titlebar_appears_transparent: Default::default(),
traffic_light_position: Default::default(),
}
}
}

View file

@ -0,0 +1,29 @@
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
#[derive(Clone, Debug)]
pub enum Event {
KeyDown {
keystroke: Keystroke,
chars: String,
is_held: bool,
},
ScrollWheel {
position: Vector2F,
delta: Vector2F,
precise: bool,
},
LeftMouseDown {
position: Vector2F,
cmd: bool,
},
LeftMouseUp {
position: Vector2F,
},
LeftMouseDragged {
position: Vector2F,
},
MouseMoved {
position: Vector2F,
left_mouse_down: bool,
},
}

View file

@ -0,0 +1,39 @@
mod atlas;
mod dispatcher;
mod event;
mod fonts;
mod geometry;
mod image_cache;
mod platform;
mod renderer;
mod sprite_cache;
mod window;
use cocoa::base::{BOOL, NO, YES};
pub use dispatcher::Dispatcher;
pub use fonts::FontSystem;
use platform::{MacForegroundPlatform, MacPlatform};
use std::{rc::Rc, sync::Arc};
use window::Window;
pub(crate) fn platform() -> Arc<dyn super::Platform> {
Arc::new(MacPlatform::new())
}
pub(crate) fn foreground_platform() -> Rc<dyn super::ForegroundPlatform> {
Rc::new(MacForegroundPlatform::default())
}
trait BoolExt {
fn to_objc(self) -> BOOL;
}
impl BoolExt for bool {
fn to_objc(self) -> BOOL {
if self {
YES
} else {
NO
}
}
}

View file

@ -0,0 +1,177 @@
use crate::geometry::{
rect::RectI,
vector::{vec2i, Vector2I},
};
use etagere::BucketedAtlasAllocator;
use foreign_types::ForeignType;
use metal::{self, Device, TextureDescriptor};
use objc::{msg_send, sel, sel_impl};
pub struct AtlasAllocator {
device: Device,
texture_descriptor: TextureDescriptor,
atlases: Vec<Atlas>,
free_atlases: Vec<Atlas>,
}
#[derive(Copy, Clone)]
pub struct AllocId {
pub atlas_id: usize,
alloc_id: etagere::AllocId,
}
impl AtlasAllocator {
pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self {
let mut me = Self {
device,
texture_descriptor,
atlases: Vec::new(),
free_atlases: Vec::new(),
};
let atlas = me.new_atlas(Vector2I::zero());
me.atlases.push(atlas);
me
}
pub fn default_atlas_size(&self) -> Vector2I {
vec2i(
self.texture_descriptor.width() as i32,
self.texture_descriptor.height() as i32,
)
}
pub fn allocate(&mut self, requested_size: Vector2I) -> (AllocId, Vector2I) {
let (alloc_id, origin) = self
.atlases
.last_mut()
.unwrap()
.allocate(requested_size)
.unwrap_or_else(|| {
let mut atlas = self.new_atlas(requested_size);
let (id, origin) = atlas.allocate(requested_size).unwrap();
self.atlases.push(atlas);
(id, origin)
});
let id = AllocId {
atlas_id: self.atlases.len() - 1,
alloc_id,
};
(id, origin)
}
pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> (AllocId, RectI) {
let (alloc_id, origin) = self.allocate(size);
let bounds = RectI::new(origin, size);
self.atlases[alloc_id.atlas_id].upload(bounds, bytes);
(alloc_id, bounds)
}
pub fn deallocate(&mut self, id: AllocId) {
if let Some(atlas) = self.atlases.get_mut(id.atlas_id) {
atlas.deallocate(id.alloc_id);
if atlas.is_empty() {
self.free_atlases.push(self.atlases.remove(id.atlas_id));
}
}
}
pub fn clear(&mut self) {
for atlas in &mut self.atlases {
atlas.clear();
}
self.free_atlases.extend(self.atlases.drain(1..));
}
pub fn texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
self.atlases.get(atlas_id).map(|a| a.texture.as_ref())
}
fn new_atlas(&mut self, required_size: Vector2I) -> Atlas {
if let Some(i) = self.free_atlases.iter().rposition(|atlas| {
atlas.size().x() >= required_size.x() && atlas.size().y() >= required_size.y()
}) {
self.free_atlases.remove(i)
} else {
let size = self.default_atlas_size().max(required_size);
let texture = if size.x() as u64 > self.texture_descriptor.width()
|| size.y() as u64 > self.texture_descriptor.height()
{
let descriptor = unsafe {
let descriptor_ptr: *mut metal::MTLTextureDescriptor =
msg_send![self.texture_descriptor, copy];
metal::TextureDescriptor::from_ptr(descriptor_ptr)
};
descriptor.set_width(size.x() as u64);
descriptor.set_height(size.y() as u64);
self.device.new_texture(&descriptor)
} else {
self.device.new_texture(&self.texture_descriptor)
};
Atlas::new(size, texture)
}
}
}
struct Atlas {
allocator: BucketedAtlasAllocator,
texture: metal::Texture,
}
impl Atlas {
fn new(size: Vector2I, texture: metal::Texture) -> Self {
Self {
allocator: BucketedAtlasAllocator::new(etagere::Size::new(size.x(), size.y())),
texture,
}
}
fn size(&self) -> Vector2I {
let size = self.allocator.size();
vec2i(size.width, size.height)
}
fn allocate(&mut self, size: Vector2I) -> Option<(etagere::AllocId, Vector2I)> {
let alloc = self
.allocator
.allocate(etagere::Size::new(size.x(), size.y()))?;
let origin = alloc.rectangle.min;
Some((alloc.id, vec2i(origin.x, origin.y)))
}
fn upload(&mut self, bounds: RectI, bytes: &[u8]) {
let region = metal::MTLRegion::new_2d(
bounds.origin().x() as u64,
bounds.origin().y() as u64,
bounds.size().x() as u64,
bounds.size().y() as u64,
);
self.texture.replace_region(
region,
0,
bytes.as_ptr() as *const _,
(bounds.size().x() * self.bytes_per_pixel() as i32) as u64,
);
}
fn bytes_per_pixel(&self) -> u8 {
use metal::MTLPixelFormat::*;
match self.texture.pixel_format() {
A8Unorm | R8Unorm => 1,
RGBA8Unorm | BGRA8Unorm => 4,
_ => unimplemented!(),
}
}
fn deallocate(&mut self, id: etagere::AllocId) {
self.allocator.deallocate(id);
}
fn is_empty(&self) -> bool {
self.allocator.is_empty()
}
fn clear(&mut self) {
self.allocator.clear();
}
}

View file

@ -0,0 +1 @@
#include <dispatch/dispatch.h>

View file

@ -0,0 +1,43 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use async_task::Runnable;
use objc::{
class, msg_send,
runtime::{BOOL, YES},
sel, sel_impl,
};
use std::ffi::c_void;
use crate::platform;
include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
pub fn dispatch_get_main_queue() -> dispatch_queue_t {
unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
}
pub struct Dispatcher;
impl platform::Dispatcher for Dispatcher {
fn is_main_thread(&self) -> bool {
let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
is_main_thread == YES
}
fn run_on_main_thread(&self, runnable: Runnable) {
unsafe {
dispatch_async_f(
dispatch_get_main_queue(),
runnable.into_raw() as *mut c_void,
Some(trampoline),
);
}
extern "C" fn trampoline(runnable: *mut c_void) {
let task = unsafe { Runnable::from_raw(runnable as *mut ()) };
task.run();
}
}
}

View file

@ -0,0 +1,124 @@
use crate::{geometry::vector::vec2f, keymap::Keystroke, platform::Event};
use cocoa::appkit::{
NSDeleteFunctionKey as DELETE_KEY, NSDownArrowFunctionKey as ARROW_DOWN_KEY,
NSLeftArrowFunctionKey as ARROW_LEFT_KEY, NSPageDownFunctionKey as PAGE_DOWN_KEY,
NSPageUpFunctionKey as PAGE_UP_KEY, NSRightArrowFunctionKey as ARROW_RIGHT_KEY,
NSUpArrowFunctionKey as ARROW_UP_KEY,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
base::{id, nil, YES},
foundation::NSString as _,
};
use std::{ffi::CStr, os::raw::c_char};
const BACKSPACE_KEY: u16 = 0x7f;
const ENTER_KEY: u16 = 0x0d;
const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
impl Event {
pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {
let event_type = native_event.eventType();
// Filter out event types that aren't in the NSEventType enum.
// See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
match event_type as u64 {
0 | 21 | 32 | 33 | 35 | 36 | 37 => {
return None;
}
_ => {}
}
match event_type {
NSEventType::NSKeyDown => {
let modifiers = native_event.modifierFlags();
let unmodified_chars = native_event.charactersIgnoringModifiers();
let unmodified_chars = CStr::from_ptr(unmodified_chars.UTF8String() as *mut c_char)
.to_str()
.unwrap();
let unmodified_chars = if let Some(first_char) = unmodified_chars.chars().next() {
match first_char as u16 {
ARROW_UP_KEY => "up",
ARROW_DOWN_KEY => "down",
ARROW_LEFT_KEY => "left",
ARROW_RIGHT_KEY => "right",
PAGE_UP_KEY => "pageup",
PAGE_DOWN_KEY => "pagedown",
BACKSPACE_KEY => "backspace",
ENTER_KEY => "enter",
DELETE_KEY => "delete",
ESCAPE_KEY => "escape",
TAB_KEY => "tab",
_ => unmodified_chars,
}
} else {
return None;
};
let chars = native_event.characters();
let chars = CStr::from_ptr(chars.UTF8String() as *mut c_char)
.to_str()
.unwrap()
.into();
Some(Self::KeyDown {
keystroke: Keystroke {
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
key: unmodified_chars.into(),
},
chars,
is_held: native_event.isARepeat() == YES,
})
}
NSEventType::NSLeftMouseDown => {
window_height.map(|window_height| Self::LeftMouseDown {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
cmd: native_event
.modifierFlags()
.contains(NSEventModifierFlags::NSCommandKeyMask),
})
}
NSEventType::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
}),
NSEventType::NSLeftMouseDragged => {
window_height.map(|window_height| Self::LeftMouseDragged {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
})
}
NSEventType::NSScrollWheel => window_height.map(|window_height| Self::ScrollWheel {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
delta: vec2f(
native_event.scrollingDeltaX() as f32,
native_event.scrollingDeltaY() as f32,
),
precise: native_event.hasPreciseScrollingDeltas() == YES,
}),
NSEventType::NSMouseMoved => window_height.map(|window_height| Self::MouseMoved {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0,
}),
_ => None,
}
}
}

View file

@ -0,0 +1,563 @@
use crate::{
fonts::{FontId, GlyphId, Metrics, Properties},
geometry::{
rect::{RectF, RectI},
transform2d::Transform2F,
vector::{vec2f, vec2i, Vector2F},
},
platform,
text_layout::{Glyph, LineLayout, Run, RunStyle},
};
use cocoa::appkit::{CGFloat, CGPoint};
use core_foundation::{
array::CFIndex,
attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
base::{CFRange, TCFType},
number::CFNumber,
string::CFString,
};
use core_graphics::{
base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform,
};
use core_text::{line::CTLine, string_attributes::kCTFontAttributeName};
use font_kit::{
canvas::RasterizationOptions, handle::Handle, hinting::HintingOptions, source::SystemSource,
sources::mem::MemSource,
};
use parking_lot::RwLock;
use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
#[allow(non_upper_case_globals)]
const kCGImageAlphaOnly: u32 = 7;
pub struct FontSystem(RwLock<FontSystemState>);
struct FontSystemState {
memory_source: MemSource,
system_source: SystemSource,
fonts: Vec<font_kit::font::Font>,
}
impl FontSystem {
pub fn new() -> Self {
Self(RwLock::new(FontSystemState {
memory_source: MemSource::empty(),
system_source: SystemSource::new(),
fonts: Vec::new(),
}))
}
}
impl platform::FontSystem for FontSystem {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()> {
self.0.write().add_fonts(fonts)
}
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>> {
self.0.write().load_family(name)
}
fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
self.0.read().select_font(font_ids, properties)
}
fn font_metrics(&self, font_id: FontId) -> Metrics {
self.0.read().font_metrics(font_id)
}
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF> {
self.0.read().typographic_bounds(font_id, glyph_id)
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
self.0.read().glyph_for_char(font_id, ch)
}
fn rasterize_glyph(
&self,
font_id: FontId,
font_size: f32,
glyph_id: GlyphId,
subpixel_shift: Vector2F,
scale_factor: f32,
) -> Option<(RectI, Vec<u8>)> {
self.0
.read()
.rasterize_glyph(font_id, font_size, glyph_id, subpixel_shift, scale_factor)
}
fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
self.0.read().layout_line(text, font_size, runs)
}
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize> {
self.0.read().wrap_line(text, font_id, font_size, width)
}
}
impl FontSystemState {
fn add_fonts(&mut self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()> {
self.memory_source.add_fonts(
fonts
.iter()
.map(|bytes| Handle::from_memory(bytes.clone(), 0)),
)?;
Ok(())
}
fn load_family(&mut self, name: &str) -> anyhow::Result<Vec<FontId>> {
let mut font_ids = Vec::new();
let family = self
.memory_source
.select_family_by_name(name)
.or_else(|_| self.system_source.select_family_by_name(name))?;
for font in family.fonts() {
let font = font.load()?;
font_ids.push(FontId(self.fonts.len()));
self.fonts.push(font);
}
Ok(font_ids)
}
fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
let candidates = font_ids
.iter()
.map(|font_id| self.fonts[font_id.0].properties())
.collect::<Vec<_>>();
let idx = font_kit::matching::find_best_match(&candidates, properties)?;
Ok(font_ids[idx])
}
fn font_metrics(&self, font_id: FontId) -> Metrics {
self.fonts[font_id.0].metrics()
}
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<RectF> {
Ok(self.fonts[font_id.0].typographic_bounds(glyph_id)?)
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
self.fonts[font_id.0].glyph_for_char(ch)
}
fn rasterize_glyph(
&self,
font_id: FontId,
font_size: f32,
glyph_id: GlyphId,
subpixel_shift: Vector2F,
scale_factor: f32,
) -> Option<(RectI, Vec<u8>)> {
let font = &self.fonts[font_id.0];
let scale = Transform2F::from_scale(scale_factor);
let bounds = font
.raster_bounds(
glyph_id,
font_size,
scale,
HintingOptions::None,
RasterizationOptions::GrayscaleAa,
)
.ok()?;
if bounds.width() == 0 || bounds.height() == 0 {
None
} else {
// Make room for subpixel variants.
let bounds = RectI::new(bounds.origin(), bounds.size() + vec2i(1, 1));
let mut pixels = vec![0; bounds.width() as usize * bounds.height() as usize];
let cx = CGContext::create_bitmap_context(
Some(pixels.as_mut_ptr() as *mut _),
bounds.width() as usize,
bounds.height() as usize,
8,
bounds.width() as usize,
&CGColorSpace::create_device_gray(),
kCGImageAlphaOnly,
);
// Move the origin to bottom left and account for scaling, this
// makes drawing text consistent with the font-kit's raster_bounds.
cx.translate(0.0, bounds.height() as CGFloat);
let transform = scale.translate(-bounds.origin().to_f32());
cx.set_text_matrix(&CGAffineTransform {
a: transform.matrix.m11() as CGFloat,
b: -transform.matrix.m21() as CGFloat,
c: -transform.matrix.m12() as CGFloat,
d: transform.matrix.m22() as CGFloat,
tx: transform.vector.x() as CGFloat,
ty: -transform.vector.y() as CGFloat,
});
cx.set_font(&font.native_font().copy_to_CGFont());
cx.set_font_size(font_size as CGFloat);
cx.show_glyphs_at_positions(
&[glyph_id as CGGlyph],
&[CGPoint::new(
(subpixel_shift.x() / scale_factor) as CGFloat,
(subpixel_shift.y() / scale_factor) as CGFloat,
)],
);
Some((bounds, pixels))
}
}
fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
let font_id_attr_name = CFString::from_static_string("zed_font_id");
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
let mut string = CFMutableAttributedString::new();
{
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
let utf16_line_len = string.char_len() as usize;
let last_run: RefCell<Option<(usize, FontId)>> = Default::default();
let font_runs = runs
.iter()
.filter_map(|(len, style)| {
let mut last_run = last_run.borrow_mut();
if let Some((last_len, last_font_id)) = last_run.as_mut() {
if style.font_id == *last_font_id {
*last_len += *len;
None
} else {
let result = (*last_len, *last_font_id);
*last_len = *len;
*last_font_id = style.font_id;
Some(result)
}
} else {
*last_run = Some((*len, style.font_id));
None
}
})
.chain(std::iter::from_fn(|| last_run.borrow_mut().take()));
let mut ix_converter = StringIndexConverter::new(text);
for (run_len, font_id) in font_runs {
let utf8_end = ix_converter.utf8_ix + run_len;
let utf16_start = ix_converter.utf16_ix;
if utf16_start >= utf16_line_len {
break;
}
ix_converter.advance_to_utf8_ix(utf8_end);
let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len);
let cf_range =
CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
let font = &self.fonts[font_id.0];
unsafe {
string.set_attribute(
cf_range,
kCTFontAttributeName,
&font.native_font().clone_with_font_size(font_size as f64),
);
string.set_attribute(
cf_range,
font_id_attr_name.as_concrete_TypeRef(),
&CFNumber::from(font_id.0 as i64),
);
}
if utf16_end == utf16_line_len {
break;
}
}
}
// Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets.
let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
let mut runs = Vec::new();
for run in line.glyph_runs().into_iter() {
let font_id = FontId(
run.attributes()
.unwrap()
.get(&font_id_attr_name)
.downcast::<CFNumber>()
.unwrap()
.to_i64()
.unwrap() as usize,
);
let mut ix_converter = StringIndexConverter::new(text);
let mut glyphs = Vec::new();
for ((glyph_id, position), glyph_utf16_ix) in run
.glyphs()
.iter()
.zip(run.positions().iter())
.zip(run.string_indices().iter())
{
let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap();
ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
glyphs.push(Glyph {
id: *glyph_id as GlyphId,
position: vec2f(position.x as f32, position.y as f32),
index: ix_converter.utf8_ix,
});
}
runs.push(Run { font_id, glyphs })
}
let typographic_bounds = line.get_typographic_bounds();
LineLayout {
width: typographic_bounds.width as f32,
ascent: typographic_bounds.ascent as f32,
descent: typographic_bounds.descent as f32,
runs,
font_size,
len: text.len(),
}
}
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize> {
let mut string = CFMutableAttributedString::new();
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
let cf_range = CFRange::init(0 as isize, text.encode_utf16().count() as isize);
let font = &self.fonts[font_id.0];
unsafe {
string.set_attribute(
cf_range,
kCTFontAttributeName,
&font.native_font().clone_with_font_size(font_size as f64),
);
let typesetter = CTTypesetterCreateWithAttributedString(string.as_concrete_TypeRef());
let mut ix_converter = StringIndexConverter::new(text);
let mut break_indices = Vec::new();
while ix_converter.utf8_ix < text.len() {
let utf16_len = CTTypesetterSuggestLineBreak(
typesetter,
ix_converter.utf16_ix as isize,
width as f64,
) as usize;
ix_converter.advance_to_utf16_ix(ix_converter.utf16_ix + utf16_len);
if ix_converter.utf8_ix >= text.len() {
break;
}
break_indices.push(ix_converter.utf8_ix as usize);
}
break_indices
}
}
}
#[derive(Clone)]
struct StringIndexConverter<'a> {
text: &'a str,
utf8_ix: usize,
utf16_ix: usize,
}
impl<'a> StringIndexConverter<'a> {
fn new(text: &'a str) -> Self {
Self {
text,
utf8_ix: 0,
utf16_ix: 0,
}
}
fn advance_to_utf8_ix(&mut self, utf8_target: usize) {
for (ix, c) in self.text[self.utf8_ix..].char_indices() {
if self.utf8_ix + ix >= utf8_target {
self.utf8_ix += ix;
return;
}
self.utf16_ix += c.len_utf16();
}
self.utf8_ix = self.text.len();
}
fn advance_to_utf16_ix(&mut self, utf16_target: usize) {
for (ix, c) in self.text[self.utf8_ix..].char_indices() {
if self.utf16_ix >= utf16_target {
self.utf8_ix += ix;
return;
}
self.utf16_ix += c.len_utf16();
}
self.utf8_ix = self.text.len();
}
}
#[repr(C)]
pub struct __CFTypesetter(c_void);
pub type CTTypesetterRef = *const __CFTypesetter;
#[link(name = "CoreText", kind = "framework")]
extern "C" {
fn CTTypesetterCreateWithAttributedString(string: CFAttributedStringRef) -> CTTypesetterRef;
fn CTTypesetterSuggestLineBreak(
typesetter: CTTypesetterRef,
start_index: CFIndex,
width: f64,
) -> CFIndex;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MutableAppContext;
use font_kit::properties::{Style, Weight};
use platform::FontSystem as _;
#[crate::test(self, retries = 5)]
fn test_layout_str(_: &mut MutableAppContext) {
// This is failing intermittently on CI and we don't have time to figure it out
let fonts = FontSystem::new();
let menlo = fonts.load_family("Menlo").unwrap();
let menlo_regular = RunStyle {
font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
color: Default::default(),
underline: false,
};
let menlo_italic = RunStyle {
font_id: fonts
.select_font(&menlo, &Properties::new().style(Style::Italic))
.unwrap(),
color: Default::default(),
underline: false,
};
let menlo_bold = RunStyle {
font_id: fonts
.select_font(&menlo, &Properties::new().weight(Weight::BOLD))
.unwrap(),
color: Default::default(),
underline: false,
};
assert_ne!(menlo_regular, menlo_italic);
assert_ne!(menlo_regular, menlo_bold);
assert_ne!(menlo_italic, menlo_bold);
let line = fonts.layout_line(
"hello world",
16.0,
&[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)],
);
assert_eq!(line.runs.len(), 3);
assert_eq!(line.runs[0].font_id, menlo_bold.font_id);
assert_eq!(line.runs[0].glyphs.len(), 2);
assert_eq!(line.runs[1].font_id, menlo_italic.font_id);
assert_eq!(line.runs[1].glyphs.len(), 4);
assert_eq!(line.runs[2].font_id, menlo_regular.font_id);
assert_eq!(line.runs[2].glyphs.len(), 5);
}
#[test]
fn test_glyph_offsets() -> anyhow::Result<()> {
let fonts = FontSystem::new();
let zapfino = fonts.load_family("Zapfino")?;
let zapfino_regular = RunStyle {
font_id: fonts.select_font(&zapfino, &Properties::new())?,
color: Default::default(),
underline: false,
};
let menlo = fonts.load_family("Menlo")?;
let menlo_regular = RunStyle {
font_id: fonts.select_font(&menlo, &Properties::new())?,
color: Default::default(),
underline: false,
};
let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
let line = fonts.layout_line(
text,
16.0,
&[
(9, zapfino_regular),
(13, menlo_regular),
(text.len() - 22, zapfino_regular),
],
);
assert_eq!(
line.runs
.iter()
.flat_map(|r| r.glyphs.iter())
.map(|g| g.index)
.collect::<Vec<_>>(),
vec![0, 2, 4, 5, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22, 23, 24, 26, 27, 28, 29, 36, 37],
);
Ok(())
}
#[test]
#[ignore]
fn test_rasterize_glyph() {
use std::{fs::File, io::BufWriter, path::Path};
let fonts = FontSystem::new();
let font_ids = fonts.load_family("Fira Code").unwrap();
let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
const VARIANTS: usize = 1;
for i in 0..VARIANTS {
let variant = i as f32 / VARIANTS as f32;
let (bounds, bytes) = fonts
.rasterize_glyph(font_id, 16.0, glyph_id, vec2f(variant, variant), 2.)
.unwrap();
let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
let path = Path::new(&name);
let file = File::create(path).unwrap();
let ref mut w = BufWriter::new(file);
let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32);
encoder.set_color(png::ColorType::Grayscale);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&bytes).unwrap();
}
}
#[test]
fn test_wrap_line() {
let fonts = FontSystem::new();
let font_ids = fonts.load_family("Helvetica").unwrap();
let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
let line = "one two three four five\n";
let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
assert_eq!(
wrap_boundaries,
&["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
);
}
#[test]
fn test_layout_line_bom_char() {
let fonts = FontSystem::new();
let font_ids = fonts.load_family("Helvetica").unwrap();
let style = RunStyle {
font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
color: Default::default(),
underline: false,
};
let line = "\u{feff}";
let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
assert_eq!(layout.len, line.len());
assert!(layout.runs.is_empty());
let line = "a\u{feff}b";
let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
assert_eq!(layout.len, line.len());
assert_eq!(layout.runs.len(), 1);
assert_eq!(layout.runs[0].glyphs.len(), 2);
assert_eq!(layout.runs[0].glyphs[0].id, 68); // a
// There's no glyph for \u{feff}
assert_eq!(layout.runs[0].glyphs[1].id, 69); // b
}
}

View file

@ -0,0 +1,27 @@
use cocoa::foundation::{NSPoint, NSRect, NSSize};
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
pub trait Vector2FExt {
fn to_ns_point(&self) -> NSPoint;
fn to_ns_size(&self) -> NSSize;
}
pub trait RectFExt {
fn to_ns_rect(&self) -> NSRect;
}
impl Vector2FExt for Vector2F {
fn to_ns_point(&self) -> NSPoint {
NSPoint::new(self.x() as f64, self.y() as f64)
}
fn to_ns_size(&self) -> NSSize {
NSSize::new(self.x() as f64, self.y() as f64)
}
}
impl RectFExt for RectF {
fn to_ns_rect(&self) -> NSRect {
NSRect::new(self.origin().to_ns_point(), self.size().to_ns_size())
}
}

View file

@ -0,0 +1,49 @@
use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
use super::atlas::{AllocId, AtlasAllocator};
use crate::{
geometry::{rect::RectI, vector::Vector2I},
ImageData,
};
use std::{collections::HashMap, mem};
pub struct ImageCache {
prev_frame: HashMap<usize, (AllocId, RectI)>,
curr_frame: HashMap<usize, (AllocId, RectI)>,
atlases: AtlasAllocator,
}
impl ImageCache {
pub fn new(device: metal::Device, size: Vector2I) -> Self {
let descriptor = TextureDescriptor::new();
descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
descriptor.set_width(size.x() as u64);
descriptor.set_height(size.y() as u64);
Self {
prev_frame: Default::default(),
curr_frame: Default::default(),
atlases: AtlasAllocator::new(device, descriptor),
}
}
pub fn render(&mut self, image: &ImageData) -> (AllocId, RectI) {
let (alloc_id, atlas_bounds) = self
.prev_frame
.remove(&image.id)
.or_else(|| self.curr_frame.get(&image.id).copied())
.unwrap_or_else(|| self.atlases.upload(image.size(), image.as_bytes()));
self.curr_frame.insert(image.id, (alloc_id, atlas_bounds));
(alloc_id, atlas_bounds)
}
pub fn finish_frame(&mut self) {
mem::swap(&mut self.prev_frame, &mut self.curr_frame);
for (_, (id, _)) in self.curr_frame.drain() {
self.atlases.deallocate(id);
}
}
pub fn atlas_texture(&self, atlas_id: usize) -> Option<&TextureRef> {
self.atlases.texture(atlas_id)
}
}

View file

@ -0,0 +1,748 @@
use super::{BoolExt as _, Dispatcher, FontSystem, Window};
use crate::{
executor,
keymap::Keystroke,
platform::{self, CursorStyle},
AnyAction, ClipboardItem, Event, Menu, MenuItem,
};
use anyhow::{anyhow, Result};
use block::ConcreteBlock;
use cocoa::{
appkit::{
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
NSPasteboardTypeString, NSSavePanel, NSWindow,
},
base::{id, nil, selector, YES},
foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL},
};
use core_foundation::{
base::{CFType, CFTypeRef, OSStatus, TCFType as _},
boolean::CFBoolean,
data::CFData,
dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
string::{CFString, CFStringRef},
};
use ctor::ctor;
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use ptr::null_mut;
use std::{
cell::{Cell, RefCell},
convert::TryInto,
ffi::{c_void, CStr},
os::raw::c_char,
path::{Path, PathBuf},
ptr,
rc::Rc,
slice, str,
sync::Arc,
};
use time::UtcOffset;
const MAC_PLATFORM_IVAR: &'static str = "platform";
static mut APP_CLASS: *const Class = ptr::null();
static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
#[ctor]
unsafe fn build_classes() {
APP_CLASS = {
let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
decl.add_method(
sel!(sendEvent:),
send_event as extern "C" fn(&mut Object, Sel, id),
);
decl.register()
};
APP_DELEGATE_CLASS = {
let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
decl.add_method(
sel!(applicationDidFinishLaunching:),
did_finish_launching as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(applicationDidBecomeActive:),
did_become_active as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(applicationDidResignActive:),
did_resign_active as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(handleGPUIMenuItem:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(application:openFiles:),
open_files as extern "C" fn(&mut Object, Sel, id, id),
);
decl.register()
}
}
#[derive(Default)]
pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
#[derive(Default)]
pub struct MacForegroundPlatformState {
become_active: Option<Box<dyn FnMut()>>,
resign_active: Option<Box<dyn FnMut()>>,
event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
menu_command: Option<Box<dyn FnMut(&dyn AnyAction)>>,
open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
finish_launching: Option<Box<dyn FnOnce() -> ()>>,
menu_actions: Vec<Box<dyn AnyAction>>,
}
impl MacForegroundPlatform {
unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
let menu_bar = NSMenu::new(nil).autorelease();
let mut state = self.0.borrow_mut();
state.menu_actions.clear();
for menu_config in menus {
let menu_bar_item = NSMenuItem::new(nil).autorelease();
let menu = NSMenu::new(nil).autorelease();
let menu_name = menu_config.name;
menu.setTitle_(ns_string(menu_name));
for item_config in menu_config.items {
let item;
match item_config {
MenuItem::Separator => {
item = NSMenuItem::separatorItem(nil);
}
MenuItem::Action {
name,
keystroke,
action,
} => {
if let Some(keystroke) = keystroke {
let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
panic!(
"Invalid keystroke for menu item {}:{} - {:?}",
menu_name, name, err
)
});
let mut mask = NSEventModifierFlags::empty();
for (modifier, flag) in &[
(keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
(keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
(keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
] {
if *modifier {
mask |= *flag;
}
}
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(name),
selector("handleGPUIMenuItem:"),
ns_string(&keystroke.key),
)
.autorelease();
item.setKeyEquivalentModifierMask_(mask);
} else {
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(name),
selector("handleGPUIMenuItem:"),
ns_string(""),
)
.autorelease();
}
let tag = state.menu_actions.len() as NSInteger;
let _: () = msg_send![item, setTag: tag];
state.menu_actions.push(action);
}
}
menu.addItem_(item);
}
menu_bar_item.setSubmenu_(menu);
menu_bar.addItem_(menu_bar_item);
}
menu_bar
}
}
impl platform::ForegroundPlatform for MacForegroundPlatform {
fn on_become_active(&self, callback: Box<dyn FnMut()>) {
self.0.borrow_mut().become_active = Some(callback);
}
fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
self.0.borrow_mut().resign_active = Some(callback);
}
fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
self.0.borrow_mut().event = Some(callback);
}
fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
self.0.borrow_mut().open_files = Some(callback);
}
fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
self.0.borrow_mut().finish_launching = Some(on_finish_launching);
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
app.setDelegate_(app_delegate);
let self_ptr = self as *const Self as *const c_void;
(*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
(*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
let pool = NSAutoreleasePool::new(nil);
app.run();
pool.drain();
(*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
(*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
}
}
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>) {
self.0.borrow_mut().menu_command = Some(callback);
}
fn set_menus(&self, menus: Vec<Menu>) {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
app.setMainMenu_(self.create_menu_bar(menus));
}
}
fn prompt_for_paths(
&self,
options: platform::PathPromptOptions,
done_fn: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
) {
unsafe {
let panel = NSOpenPanel::openPanel(nil);
panel.setCanChooseDirectories_(options.directories.to_objc());
panel.setCanChooseFiles_(options.files.to_objc());
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
panel.setResolvesAliases_(false.to_objc());
let done_fn = Cell::new(Some(done_fn));
let block = ConcreteBlock::new(move |response: NSModalResponse| {
let result = if response == NSModalResponse::NSModalResponseOk {
let mut result = Vec::new();
let urls = panel.URLs();
for i in 0..urls.count() {
let url = urls.objectAtIndex(i);
if url.isFileURL() == YES {
let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
.to_string_lossy()
.to_string();
result.push(PathBuf::from(path));
}
}
Some(result)
} else {
None
};
if let Some(done_fn) = done_fn.take() {
(done_fn)(result);
}
});
let block = block.copy();
let _: () = msg_send![panel, beginWithCompletionHandler: block];
}
}
fn prompt_for_new_path(
&self,
directory: &Path,
done_fn: Box<dyn FnOnce(Option<std::path::PathBuf>)>,
) {
unsafe {
let panel = NSSavePanel::savePanel(nil);
let path = ns_string(directory.to_string_lossy().as_ref());
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
panel.setDirectoryURL(url);
let done_fn = Cell::new(Some(done_fn));
let block = ConcreteBlock::new(move |response: NSModalResponse| {
let result = if response == NSModalResponse::NSModalResponseOk {
let url = panel.URL();
if url.isFileURL() == YES {
let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
.to_string_lossy()
.to_string();
Some(PathBuf::from(path))
} else {
None
}
} else {
None
};
if let Some(done_fn) = done_fn.take() {
(done_fn)(result);
}
});
let block = block.copy();
let _: () = msg_send![panel, beginWithCompletionHandler: block];
}
}
}
pub struct MacPlatform {
dispatcher: Arc<Dispatcher>,
fonts: Arc<FontSystem>,
pasteboard: id,
text_hash_pasteboard_type: id,
metadata_pasteboard_type: id,
}
impl MacPlatform {
pub fn new() -> Self {
Self {
dispatcher: Arc::new(Dispatcher),
fonts: Arc::new(FontSystem::new()),
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
}
}
unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> {
let data = self.pasteboard.dataForType(kind);
if data == nil {
None
} else {
Some(slice::from_raw_parts(
data.bytes() as *mut u8,
data.length() as usize,
))
}
}
}
unsafe impl Send for MacPlatform {}
unsafe impl Sync for MacPlatform {}
impl platform::Platform for MacPlatform {
fn dispatcher(&self) -> Arc<dyn platform::Dispatcher> {
self.dispatcher.clone()
}
fn activate(&self, ignoring_other_apps: bool) {
unsafe {
let app = NSApplication::sharedApplication(nil);
app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
}
}
fn open_window(
&self,
id: usize,
options: platform::WindowOptions,
executor: Rc<executor::Foreground>,
) -> Box<dyn platform::Window> {
Box::new(Window::open(id, options, executor, self.fonts()))
}
fn key_window_id(&self) -> Option<usize> {
Window::key_window_id()
}
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
self.fonts.clone()
}
fn quit(&self) {
// Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks
// synchronously before this method terminates. If we call `Platform::quit` while holding a
// borrow of the app state (which most of the time we will do), we will end up
// double-borrowing the app state in the `on_close` callbacks for our open windows. To solve
// this, we make quitting the application asynchronous so that we aren't holding borrows to
// the app state on the stack when we actually terminate the app.
use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue};
unsafe {
dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit));
}
unsafe extern "C" fn quit(_: *mut c_void) {
let app = NSApplication::sharedApplication(nil);
let _: () = msg_send![app, terminate: nil];
}
}
fn write_to_clipboard(&self, item: ClipboardItem) {
unsafe {
self.pasteboard.clearContents();
let text_bytes = NSData::dataWithBytes_length_(
nil,
item.text.as_ptr() as *const c_void,
item.text.len() as u64,
);
self.pasteboard
.setData_forType(text_bytes, NSPasteboardTypeString);
if let Some(metadata) = item.metadata.as_ref() {
let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes();
let hash_bytes = NSData::dataWithBytes_length_(
nil,
hash_bytes.as_ptr() as *const c_void,
hash_bytes.len() as u64,
);
self.pasteboard
.setData_forType(hash_bytes, self.text_hash_pasteboard_type);
let metadata_bytes = NSData::dataWithBytes_length_(
nil,
metadata.as_ptr() as *const c_void,
metadata.len() as u64,
);
self.pasteboard
.setData_forType(metadata_bytes, self.metadata_pasteboard_type);
}
}
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
unsafe {
if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) {
let text = String::from_utf8_lossy(&text_bytes).to_string();
let hash_bytes = self
.read_from_pasteboard(self.text_hash_pasteboard_type)
.and_then(|bytes| bytes.try_into().ok())
.map(u64::from_be_bytes);
let metadata_bytes = self
.read_from_pasteboard(self.metadata_pasteboard_type)
.and_then(|bytes| String::from_utf8(bytes.to_vec()).ok());
if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) {
if hash == ClipboardItem::text_hash(&text) {
Some(ClipboardItem {
text,
metadata: Some(metadata),
})
} else {
Some(ClipboardItem {
text,
metadata: None,
})
}
} else {
Some(ClipboardItem {
text,
metadata: None,
})
}
} else {
None
}
}
}
fn open_url(&self, url: &str) {
unsafe {
let url = NSURL::alloc(nil)
.initWithString_(ns_string(url))
.autorelease();
let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
msg_send![workspace, openURL: url]
}
}
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
let url = CFString::from(url);
let username = CFString::from(username);
let password = CFData::from_buffer(password);
unsafe {
use security::*;
// First, check if there are already credentials for the given server. If so, then
// update the username and password.
let mut verb = "updating";
let mut query_attrs = CFMutableDictionary::with_capacity(2);
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
let mut attrs = CFMutableDictionary::with_capacity(4);
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
let mut status = SecItemUpdate(
query_attrs.as_concrete_TypeRef(),
attrs.as_concrete_TypeRef(),
);
// If there were no existing credentials for the given server, then create them.
if status == errSecItemNotFound {
verb = "creating";
status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
}
if status != errSecSuccess {
return Err(anyhow!("{} password failed: {}", verb, status));
}
}
Ok(())
}
fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
let url = CFString::from(url);
let cf_true = CFBoolean::true_value().as_CFTypeRef();
unsafe {
use security::*;
// Find any credentials for the given server URL.
let mut attrs = CFMutableDictionary::with_capacity(5);
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
attrs.set(kSecReturnAttributes as *const _, cf_true);
attrs.set(kSecReturnData as *const _, cf_true);
let mut result = CFTypeRef::from(ptr::null_mut());
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
match status {
security::errSecSuccess => {}
security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
_ => return Err(anyhow!("reading password failed: {}", status)),
}
let result = CFType::wrap_under_create_rule(result)
.downcast::<CFDictionary>()
.ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
let username = result
.find(kSecAttrAccount as *const _)
.ok_or_else(|| anyhow!("account was missing from keychain item"))?;
let username = CFType::wrap_under_get_rule(*username)
.downcast::<CFString>()
.ok_or_else(|| anyhow!("account was not a string"))?;
let password = result
.find(kSecValueData as *const _)
.ok_or_else(|| anyhow!("password was missing from keychain item"))?;
let password = CFType::wrap_under_get_rule(*password)
.downcast::<CFData>()
.ok_or_else(|| anyhow!("password was not a string"))?;
Ok(Some((username.to_string(), password.bytes().to_vec())))
}
}
fn delete_credentials(&self, url: &str) -> Result<()> {
let url = CFString::from(url);
unsafe {
use security::*;
let mut query_attrs = CFMutableDictionary::with_capacity(2);
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
if status != errSecSuccess {
return Err(anyhow!("delete password failed: {}", status));
}
}
Ok(())
}
fn set_cursor_style(&self, style: CursorStyle) {
unsafe {
let cursor: id = match style {
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
};
let _: () = msg_send![cursor, set];
}
}
fn local_timezone(&self) -> UtcOffset {
unsafe {
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT];
UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
}
}
}
unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform {
let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
assert!(!platform_ptr.is_null());
&*(platform_ptr as *const MacForegroundPlatform)
}
extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
unsafe {
if let Some(event) = Event::from_native(native_event, None) {
let platform = get_foreground_platform(this);
if let Some(callback) = platform.0.borrow_mut().event.as_mut() {
if callback(event) {
return;
}
}
}
msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
}
}
extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
let platform = get_foreground_platform(this);
let callback = platform.0.borrow_mut().finish_launching.take();
if let Some(callback) = callback {
callback();
}
}
}
extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
let platform = unsafe { get_foreground_platform(this) };
if let Some(callback) = platform.0.borrow_mut().become_active.as_mut() {
callback();
}
}
extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
let platform = unsafe { get_foreground_platform(this) };
if let Some(callback) = platform.0.borrow_mut().resign_active.as_mut() {
callback();
}
}
extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
let paths = unsafe {
(0..paths.count())
.into_iter()
.filter_map(|i| {
let path = paths.objectAtIndex(i);
match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
Ok(string) => Some(PathBuf::from(string)),
Err(err) => {
log::error!("error converting path to string: {}", err);
None
}
}
})
.collect::<Vec<_>>()
};
let platform = unsafe { get_foreground_platform(this) };
if let Some(callback) = platform.0.borrow_mut().open_files.as_mut() {
callback(paths);
}
}
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
unsafe {
let platform = get_foreground_platform(this);
let mut platform = platform.0.borrow_mut();
if let Some(mut callback) = platform.menu_command.take() {
let tag: NSInteger = msg_send![item, tag];
let index = tag as usize;
if let Some(action) = platform.menu_actions.get(index) {
callback(action.as_ref());
}
platform.menu_command = Some(callback);
}
}
}
unsafe fn ns_string(string: &str) -> id {
NSString::alloc(nil).init_str(string).autorelease()
}
mod security {
#![allow(non_upper_case_globals)]
use super::*;
#[link(name = "Security", kind = "framework")]
extern "C" {
pub static kSecClass: CFStringRef;
pub static kSecClassInternetPassword: CFStringRef;
pub static kSecAttrServer: CFStringRef;
pub static kSecAttrAccount: CFStringRef;
pub static kSecValueData: CFStringRef;
pub static kSecReturnAttributes: CFStringRef;
pub static kSecReturnData: CFStringRef;
pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus;
pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
}
pub const errSecSuccess: OSStatus = 0;
pub const errSecUserCanceled: OSStatus = -128;
pub const errSecItemNotFound: OSStatus = -25300;
}
#[cfg(test)]
mod tests {
use crate::platform::Platform;
use super::*;
#[test]
fn test_clipboard() {
let platform = build_platform();
assert_eq!(platform.read_from_clipboard(), None);
let item = ClipboardItem::new("1".to_string());
platform.write_to_clipboard(item.clone());
assert_eq!(platform.read_from_clipboard(), Some(item));
let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]);
platform.write_to_clipboard(item.clone());
assert_eq!(platform.read_from_clipboard(), Some(item));
let text_from_other_app = "text from other app";
unsafe {
let bytes = NSData::dataWithBytes_length_(
nil,
text_from_other_app.as_ptr() as *const c_void,
text_from_other_app.len() as u64,
);
platform
.pasteboard
.setData_forType(bytes, NSPasteboardTypeString);
}
assert_eq!(
platform.read_from_clipboard(),
Some(ClipboardItem::new(text_from_other_app.to_string()))
);
}
fn build_platform() -> MacPlatform {
let mut platform = MacPlatform::new();
platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
platform
}
}

View file

@ -0,0 +1,967 @@
use super::{atlas::AtlasAllocator, image_cache::ImageCache, sprite_cache::SpriteCache};
use crate::{
color::Color,
geometry::{
rect::RectF,
vector::{vec2f, vec2i, Vector2F},
},
platform,
scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow},
};
use cocoa::foundation::NSUInteger;
use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
use shaders::ToFloat2 as _;
use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec};
const SHADERS_METALLIB: &'static [u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value.
pub struct Renderer {
sprite_cache: SpriteCache,
image_cache: ImageCache,
path_atlases: AtlasAllocator,
quad_pipeline_state: metal::RenderPipelineState,
shadow_pipeline_state: metal::RenderPipelineState,
sprite_pipeline_state: metal::RenderPipelineState,
image_pipeline_state: metal::RenderPipelineState,
path_atlas_pipeline_state: metal::RenderPipelineState,
unit_vertices: metal::Buffer,
instances: metal::Buffer,
}
struct PathSprite {
layer_id: usize,
atlas_id: usize,
shader_data: shaders::GPUISprite,
}
impl Renderer {
pub fn new(
device: metal::Device,
pixel_format: metal::MTLPixelFormat,
fonts: Arc<dyn platform::FontSystem>,
) -> Self {
let library = device
.new_library_with_data(SHADERS_METALLIB)
.expect("error building metal library");
let unit_vertices = [
(0., 0.).to_float2(),
(1., 0.).to_float2(),
(0., 1.).to_float2(),
(0., 1.).to_float2(),
(1., 0.).to_float2(),
(1., 1.).to_float2(),
];
let unit_vertices = device.new_buffer_with_data(
unit_vertices.as_ptr() as *const c_void,
(unit_vertices.len() * mem::size_of::<shaders::vector_float2>()) as u64,
MTLResourceOptions::StorageModeManaged,
);
let instances = device.new_buffer(
INSTANCE_BUFFER_SIZE as u64,
MTLResourceOptions::StorageModeManaged,
);
let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), fonts);
let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768));
let path_atlases =
AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
let quad_pipeline_state = build_pipeline_state(
&device,
&library,
"quad",
"quad_vertex",
"quad_fragment",
pixel_format,
);
let shadow_pipeline_state = build_pipeline_state(
&device,
&library,
"shadow",
"shadow_vertex",
"shadow_fragment",
pixel_format,
);
let sprite_pipeline_state = build_pipeline_state(
&device,
&library,
"sprite",
"sprite_vertex",
"sprite_fragment",
pixel_format,
);
let image_pipeline_state = build_pipeline_state(
&device,
&library,
"image",
"image_vertex",
"image_fragment",
pixel_format,
);
let path_atlas_pipeline_state = build_path_atlas_pipeline_state(
&device,
&library,
"path_atlas",
"path_atlas_vertex",
"path_atlas_fragment",
MTLPixelFormat::R8Unorm,
);
Self {
sprite_cache,
image_cache,
path_atlases,
quad_pipeline_state,
shadow_pipeline_state,
sprite_pipeline_state,
image_pipeline_state,
path_atlas_pipeline_state,
unit_vertices,
instances,
}
}
pub fn render(
&mut self,
scene: &Scene,
drawable_size: Vector2F,
command_buffer: &metal::CommandBufferRef,
output: &metal::TextureRef,
) {
let mut offset = 0;
let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer);
self.render_layers(
scene,
path_sprites,
&mut offset,
drawable_size,
command_buffer,
output,
);
self.instances.did_modify_range(NSRange {
location: 0,
length: offset as NSUInteger,
});
self.image_cache.finish_frame();
}
fn render_path_atlases(
&mut self,
scene: &Scene,
offset: &mut usize,
command_buffer: &metal::CommandBufferRef,
) -> Vec<PathSprite> {
self.path_atlases.clear();
let mut sprites = Vec::new();
let mut vertices = Vec::<shaders::GPUIPathVertex>::new();
let mut current_atlas_id = None;
for (layer_id, layer) in scene.layers().enumerate() {
for path in layer.paths() {
let origin = path.bounds.origin() * scene.scale_factor();
let size = (path.bounds.size() * scene.scale_factor()).ceil();
let (alloc_id, atlas_origin) = self.path_atlases.allocate(size.to_i32());
let atlas_origin = atlas_origin.to_f32();
sprites.push(PathSprite {
layer_id,
atlas_id: alloc_id.atlas_id,
shader_data: shaders::GPUISprite {
origin: origin.floor().to_float2(),
target_size: size.to_float2(),
source_size: size.to_float2(),
atlas_origin: atlas_origin.to_float2(),
color: path.color.to_uchar4(),
compute_winding: 1,
},
});
if let Some(current_atlas_id) = current_atlas_id {
if alloc_id.atlas_id != current_atlas_id {
self.render_paths_to_atlas(
offset,
&vertices,
current_atlas_id,
command_buffer,
);
vertices.clear();
}
}
current_atlas_id = Some(alloc_id.atlas_id);
for vertex in &path.vertices {
let xy_position =
(vertex.xy_position - path.bounds.origin()) * scene.scale_factor();
vertices.push(shaders::GPUIPathVertex {
xy_position: (atlas_origin + xy_position).to_float2(),
st_position: vertex.st_position.to_float2(),
clip_rect_origin: atlas_origin.to_float2(),
clip_rect_size: size.to_float2(),
});
}
}
}
if let Some(atlas_id) = current_atlas_id {
self.render_paths_to_atlas(offset, &vertices, atlas_id, command_buffer);
}
sprites
}
fn render_paths_to_atlas(
&mut self,
offset: &mut usize,
vertices: &[shaders::GPUIPathVertex],
atlas_id: usize,
command_buffer: &metal::CommandBufferRef,
) {
align_offset(offset);
let next_offset = *offset + vertices.len() * mem::size_of::<shaders::GPUIPathVertex>();
assert!(
next_offset <= INSTANCE_BUFFER_SIZE,
"instance buffer exhausted"
);
let render_pass_descriptor = metal::RenderPassDescriptor::new();
let color_attachment = render_pass_descriptor
.color_attachments()
.object_at(0)
.unwrap();
let texture = self.path_atlases.texture(atlas_id).unwrap();
color_attachment.set_texture(Some(texture));
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_store_action(metal::MTLStoreAction::Store);
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
let path_atlas_command_encoder =
command_buffer.new_render_command_encoder(render_pass_descriptor);
path_atlas_command_encoder.set_render_pipeline_state(&self.path_atlas_pipeline_state);
path_atlas_command_encoder.set_vertex_buffer(
shaders::GPUIPathAtlasVertexInputIndex_GPUIPathAtlasVertexInputIndexVertices as u64,
Some(&self.instances),
*offset as u64,
);
path_atlas_command_encoder.set_vertex_bytes(
shaders::GPUIPathAtlasVertexInputIndex_GPUIPathAtlasVertexInputIndexAtlasSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
as *const c_void,
);
let buffer_contents = unsafe {
(self.instances.contents() as *mut u8).add(*offset) as *mut shaders::GPUIPathVertex
};
for (ix, vertex) in vertices.iter().enumerate() {
unsafe {
*buffer_contents.add(ix) = *vertex;
}
}
path_atlas_command_encoder.draw_primitives(
metal::MTLPrimitiveType::Triangle,
0,
vertices.len() as u64,
);
path_atlas_command_encoder.end_encoding();
*offset = next_offset;
}
fn render_layers(
&mut self,
scene: &Scene,
path_sprites: Vec<PathSprite>,
offset: &mut usize,
drawable_size: Vector2F,
command_buffer: &metal::CommandBufferRef,
output: &metal::TextureRef,
) {
let render_pass_descriptor = metal::RenderPassDescriptor::new();
let color_attachment = render_pass_descriptor
.color_attachments()
.object_at(0)
.unwrap();
color_attachment.set_texture(Some(output));
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_store_action(metal::MTLStoreAction::Store);
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
command_encoder.set_viewport(metal::MTLViewport {
originX: 0.0,
originY: 0.0,
width: drawable_size.x() as f64,
height: drawable_size.y() as f64,
znear: 0.0,
zfar: 1.0,
});
let scale_factor = scene.scale_factor();
let mut path_sprites = path_sprites.into_iter().peekable();
for (layer_id, layer) in scene.layers().enumerate() {
self.clip(scene, layer, drawable_size, command_encoder);
self.render_shadows(
layer.shadows(),
scale_factor,
offset,
drawable_size,
command_encoder,
);
self.render_quads(
layer.quads(),
scale_factor,
offset,
drawable_size,
command_encoder,
);
self.render_path_sprites(
layer_id,
&mut path_sprites,
offset,
drawable_size,
command_encoder,
);
self.render_sprites(
layer.glyphs(),
layer.icons(),
scale_factor,
offset,
drawable_size,
command_encoder,
);
self.render_images(
layer.images(),
scale_factor,
offset,
drawable_size,
command_encoder,
);
self.render_quads(
layer.underlines(),
scale_factor,
offset,
drawable_size,
command_encoder,
);
}
command_encoder.end_encoding();
}
fn clip(
&mut self,
scene: &Scene,
layer: &Layer,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
let clip_bounds = (layer.clip_bounds().unwrap_or(RectF::new(
vec2f(0., 0.),
drawable_size / scene.scale_factor(),
)) * scene.scale_factor())
.round();
command_encoder.set_scissor_rect(metal::MTLScissorRect {
x: clip_bounds.origin_x() as NSUInteger,
y: clip_bounds.origin_y() as NSUInteger,
width: clip_bounds.width() as NSUInteger,
height: clip_bounds.height() as NSUInteger,
});
}
fn render_shadows(
&mut self,
shadows: &[Shadow],
scale_factor: f32,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if shadows.is_empty() {
return;
}
align_offset(offset);
let next_offset = *offset + shadows.len() * mem::size_of::<shaders::GPUIShadow>();
assert!(
next_offset <= INSTANCE_BUFFER_SIZE,
"instance buffer exhausted"
);
command_encoder.set_render_pipeline_state(&self.shadow_pipeline_state);
command_encoder.set_vertex_buffer(
shaders::GPUIShadowInputIndex_GPUIShadowInputIndexVertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_buffer(
shaders::GPUIShadowInputIndex_GPUIShadowInputIndexShadows as u64,
Some(&self.instances),
*offset as u64,
);
command_encoder.set_vertex_bytes(
shaders::GPUIShadowInputIndex_GPUIShadowInputIndexUniforms as u64,
mem::size_of::<shaders::GPUIUniforms>() as u64,
[shaders::GPUIUniforms {
viewport_size: drawable_size.to_float2(),
}]
.as_ptr() as *const c_void,
);
let buffer_contents = unsafe {
(self.instances.contents() as *mut u8).offset(*offset as isize)
as *mut shaders::GPUIShadow
};
for (ix, shadow) in shadows.iter().enumerate() {
let shape_bounds = shadow.bounds * scale_factor;
let shader_shadow = shaders::GPUIShadow {
origin: shape_bounds.origin().to_float2(),
size: shape_bounds.size().to_float2(),
corner_radius: shadow.corner_radius * scale_factor,
sigma: shadow.sigma,
color: shadow.color.to_uchar4(),
};
unsafe {
*(buffer_contents.offset(ix as isize)) = shader_shadow;
}
}
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
0,
6,
shadows.len() as u64,
);
*offset = next_offset;
}
fn render_quads(
&mut self,
quads: &[Quad],
scale_factor: f32,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if quads.is_empty() {
return;
}
align_offset(offset);
let next_offset = *offset + quads.len() * mem::size_of::<shaders::GPUIQuad>();
assert!(
next_offset <= INSTANCE_BUFFER_SIZE,
"instance buffer exhausted"
);
command_encoder.set_render_pipeline_state(&self.quad_pipeline_state);
command_encoder.set_vertex_buffer(
shaders::GPUIQuadInputIndex_GPUIQuadInputIndexVertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_buffer(
shaders::GPUIQuadInputIndex_GPUIQuadInputIndexQuads as u64,
Some(&self.instances),
*offset as u64,
);
command_encoder.set_vertex_bytes(
shaders::GPUIQuadInputIndex_GPUIQuadInputIndexUniforms as u64,
mem::size_of::<shaders::GPUIUniforms>() as u64,
[shaders::GPUIUniforms {
viewport_size: drawable_size.to_float2(),
}]
.as_ptr() as *const c_void,
);
let buffer_contents = unsafe {
(self.instances.contents() as *mut u8).offset(*offset as isize)
as *mut shaders::GPUIQuad
};
for (ix, quad) in quads.iter().enumerate() {
let bounds = quad.bounds * scale_factor;
let border_width = quad.border.width * scale_factor;
let shader_quad = shaders::GPUIQuad {
origin: bounds.origin().round().to_float2(),
size: bounds.size().round().to_float2(),
background_color: quad
.background
.unwrap_or(Color::transparent_black())
.to_uchar4(),
border_top: border_width * (quad.border.top as usize as f32),
border_right: border_width * (quad.border.right as usize as f32),
border_bottom: border_width * (quad.border.bottom as usize as f32),
border_left: border_width * (quad.border.left as usize as f32),
border_color: quad.border.color.to_uchar4(),
corner_radius: quad.corner_radius * scale_factor,
};
unsafe {
*(buffer_contents.offset(ix as isize)) = shader_quad;
}
}
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
0,
6,
quads.len() as u64,
);
*offset = next_offset;
}
fn render_sprites(
&mut self,
glyphs: &[Glyph],
icons: &[Icon],
scale_factor: f32,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if glyphs.is_empty() && icons.is_empty() {
return;
}
let mut sprites_by_atlas = HashMap::new();
for glyph in glyphs {
if let Some(sprite) = self.sprite_cache.render_glyph(
glyph.font_id,
glyph.font_size,
glyph.id,
glyph.origin,
scale_factor,
) {
// Snap sprite to pixel grid.
let origin = (glyph.origin * scale_factor).floor() + sprite.offset.to_f32();
sprites_by_atlas
.entry(sprite.atlas_id)
.or_insert_with(Vec::new)
.push(shaders::GPUISprite {
origin: origin.to_float2(),
target_size: sprite.size.to_float2(),
source_size: sprite.size.to_float2(),
atlas_origin: sprite.atlas_origin.to_float2(),
color: glyph.color.to_uchar4(),
compute_winding: 0,
});
}
}
for icon in icons {
let origin = icon.bounds.origin() * scale_factor;
let target_size = icon.bounds.size() * scale_factor;
let source_size = (target_size * 2.).ceil().to_i32();
let sprite =
self.sprite_cache
.render_icon(source_size, icon.path.clone(), icon.svg.clone());
sprites_by_atlas
.entry(sprite.atlas_id)
.or_insert_with(Vec::new)
.push(shaders::GPUISprite {
origin: origin.to_float2(),
target_size: target_size.to_float2(),
source_size: sprite.size.to_float2(),
atlas_origin: sprite.atlas_origin.to_float2(),
color: icon.color.to_uchar4(),
compute_winding: 0,
});
}
command_encoder.set_render_pipeline_state(&self.sprite_pipeline_state);
command_encoder.set_vertex_buffer(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexVertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_bytes(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexViewportSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[drawable_size.to_float2()].as_ptr() as *const c_void,
);
for (atlas_id, sprites) in sprites_by_atlas {
align_offset(offset);
let next_offset = *offset + sprites.len() * mem::size_of::<shaders::GPUISprite>();
assert!(
next_offset <= INSTANCE_BUFFER_SIZE,
"instance buffer exhausted"
);
let texture = self.sprite_cache.atlas_texture(atlas_id).unwrap();
command_encoder.set_vertex_buffer(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexSprites as u64,
Some(&self.instances),
*offset as u64,
);
command_encoder.set_vertex_bytes(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
as *const c_void,
);
command_encoder.set_fragment_texture(
shaders::GPUISpriteFragmentInputIndex_GPUISpriteFragmentInputIndexAtlas as u64,
Some(texture),
);
unsafe {
let buffer_contents = (self.instances.contents() as *mut u8)
.offset(*offset as isize)
as *mut shaders::GPUISprite;
std::ptr::copy_nonoverlapping(sprites.as_ptr(), buffer_contents, sprites.len());
}
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
0,
6,
sprites.len() as u64,
);
*offset = next_offset;
}
}
fn render_images(
&mut self,
images: &[Image],
scale_factor: f32,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
if images.is_empty() {
return;
}
let mut images_by_atlas = HashMap::new();
for image in images {
let origin = image.bounds.origin() * scale_factor;
let target_size = image.bounds.size() * scale_factor;
let corner_radius = image.corner_radius * scale_factor;
let border_width = image.border.width * scale_factor;
let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data);
images_by_atlas
.entry(alloc_id.atlas_id)
.or_insert_with(Vec::new)
.push(shaders::GPUIImage {
origin: origin.to_float2(),
target_size: target_size.to_float2(),
source_size: atlas_bounds.size().to_float2(),
atlas_origin: atlas_bounds.origin().to_float2(),
border_top: border_width * (image.border.top as usize as f32),
border_right: border_width * (image.border.right as usize as f32),
border_bottom: border_width * (image.border.bottom as usize as f32),
border_left: border_width * (image.border.left as usize as f32),
border_color: image.border.color.to_uchar4(),
corner_radius,
});
}
command_encoder.set_render_pipeline_state(&self.image_pipeline_state);
command_encoder.set_vertex_buffer(
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_bytes(
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexViewportSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[drawable_size.to_float2()].as_ptr() as *const c_void,
);
for (atlas_id, images) in images_by_atlas {
align_offset(offset);
let next_offset = *offset + images.len() * mem::size_of::<shaders::GPUIImage>();
assert!(
next_offset <= INSTANCE_BUFFER_SIZE,
"instance buffer exhausted"
);
let texture = self.image_cache.atlas_texture(atlas_id).unwrap();
command_encoder.set_vertex_buffer(
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexImages as u64,
Some(&self.instances),
*offset as u64,
);
command_encoder.set_vertex_bytes(
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexAtlasSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
as *const c_void,
);
command_encoder.set_fragment_texture(
shaders::GPUIImageFragmentInputIndex_GPUIImageFragmentInputIndexAtlas as u64,
Some(texture),
);
unsafe {
let buffer_contents = (self.instances.contents() as *mut u8)
.offset(*offset as isize)
as *mut shaders::GPUIImage;
std::ptr::copy_nonoverlapping(images.as_ptr(), buffer_contents, images.len());
}
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
0,
6,
images.len() as u64,
);
*offset = next_offset;
}
}
fn render_path_sprites(
&mut self,
layer_id: usize,
sprites: &mut Peekable<vec::IntoIter<PathSprite>>,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
command_encoder.set_render_pipeline_state(&self.sprite_pipeline_state);
command_encoder.set_vertex_buffer(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexVertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_bytes(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexViewportSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[drawable_size.to_float2()].as_ptr() as *const c_void,
);
let mut atlas_id = None;
let mut atlas_sprite_count = 0;
align_offset(offset);
while let Some(sprite) = sprites.peek() {
if sprite.layer_id != layer_id {
break;
}
let sprite = sprites.next().unwrap();
if let Some(atlas_id) = atlas_id.as_mut() {
if sprite.atlas_id != *atlas_id {
self.render_path_sprites_for_atlas(
offset,
*atlas_id,
atlas_sprite_count,
command_encoder,
);
*atlas_id = sprite.atlas_id;
atlas_sprite_count = 0;
align_offset(offset);
}
} else {
atlas_id = Some(sprite.atlas_id);
}
unsafe {
let buffer_contents = (self.instances.contents() as *mut u8)
.offset(*offset as isize)
as *mut shaders::GPUISprite;
*buffer_contents.offset(atlas_sprite_count as isize) = sprite.shader_data;
}
atlas_sprite_count += 1;
}
if let Some(atlas_id) = atlas_id {
self.render_path_sprites_for_atlas(
offset,
atlas_id,
atlas_sprite_count,
command_encoder,
);
}
}
fn render_path_sprites_for_atlas(
&mut self,
offset: &mut usize,
atlas_id: usize,
sprite_count: usize,
command_encoder: &metal::RenderCommandEncoderRef,
) {
let next_offset = *offset + sprite_count * mem::size_of::<shaders::GPUISprite>();
assert!(
next_offset <= INSTANCE_BUFFER_SIZE,
"instance buffer exhausted"
);
command_encoder.set_vertex_buffer(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexSprites as u64,
Some(&self.instances),
*offset as u64,
);
let texture = self.path_atlases.texture(atlas_id).unwrap();
command_encoder.set_fragment_texture(
shaders::GPUISpriteFragmentInputIndex_GPUISpriteFragmentInputIndexAtlas as u64,
Some(texture),
);
command_encoder.set_vertex_bytes(
shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexAtlasSize as u64,
mem::size_of::<shaders::vector_float2>() as u64,
[vec2i(texture.width() as i32, texture.height() as i32).to_float2()].as_ptr()
as *const c_void,
);
command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle,
0,
6,
sprite_count as u64,
);
*offset = next_offset;
}
}
fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor {
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(2048);
texture_descriptor.set_height(2048);
texture_descriptor.set_pixel_format(MTLPixelFormat::R8Unorm);
texture_descriptor
.set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
texture_descriptor
}
fn align_offset(offset: &mut usize) {
let r = *offset % 256;
if r > 0 {
*offset += 256 - r; // Align to a multiple of 256 to make Metal happy
}
}
fn build_pipeline_state(
device: &metal::DeviceRef,
library: &metal::LibraryRef,
label: &str,
vertex_fn_name: &str,
fragment_fn_name: &str,
pixel_format: metal::MTLPixelFormat,
) -> metal::RenderPipelineState {
let vertex_fn = library
.get_function(vertex_fn_name, None)
.expect("error locating vertex function");
let fragment_fn = library
.get_function(fragment_fn_name, None)
.expect("error locating fragment function");
let descriptor = metal::RenderPipelineDescriptor::new();
descriptor.set_label(label);
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
color_attachment.set_pixel_format(pixel_format);
color_attachment.set_blending_enabled(true);
color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
device
.new_render_pipeline_state(&descriptor)
.expect("could not create render pipeline state")
}
fn build_path_atlas_pipeline_state(
device: &metal::DeviceRef,
library: &metal::LibraryRef,
label: &str,
vertex_fn_name: &str,
fragment_fn_name: &str,
pixel_format: metal::MTLPixelFormat,
) -> metal::RenderPipelineState {
let vertex_fn = library
.get_function(vertex_fn_name, None)
.expect("error locating vertex function");
let fragment_fn = library
.get_function(fragment_fn_name, None)
.expect("error locating fragment function");
let descriptor = metal::RenderPipelineDescriptor::new();
descriptor.set_label(label);
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
color_attachment.set_pixel_format(pixel_format);
color_attachment.set_blending_enabled(true);
color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::One);
color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
device
.new_render_pipeline_state(&descriptor)
.expect("could not create render pipeline state")
}
mod shaders {
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use crate::{
color::Color,
geometry::vector::{Vector2F, Vector2I},
};
use std::mem;
include!(concat!(env!("OUT_DIR"), "/shaders.rs"));
pub trait ToFloat2 {
fn to_float2(&self) -> vector_float2;
}
impl ToFloat2 for (f32, f32) {
fn to_float2(&self) -> vector_float2 {
unsafe {
let mut output = mem::transmute::<_, u32>(self.1.to_bits()) as vector_float2;
output <<= 32;
output |= mem::transmute::<_, u32>(self.0.to_bits()) as vector_float2;
output
}
}
}
impl ToFloat2 for Vector2F {
fn to_float2(&self) -> vector_float2 {
unsafe {
let mut output = mem::transmute::<_, u32>(self.y().to_bits()) as vector_float2;
output <<= 32;
output |= mem::transmute::<_, u32>(self.x().to_bits()) as vector_float2;
output
}
}
}
impl ToFloat2 for Vector2I {
fn to_float2(&self) -> vector_float2 {
self.to_f32().to_float2()
}
}
impl Color {
pub fn to_uchar4(&self) -> vector_uchar4 {
let mut vec = self.a as vector_uchar4;
vec <<= 8;
vec |= self.b as vector_uchar4;
vec <<= 8;
vec |= self.g as vector_uchar4;
vec <<= 8;
vec |= self.r as vector_uchar4;
vec
}
}
}

View file

@ -0,0 +1,106 @@
#include <simd/simd.h>
typedef struct
{
vector_float2 viewport_size;
} GPUIUniforms;
typedef enum
{
GPUIQuadInputIndexVertices = 0,
GPUIQuadInputIndexQuads = 1,
GPUIQuadInputIndexUniforms = 2,
} GPUIQuadInputIndex;
typedef struct
{
vector_float2 origin;
vector_float2 size;
vector_uchar4 background_color;
float border_top;
float border_right;
float border_bottom;
float border_left;
vector_uchar4 border_color;
float corner_radius;
} GPUIQuad;
typedef enum
{
GPUIShadowInputIndexVertices = 0,
GPUIShadowInputIndexShadows = 1,
GPUIShadowInputIndexUniforms = 2,
} GPUIShadowInputIndex;
typedef struct
{
vector_float2 origin;
vector_float2 size;
float corner_radius;
float sigma;
vector_uchar4 color;
} GPUIShadow;
typedef enum
{
GPUISpriteVertexInputIndexVertices = 0,
GPUISpriteVertexInputIndexSprites = 1,
GPUISpriteVertexInputIndexViewportSize = 2,
GPUISpriteVertexInputIndexAtlasSize = 3,
} GPUISpriteVertexInputIndex;
typedef enum
{
GPUISpriteFragmentInputIndexAtlas = 0,
} GPUISpriteFragmentInputIndex;
typedef struct
{
vector_float2 origin;
vector_float2 target_size;
vector_float2 source_size;
vector_float2 atlas_origin;
vector_uchar4 color;
uint8_t compute_winding;
} GPUISprite;
typedef enum
{
GPUIPathAtlasVertexInputIndexVertices = 0,
GPUIPathAtlasVertexInputIndexAtlasSize = 1,
} GPUIPathAtlasVertexInputIndex;
typedef struct
{
vector_float2 xy_position;
vector_float2 st_position;
vector_float2 clip_rect_origin;
vector_float2 clip_rect_size;
} GPUIPathVertex;
typedef enum
{
GPUIImageVertexInputIndexVertices = 0,
GPUIImageVertexInputIndexImages = 1,
GPUIImageVertexInputIndexViewportSize = 2,
GPUIImageVertexInputIndexAtlasSize = 3,
} GPUIImageVertexInputIndex;
typedef enum
{
GPUIImageFragmentInputIndexAtlas = 0,
} GPUIImageFragmentInputIndex;
typedef struct
{
vector_float2 origin;
vector_float2 target_size;
vector_float2 source_size;
vector_float2 atlas_origin;
float border_top;
float border_right;
float border_bottom;
float border_left;
vector_uchar4 border_color;
float corner_radius;
} GPUIImage;

View file

@ -0,0 +1,308 @@
#include <metal_stdlib>
#include "shaders.h"
using namespace metal;
float4 coloru_to_colorf(uchar4 coloru) {
return float4(coloru) / float4(0xff, 0xff, 0xff, 0xff);
}
float4 to_device_position(float2 pixel_position, float2 viewport_size) {
return float4(pixel_position / viewport_size * float2(2., -2.) + float2(-1., 1.), 0., 1.);
}
// A standard gaussian function, used for weighting samples
float gaussian(float x, float sigma) {
return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma);
}
// This approximates the error function, needed for the gaussian integral
float2 erf(float2 x) {
float2 s = sign(x);
float2 a = abs(x);
x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
x *= x;
return s - s / (x * x);
}
float blur_along_x(float x, float y, float sigma, float corner, float2 halfSize) {
float delta = min(halfSize.y - corner - abs(y), 0.);
float curved = halfSize.x - corner + sqrt(max(0., corner * corner - delta * delta));
float2 integral = 0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma));
return integral.y - integral.x;
}
struct QuadFragmentInput {
float4 position [[position]];
float2 atlas_position; // only used in the image shader
float2 origin;
float2 size;
float4 background_color;
float border_top;
float border_right;
float border_bottom;
float border_left;
float4 border_color;
float corner_radius;
};
float4 quad_sdf(QuadFragmentInput input) {
float2 half_size = input.size / 2.;
float2 center = input.origin + half_size;
float2 center_to_point = input.position.xy - center;
float2 rounded_edge_to_point = abs(center_to_point) - half_size + input.corner_radius;
float distance = length(max(0., rounded_edge_to_point)) + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - input.corner_radius;
float vertical_border = center_to_point.x <= 0. ? input.border_left : input.border_right;
float horizontal_border = center_to_point.y <= 0. ? input.border_top : input.border_bottom;
float2 inset_size = half_size - input.corner_radius - float2(vertical_border, horizontal_border);
float2 point_to_inset_corner = abs(center_to_point) - inset_size;
float border_width;
if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) {
border_width = 0.;
} else if (point_to_inset_corner.y > point_to_inset_corner.x) {
border_width = horizontal_border;
} else {
border_width = vertical_border;
}
float4 color;
if (border_width == 0.) {
color = input.background_color;
} else {
float4 border_color = float4(mix(float3(input.background_color), float3(input.border_color), input.border_color.a), 1.);
float inset_distance = distance + border_width;
color = mix(
border_color,
input.background_color,
saturate(0.5 - inset_distance)
);
}
float4 coverage = float4(1., 1., 1., saturate(0.5 - distance));
return coverage * color;
}
vertex QuadFragmentInput quad_vertex(
uint unit_vertex_id [[vertex_id]],
uint quad_id [[instance_id]],
constant float2 *unit_vertices [[buffer(GPUIQuadInputIndexVertices)]],
constant GPUIQuad *quads [[buffer(GPUIQuadInputIndexQuads)]],
constant GPUIUniforms *uniforms [[buffer(GPUIQuadInputIndexUniforms)]]
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUIQuad quad = quads[quad_id];
float2 position = unit_vertex * quad.size + quad.origin;
float4 device_position = to_device_position(position, uniforms->viewport_size);
return QuadFragmentInput {
device_position,
float2(0., 0.),
quad.origin,
quad.size,
coloru_to_colorf(quad.background_color),
quad.border_top,
quad.border_right,
quad.border_bottom,
quad.border_left,
coloru_to_colorf(quad.border_color),
quad.corner_radius,
};
}
fragment float4 quad_fragment(
QuadFragmentInput input [[stage_in]]
) {
return quad_sdf(input);
}
struct ShadowFragmentInput {
float4 position [[position]];
vector_float2 origin;
vector_float2 size;
float corner_radius;
float sigma;
vector_uchar4 color;
};
vertex ShadowFragmentInput shadow_vertex(
uint unit_vertex_id [[vertex_id]],
uint shadow_id [[instance_id]],
constant float2 *unit_vertices [[buffer(GPUIShadowInputIndexVertices)]],
constant GPUIShadow *shadows [[buffer(GPUIShadowInputIndexShadows)]],
constant GPUIUniforms *uniforms [[buffer(GPUIShadowInputIndexUniforms)]]
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUIShadow shadow = shadows[shadow_id];
float margin = 3. * shadow.sigma;
float2 position = unit_vertex * (shadow.size + 2. * margin) + shadow.origin - margin;
float4 device_position = to_device_position(position, uniforms->viewport_size);
return ShadowFragmentInput {
device_position,
shadow.origin,
shadow.size,
shadow.corner_radius,
shadow.sigma,
shadow.color,
};
}
fragment float4 shadow_fragment(
ShadowFragmentInput input [[stage_in]]
) {
float sigma = input.sigma;
float corner_radius = input.corner_radius;
float2 half_size = input.size / 2.;
float2 center = input.origin + half_size;
float2 point = input.position.xy - center;
// The signal is only non-zero in a limited range, so don't waste samples
float low = point.y - half_size.y;
float high = point.y + half_size.y;
float start = clamp(-3. * sigma, low, high);
float end = clamp(3. * sigma, low, high);
// Accumulate samples (we can get away with surprisingly few samples)
float step = (end - start) / 4.;
float y = start + step * 0.5;
float alpha = 0.;
for (int i = 0; i < 4; i++) {
alpha += blur_along_x(point.x, point.y - y, sigma, corner_radius, half_size) * gaussian(y, sigma) * step;
y += step;
}
return float4(1., 1., 1., alpha) * coloru_to_colorf(input.color);
}
struct SpriteFragmentInput {
float4 position [[position]];
float2 atlas_position;
float4 color [[flat]];
uchar compute_winding [[flat]];
};
vertex SpriteFragmentInput sprite_vertex(
uint unit_vertex_id [[vertex_id]],
uint sprite_id [[instance_id]],
constant float2 *unit_vertices [[buffer(GPUISpriteVertexInputIndexVertices)]],
constant GPUISprite *sprites [[buffer(GPUISpriteVertexInputIndexSprites)]],
constant float2 *viewport_size [[buffer(GPUISpriteVertexInputIndexViewportSize)]],
constant float2 *atlas_size [[buffer(GPUISpriteVertexInputIndexAtlasSize)]]
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUISprite sprite = sprites[sprite_id];
float2 position = unit_vertex * sprite.target_size + sprite.origin;
float4 device_position = to_device_position(position, *viewport_size);
float2 atlas_position = (unit_vertex * sprite.source_size + sprite.atlas_origin) / *atlas_size;
return SpriteFragmentInput {
device_position,
atlas_position,
coloru_to_colorf(sprite.color),
sprite.compute_winding
};
}
#define MAX_WINDINGS 32.
fragment float4 sprite_fragment(
SpriteFragmentInput input [[stage_in]],
texture2d<float> atlas [[ texture(GPUISpriteFragmentInputIndexAtlas) ]]
) {
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
float4 color = input.color;
float4 sample = atlas.sample(atlas_sampler, input.atlas_position);
float mask;
if (input.compute_winding) {
mask = 1. - abs(1. - fmod(sample.r * MAX_WINDINGS, 2.));
} else {
mask = sample.a;
}
color.a *= mask;
return color;
}
vertex QuadFragmentInput image_vertex(
uint unit_vertex_id [[vertex_id]],
uint image_id [[instance_id]],
constant float2 *unit_vertices [[buffer(GPUIImageVertexInputIndexVertices)]],
constant GPUIImage *images [[buffer(GPUIImageVertexInputIndexImages)]],
constant float2 *viewport_size [[buffer(GPUIImageVertexInputIndexViewportSize)]],
constant float2 *atlas_size [[buffer(GPUIImageVertexInputIndexAtlasSize)]]
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUIImage image = images[image_id];
float2 position = unit_vertex * image.target_size + image.origin;
float4 device_position = to_device_position(position, *viewport_size);
float2 atlas_position = (unit_vertex * image.source_size + image.atlas_origin) / *atlas_size;
return QuadFragmentInput {
device_position,
atlas_position,
image.origin,
image.target_size,
float4(0.),
image.border_top,
image.border_right,
image.border_bottom,
image.border_left,
coloru_to_colorf(image.border_color),
image.corner_radius,
};
}
fragment float4 image_fragment(
QuadFragmentInput input [[stage_in]],
texture2d<float> atlas [[ texture(GPUIImageFragmentInputIndexAtlas) ]]
) {
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
return quad_sdf(input);
}
struct PathAtlasVertexOutput {
float4 position [[position]];
float2 st_position;
float clip_rect_distance [[clip_distance]] [4];
};
struct PathAtlasFragmentInput {
float4 position [[position]];
float2 st_position;
};
vertex PathAtlasVertexOutput path_atlas_vertex(
uint vertex_id [[vertex_id]],
constant GPUIPathVertex *vertices [[buffer(GPUIPathAtlasVertexInputIndexVertices)]],
constant float2 *atlas_size [[buffer(GPUIPathAtlasVertexInputIndexAtlasSize)]]
) {
GPUIPathVertex v = vertices[vertex_id];
float4 device_position = to_device_position(v.xy_position, *atlas_size);
return PathAtlasVertexOutput {
device_position,
v.st_position,
{
v.xy_position.x - v.clip_rect_origin.x,
v.clip_rect_origin.x + v.clip_rect_size.x - v.xy_position.x,
v.xy_position.y - v.clip_rect_origin.y,
v.clip_rect_origin.y + v.clip_rect_size.y - v.xy_position.y
}
};
}
fragment float4 path_atlas_fragment(
PathAtlasFragmentInput input [[stage_in]]
) {
float2 dx = dfdx(input.st_position);
float2 dy = dfdy(input.st_position);
float2 gradient = float2(
(2. * input.st_position.x) * dx.x - dx.y,
(2. * input.st_position.x) * dy.x - dy.y
);
float f = (input.st_position.x * input.st_position.x) - input.st_position.y;
float distance = f / length(gradient);
float alpha = saturate(0.5 - distance) / MAX_WINDINGS;
return float4(alpha, 0., 0., 1.);
}

View file

@ -0,0 +1,151 @@
use super::atlas::AtlasAllocator;
use crate::{
fonts::{FontId, GlyphId},
geometry::vector::{vec2f, Vector2F, Vector2I},
platform,
};
use metal::{MTLPixelFormat, TextureDescriptor};
use ordered_float::OrderedFloat;
use std::{borrow::Cow, collections::HashMap, sync::Arc};
#[derive(Hash, Eq, PartialEq)]
struct GlyphDescriptor {
font_id: FontId,
font_size: OrderedFloat<f32>,
glyph_id: GlyphId,
subpixel_variant: (u8, u8),
}
#[derive(Clone)]
pub struct GlyphSprite {
pub atlas_id: usize,
pub atlas_origin: Vector2I,
pub offset: Vector2I,
pub size: Vector2I,
}
#[derive(Hash, Eq, PartialEq)]
struct IconDescriptor {
path: Cow<'static, str>,
width: i32,
height: i32,
}
#[derive(Clone)]
pub struct IconSprite {
pub atlas_id: usize,
pub atlas_origin: Vector2I,
pub size: Vector2I,
}
pub struct SpriteCache {
fonts: Arc<dyn platform::FontSystem>,
atlases: AtlasAllocator,
glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
icons: HashMap<IconDescriptor, IconSprite>,
}
impl SpriteCache {
pub fn new(
device: metal::Device,
size: Vector2I,
fonts: Arc<dyn platform::FontSystem>,
) -> Self {
let descriptor = TextureDescriptor::new();
descriptor.set_pixel_format(MTLPixelFormat::A8Unorm);
descriptor.set_width(size.x() as u64);
descriptor.set_height(size.y() as u64);
Self {
fonts,
atlases: AtlasAllocator::new(device, descriptor),
glyphs: Default::default(),
icons: Default::default(),
}
}
pub fn render_glyph(
&mut self,
font_id: FontId,
font_size: f32,
glyph_id: GlyphId,
target_position: Vector2F,
scale_factor: f32,
) -> Option<GlyphSprite> {
const SUBPIXEL_VARIANTS: u8 = 4;
let target_position = target_position * scale_factor;
let fonts = &self.fonts;
let atlases = &mut self.atlases;
let subpixel_variant = (
(target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
% SUBPIXEL_VARIANTS,
(target_position.y().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
% SUBPIXEL_VARIANTS,
);
self.glyphs
.entry(GlyphDescriptor {
font_id,
font_size: OrderedFloat(font_size),
glyph_id,
subpixel_variant,
})
.or_insert_with(|| {
let subpixel_shift = vec2f(
subpixel_variant.0 as f32 / SUBPIXEL_VARIANTS as f32,
subpixel_variant.1 as f32 / SUBPIXEL_VARIANTS as f32,
);
let (glyph_bounds, mask) = fonts.rasterize_glyph(
font_id,
font_size,
glyph_id,
subpixel_shift,
scale_factor,
)?;
let (alloc_id, atlas_bounds) = atlases.upload(glyph_bounds.size(), &mask);
Some(GlyphSprite {
atlas_id: alloc_id.atlas_id,
atlas_origin: atlas_bounds.origin(),
offset: glyph_bounds.origin(),
size: glyph_bounds.size(),
})
})
.clone()
}
pub fn render_icon(
&mut self,
size: Vector2I,
path: Cow<'static, str>,
svg: usvg::Tree,
) -> IconSprite {
let atlases = &mut self.atlases;
self.icons
.entry(IconDescriptor {
path,
width: size.x(),
height: size.y(),
})
.or_insert_with(|| {
let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32).unwrap();
resvg::render(&svg, usvg::FitTo::Width(size.x() as u32), pixmap.as_mut());
let mask = pixmap
.pixels()
.iter()
.map(|a| a.alpha())
.collect::<Vec<_>>();
let (alloc_id, atlas_bounds) = atlases.upload(size, &mask);
IconSprite {
atlas_id: alloc_id.atlas_id,
atlas_origin: atlas_bounds.origin(),
size,
}
})
.clone()
}
pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
self.atlases.texture(atlas_id)
}
}

View file

@ -0,0 +1,644 @@
use crate::{
executor,
geometry::vector::Vector2F,
keymap::Keystroke,
platform::{self, Event, WindowContext},
Scene,
};
use block::ConcreteBlock;
use cocoa::{
appkit::{
CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask,
},
base::{id, nil},
foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString},
quartzcore::AutoresizingMask,
};
use core_graphics::display::CGRect;
use ctor::ctor;
use foreign_types::ForeignType as _;
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES},
sel, sel_impl,
};
use pathfinder_geometry::vector::vec2f;
use smol::Timer;
use std::{
any::Any,
cell::{Cell, RefCell},
convert::TryInto,
ffi::c_void,
mem, ptr,
rc::{Rc, Weak},
sync::Arc,
time::Duration,
};
use super::{geometry::RectFExt, renderer::Renderer};
const WINDOW_STATE_IVAR: &'static str = "windowState";
static mut WINDOW_CLASS: *const Class = ptr::null();
static mut VIEW_CLASS: *const Class = ptr::null();
#[allow(non_upper_case_globals)]
const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
#[ctor]
unsafe fn build_classes() {
WINDOW_CLASS = {
let mut decl = ClassDecl::new("GPUIWindow", class!(NSWindow)).unwrap();
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
decl.add_method(
sel!(canBecomeMainWindow),
yes as extern "C" fn(&Object, Sel) -> BOOL,
);
decl.add_method(
sel!(canBecomeKeyWindow),
yes as extern "C" fn(&Object, Sel) -> BOOL,
);
decl.add_method(
sel!(sendEvent:),
send_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidResize:),
window_did_resize as extern "C" fn(&Object, Sel, id),
);
decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
decl.register()
};
VIEW_CLASS = {
let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
decl.add_method(
sel!(keyDown:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseDown:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseUp:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseMoved:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseDragged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(scrollWheel:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(makeBackingLayer),
make_backing_layer as extern "C" fn(&Object, Sel) -> id,
);
decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
decl.add_method(
sel!(viewDidChangeBackingProperties),
view_did_change_backing_properties as extern "C" fn(&Object, Sel),
);
decl.add_method(
sel!(setFrameSize:),
set_frame_size as extern "C" fn(&Object, Sel, NSSize),
);
decl.add_method(
sel!(displayLayer:),
display_layer as extern "C" fn(&Object, Sel, id),
);
decl.register()
};
}
pub struct Window(Rc<RefCell<WindowState>>);
struct WindowState {
id: usize,
native_window: id,
event_callback: Option<Box<dyn FnMut(Event)>>,
resize_callback: Option<Box<dyn FnMut()>>,
close_callback: Option<Box<dyn FnOnce()>>,
synthetic_drag_counter: usize,
executor: Rc<executor::Foreground>,
scene_to_render: Option<Scene>,
renderer: Renderer,
command_queue: metal::CommandQueue,
last_fresh_keydown: Option<(Keystroke, String)>,
layer: id,
traffic_light_position: Option<Vector2F>,
}
impl Window {
pub fn open(
id: usize,
options: platform::WindowOptions,
executor: Rc<executor::Foreground>,
fonts: Arc<dyn platform::FontSystem>,
) -> Self {
const PIXEL_FORMAT: metal::MTLPixelFormat = metal::MTLPixelFormat::BGRA8Unorm;
unsafe {
let pool = NSAutoreleasePool::new(nil);
let frame = options.bounds.to_ns_rect();
let mut style_mask = NSWindowStyleMask::NSClosableWindowMask
| NSWindowStyleMask::NSMiniaturizableWindowMask
| NSWindowStyleMask::NSResizableWindowMask
| NSWindowStyleMask::NSTitledWindowMask;
if options.titlebar_appears_transparent {
style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
}
let native_window: id = msg_send![WINDOW_CLASS, alloc];
let native_window = native_window.initWithContentRect_styleMask_backing_defer_(
frame,
style_mask,
NSBackingStoreBuffered,
NO,
);
assert!(!native_window.is_null());
let device =
metal::Device::system_default().expect("could not find default metal device");
let layer: id = msg_send![class!(CAMetalLayer), layer];
let _: () = msg_send![layer, setDevice: device.as_ptr()];
let _: () = msg_send![layer, setPixelFormat: PIXEL_FORMAT];
let _: () = msg_send![layer, setAllowsNextDrawableTimeout: NO];
let _: () = msg_send![layer, setNeedsDisplayOnBoundsChange: YES];
let _: () = msg_send![layer, setPresentsWithTransaction: YES];
let _: () = msg_send![
layer,
setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
| AutoresizingMask::HEIGHT_SIZABLE
];
let native_view: id = msg_send![VIEW_CLASS, alloc];
let native_view = NSView::init(native_view);
assert!(!native_view.is_null());
let window = Self(Rc::new(RefCell::new(WindowState {
id,
native_window,
event_callback: None,
resize_callback: None,
close_callback: None,
synthetic_drag_counter: 0,
executor,
scene_to_render: Default::default(),
renderer: Renderer::new(device.clone(), PIXEL_FORMAT, fonts),
command_queue: device.new_command_queue(),
last_fresh_keydown: None,
layer,
traffic_light_position: options.traffic_light_position,
})));
(*native_window).set_ivar(
WINDOW_STATE_IVAR,
Rc::into_raw(window.0.clone()) as *const c_void,
);
native_window.setDelegate_(native_window);
(*native_view).set_ivar(
WINDOW_STATE_IVAR,
Rc::into_raw(window.0.clone()) as *const c_void,
);
if let Some(title) = options.title.as_ref() {
native_window.setTitle_(NSString::alloc(nil).init_str(title));
}
if options.titlebar_appears_transparent {
native_window.setTitlebarAppearsTransparent_(YES);
}
native_window.setAcceptsMouseMovedEvents_(YES);
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
native_view.setWantsBestResolutionOpenGLSurface_(YES);
// From winit crate: On Mojave, views automatically become layer-backed shortly after
// being added to a native_window. Changing the layer-backedness of a view breaks the
// association between the view and its associated OpenGL context. To work around this,
// on we explicitly make the view layer-backed up front so that AppKit doesn't do it
// itself and break the association with its context.
native_view.setWantsLayer(YES);
let _: () = msg_send![
native_view,
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
];
native_window.setContentView_(native_view.autorelease());
native_window.makeFirstResponder_(native_view);
native_window.center();
native_window.makeKeyAndOrderFront_(nil);
window.0.borrow().move_traffic_light();
pool.drain();
window
}
}
pub fn key_window_id() -> Option<usize> {
unsafe {
let app = NSApplication::sharedApplication(nil);
let key_window: id = msg_send![app, keyWindow];
if msg_send![key_window, isKindOfClass: WINDOW_CLASS] {
let id = get_window_state(&*key_window).borrow().id;
Some(id)
} else {
None
}
}
}
}
impl Drop for Window {
fn drop(&mut self) {
unsafe {
self.0.as_ref().borrow().native_window.close();
}
}
}
impl platform::Window for Window {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn on_event(&mut self, callback: Box<dyn FnMut(Event)>) {
self.0.as_ref().borrow_mut().event_callback = Some(callback);
}
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.0.as_ref().borrow_mut().resize_callback = Some(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.0.as_ref().borrow_mut().close_callback = Some(callback);
}
fn prompt(
&self,
level: platform::PromptLevel,
msg: &str,
answers: &[&str],
done_fn: Box<dyn FnOnce(usize)>,
) {
unsafe {
let alert: id = msg_send![class!(NSAlert), alloc];
let alert: id = msg_send![alert, init];
let alert_style = match level {
platform::PromptLevel::Info => 1,
platform::PromptLevel::Warning => 0,
platform::PromptLevel::Critical => 2,
};
let _: () = msg_send![alert, setAlertStyle: alert_style];
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
for (ix, answer) in answers.into_iter().enumerate() {
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
let _: () = msg_send![button, setTag: ix as NSInteger];
}
let done_fn = Cell::new(Some(done_fn));
let block = ConcreteBlock::new(move |answer: NSInteger| {
if let Some(done_fn) = done_fn.take() {
(done_fn)(answer.try_into().unwrap());
}
});
let block = block.copy();
let _: () = msg_send![
alert,
beginSheetModalForWindow: self.0.borrow().native_window
completionHandler: block
];
}
}
}
impl platform::WindowContext for Window {
fn size(&self) -> Vector2F {
self.0.as_ref().borrow().size()
}
fn scale_factor(&self) -> f32 {
self.0.as_ref().borrow().scale_factor()
}
fn present_scene(&mut self, scene: Scene) {
self.0.as_ref().borrow_mut().present_scene(scene);
}
fn titlebar_height(&self) -> f32 {
self.0.as_ref().borrow().titlebar_height()
}
}
impl WindowState {
fn move_traffic_light(&self) {
if let Some(traffic_light_position) = self.traffic_light_position {
let titlebar_height = self.titlebar_height();
unsafe {
let close_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowCloseButton
];
let min_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton
];
let zoom_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowZoomButton
];
let mut close_button_frame: CGRect = msg_send![close_button, frame];
let mut min_button_frame: CGRect = msg_send![min_button, frame];
let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
let mut origin = vec2f(
traffic_light_position.x(),
titlebar_height
- traffic_light_position.y()
- close_button_frame.size.height as f32,
);
let button_spacing =
(min_button_frame.origin.x - close_button_frame.origin.x) as f32;
close_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
let _: () = msg_send![close_button, setFrame: close_button_frame];
origin.set_x(origin.x() + button_spacing);
min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
let _: () = msg_send![min_button, setFrame: min_button_frame];
origin.set_x(origin.x() + button_spacing);
zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
let _: () = msg_send![zoom_button, setFrame: zoom_button_frame];
}
}
}
}
impl platform::WindowContext for WindowState {
fn size(&self) -> Vector2F {
let NSSize { width, height, .. } =
unsafe { NSView::frame(self.native_window.contentView()) }.size;
vec2f(width as f32, height as f32)
}
fn scale_factor(&self) -> f32 {
unsafe {
let screen: id = msg_send![self.native_window, screen];
NSScreen::backingScaleFactor(screen) as f32
}
}
fn titlebar_height(&self) -> f32 {
unsafe {
let frame = NSWindow::frame(self.native_window);
let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
(frame.size.height - content_layout_rect.size.height) as f32
}
}
fn present_scene(&mut self, scene: Scene) {
self.scene_to_render = Some(scene);
unsafe {
let _: () = msg_send![self.native_window.contentView(), setNeedsDisplay: YES];
}
}
}
unsafe fn get_window_state(object: &Object) -> Rc<RefCell<WindowState>> {
let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
let rc1 = Rc::from_raw(raw as *mut RefCell<WindowState>);
let rc2 = rc1.clone();
mem::forget(rc1);
rc2
}
unsafe fn drop_window_state(object: &Object) {
let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
Rc::from_raw(raw as *mut RefCell<WindowState>);
}
extern "C" fn yes(_: &Object, _: Sel) -> BOOL {
YES
}
extern "C" fn dealloc_window(this: &Object, _: Sel) {
unsafe {
drop_window_state(this);
let () = msg_send![super(this, class!(NSWindow)), dealloc];
}
}
extern "C" fn dealloc_view(this: &Object, _: Sel) {
unsafe {
drop_window_state(this);
let () = msg_send![super(this, class!(NSView)), dealloc];
}
}
extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
let window_state = unsafe { get_window_state(this) };
let weak_window_state = Rc::downgrade(&window_state);
let mut window_state_borrow = window_state.as_ref().borrow_mut();
let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
if let Some(event) = event {
match &event {
Event::LeftMouseDragged { position } => {
window_state_borrow.synthetic_drag_counter += 1;
window_state_borrow
.executor
.spawn(synthetic_drag(
weak_window_state,
window_state_borrow.synthetic_drag_counter,
*position,
))
.detach();
}
Event::LeftMouseUp { .. } => {
window_state_borrow.synthetic_drag_counter += 1;
}
// Ignore events from held-down keys after some of the initially-pressed keys
// were released.
Event::KeyDown {
chars,
keystroke,
is_held,
} => {
let keydown = (keystroke.clone(), chars.clone());
if *is_held {
if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
return;
}
} else {
window_state_borrow.last_fresh_keydown = Some(keydown);
}
}
_ => {}
}
if let Some(mut callback) = window_state_borrow.event_callback.take() {
drop(window_state_borrow);
callback(event);
window_state.borrow_mut().event_callback = Some(callback);
}
}
}
extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
unsafe {
let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
}
}
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
window_state.as_ref().borrow().move_traffic_light();
}
extern "C" fn close_window(this: &Object, _: Sel) {
unsafe {
let close_callback = {
let window_state = get_window_state(this);
window_state
.as_ref()
.try_borrow_mut()
.ok()
.and_then(|mut window_state| window_state.close_callback.take())
};
if let Some(callback) = close_callback {
callback();
}
let () = msg_send![super(this, class!(NSWindow)), close];
}
}
extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
let window_state = unsafe { get_window_state(this) };
let window_state = window_state.as_ref().borrow();
window_state.layer
}
extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
let window_state = unsafe { get_window_state(this) };
let mut window_state_borrow = window_state.as_ref().borrow_mut();
unsafe {
let _: () = msg_send![window_state_borrow.layer, setContentsScale: window_state_borrow.scale_factor() as f64];
}
if let Some(mut callback) = window_state_borrow.resize_callback.take() {
drop(window_state_borrow);
callback();
window_state.as_ref().borrow_mut().resize_callback = Some(callback);
};
}
extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
let window_state = unsafe { get_window_state(this) };
let mut window_state_borrow = window_state.as_ref().borrow_mut();
if window_state_borrow.size() == vec2f(size.width as f32, size.height as f32) {
return;
}
unsafe {
let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size];
}
let scale_factor = window_state_borrow.scale_factor() as f64;
let drawable_size: NSSize = NSSize {
width: size.width * scale_factor,
height: size.height * scale_factor,
};
unsafe {
let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size];
}
if let Some(mut callback) = window_state_borrow.resize_callback.take() {
drop(window_state_borrow);
callback();
window_state.borrow_mut().resize_callback = Some(callback);
};
}
extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
unsafe {
let window_state = get_window_state(this);
let mut window_state = window_state.as_ref().borrow_mut();
if let Some(scene) = window_state.scene_to_render.take() {
let drawable: &metal::MetalDrawableRef = msg_send![window_state.layer, nextDrawable];
let command_queue = window_state.command_queue.clone();
let command_buffer = command_queue.new_command_buffer();
let size = window_state.size();
let scale_factor = window_state.scale_factor();
window_state.renderer.render(
&scene,
size * scale_factor,
command_buffer,
drawable.texture(),
);
command_buffer.commit();
command_buffer.wait_until_completed();
drawable.present();
};
}
}
async fn synthetic_drag(
window_state: Weak<RefCell<WindowState>>,
drag_id: usize,
position: Vector2F,
) {
loop {
Timer::after(Duration::from_millis(16)).await;
if let Some(window_state) = window_state.upgrade() {
let mut window_state_borrow = window_state.borrow_mut();
if window_state_borrow.synthetic_drag_counter == drag_id {
if let Some(mut callback) = window_state_borrow.event_callback.take() {
drop(window_state_borrow);
callback(Event::LeftMouseDragged { position });
window_state.borrow_mut().event_callback = Some(callback);
}
} else {
break;
}
}
}
}
unsafe fn ns_string(string: &str) -> id {
NSString::alloc(nil).init_str(string).autorelease()
}

View file

@ -0,0 +1,223 @@
use super::CursorStyle;
use crate::{AnyAction, ClipboardItem};
use anyhow::Result;
use parking_lot::Mutex;
use pathfinder_geometry::vector::Vector2F;
use std::{
any::Any,
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use time::UtcOffset;
pub struct Platform {
dispatcher: Arc<dyn super::Dispatcher>,
fonts: Arc<dyn super::FontSystem>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
cursor: Mutex<CursorStyle>,
}
#[derive(Default)]
pub struct ForegroundPlatform {
last_prompt_for_new_path_args: RefCell<Option<(PathBuf, Box<dyn FnOnce(Option<PathBuf>)>)>>,
}
struct Dispatcher;
pub struct Window {
size: Vector2F,
scale_factor: f32,
current_scene: Option<crate::Scene>,
event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
resize_handlers: Vec<Box<dyn FnMut()>>,
close_handlers: Vec<Box<dyn FnOnce()>>,
pub(crate) last_prompt: RefCell<Option<Box<dyn FnOnce(usize)>>>,
}
impl ForegroundPlatform {
pub(crate) fn simulate_new_path_selection(
&self,
result: impl FnOnce(PathBuf) -> Option<PathBuf>,
) {
let (dir_path, callback) = self
.last_prompt_for_new_path_args
.take()
.expect("prompt_for_new_path was not called");
callback(result(dir_path));
}
pub(crate) fn did_prompt_for_new_path(&self) -> bool {
self.last_prompt_for_new_path_args.borrow().is_some()
}
}
impl super::ForegroundPlatform for ForegroundPlatform {
fn on_become_active(&self, _: Box<dyn FnMut()>) {}
fn on_resign_active(&self, _: Box<dyn FnMut()>) {}
fn on_event(&self, _: Box<dyn FnMut(crate::Event) -> bool>) {}
fn on_open_files(&self, _: Box<dyn FnMut(Vec<std::path::PathBuf>)>) {}
fn run(&self, _on_finish_launching: Box<dyn FnOnce() -> ()>) {
unimplemented!()
}
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn AnyAction)>) {}
fn set_menus(&self, _: Vec<crate::Menu>) {}
fn prompt_for_paths(
&self,
_: super::PathPromptOptions,
_: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
) {
}
fn prompt_for_new_path(&self, path: &Path, f: Box<dyn FnOnce(Option<std::path::PathBuf>)>) {
*self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), f));
}
}
impl Platform {
fn new() -> Self {
Self {
dispatcher: Arc::new(Dispatcher),
fonts: Arc::new(super::current::FontSystem::new()),
current_clipboard_item: Default::default(),
cursor: Mutex::new(CursorStyle::Arrow),
}
}
}
impl super::Platform for Platform {
fn dispatcher(&self) -> Arc<dyn super::Dispatcher> {
self.dispatcher.clone()
}
fn fonts(&self) -> std::sync::Arc<dyn super::FontSystem> {
self.fonts.clone()
}
fn activate(&self, _ignoring_other_apps: bool) {}
fn open_window(
&self,
_: usize,
options: super::WindowOptions,
_executor: Rc<super::executor::Foreground>,
) -> Box<dyn super::Window> {
Box::new(Window::new(options.bounds.size()))
}
fn key_window_id(&self) -> Option<usize> {
None
}
fn quit(&self) {}
fn write_to_clipboard(&self, item: ClipboardItem) {
*self.current_clipboard_item.lock() = Some(item);
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.current_clipboard_item.lock().clone()
}
fn open_url(&self, _: &str) {}
fn write_credentials(&self, _: &str, _: &str, _: &[u8]) -> Result<()> {
Ok(())
}
fn read_credentials(&self, _: &str) -> Result<Option<(String, Vec<u8>)>> {
Ok(None)
}
fn delete_credentials(&self, _: &str) -> Result<()> {
Ok(())
}
fn set_cursor_style(&self, style: CursorStyle) {
*self.cursor.lock() = style;
}
fn local_timezone(&self) -> UtcOffset {
UtcOffset::UTC
}
}
impl Window {
fn new(size: Vector2F) -> Self {
Self {
size,
event_handlers: Vec::new(),
resize_handlers: Vec::new(),
close_handlers: Vec::new(),
scale_factor: 1.0,
current_scene: None,
last_prompt: RefCell::new(None),
}
}
}
impl super::Dispatcher for Dispatcher {
fn is_main_thread(&self) -> bool {
true
}
fn run_on_main_thread(&self, task: async_task::Runnable) {
task.run();
}
}
impl super::WindowContext for Window {
fn size(&self) -> Vector2F {
self.size
}
fn scale_factor(&self) -> f32 {
self.scale_factor
}
fn titlebar_height(&self) -> f32 {
24.
}
fn present_scene(&mut self, scene: crate::Scene) {
self.current_scene = Some(scene);
}
}
impl super::Window for Window {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event)>) {
self.event_handlers.push(callback);
}
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.resize_handlers.push(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.close_handlers.push(callback);
}
fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], f: Box<dyn FnOnce(usize)>) {
self.last_prompt.replace(Some(f));
}
}
pub fn platform() -> Platform {
Platform::new()
}
pub fn foreground_platform() -> ForegroundPlatform {
ForegroundPlatform::default()
}

View file

@ -0,0 +1,562 @@
use crate::{
app::{AppContext, MutableAppContext, WindowInvalidation},
elements::Element,
font_cache::FontCache,
geometry::rect::RectF,
json::{self, ToJson},
platform::Event,
text_layout::TextLayoutCache,
Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel,
ReadView, Scene, View, ViewHandle,
};
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
use std::{
collections::{HashMap, HashSet},
ops::{Deref, DerefMut},
sync::Arc,
};
pub struct Presenter {
window_id: usize,
rendered_views: HashMap<usize, ElementBox>,
parents: HashMap<usize, usize>,
font_cache: Arc<FontCache>,
text_layout_cache: TextLayoutCache,
asset_cache: Arc<AssetCache>,
last_mouse_moved_event: Option<Event>,
titlebar_height: f32,
}
impl Presenter {
pub fn new(
window_id: usize,
titlebar_height: f32,
font_cache: Arc<FontCache>,
text_layout_cache: TextLayoutCache,
asset_cache: Arc<AssetCache>,
cx: &mut MutableAppContext,
) -> Self {
Self {
window_id,
rendered_views: cx.render_views(window_id, titlebar_height),
parents: HashMap::new(),
font_cache,
text_layout_cache,
asset_cache,
last_mouse_moved_event: None,
titlebar_height,
}
}
pub fn dispatch_path(&self, app: &AppContext) -> Vec<usize> {
let mut view_id = app.focused_view_id(self.window_id).unwrap();
let mut path = vec![view_id];
while let Some(parent_id) = self.parents.get(&view_id).copied() {
path.push(parent_id);
view_id = parent_id;
}
path.reverse();
path
}
pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &mut MutableAppContext) {
for view_id in invalidation.removed {
invalidation.updated.remove(&view_id);
self.rendered_views.remove(&view_id);
self.parents.remove(&view_id);
}
for view_id in invalidation.updated {
self.rendered_views.insert(
view_id,
cx.render_view(self.window_id, view_id, self.titlebar_height, false)
.unwrap(),
);
}
}
pub fn refresh(
&mut self,
invalidation: Option<WindowInvalidation>,
cx: &mut MutableAppContext,
) {
if let Some(invalidation) = invalidation {
for view_id in invalidation.removed {
self.rendered_views.remove(&view_id);
self.parents.remove(&view_id);
}
}
for (view_id, view) in &mut self.rendered_views {
*view = cx
.render_view(self.window_id, *view_id, self.titlebar_height, true)
.unwrap();
}
}
pub fn build_scene(
&mut self,
window_size: Vector2F,
scale_factor: f32,
refreshing: bool,
cx: &mut MutableAppContext,
) -> Scene {
let mut scene = Scene::new(scale_factor);
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
self.layout(window_size, refreshing, cx);
let mut paint_cx = PaintContext {
scene: &mut scene,
font_cache: &self.font_cache,
text_layout_cache: &self.text_layout_cache,
rendered_views: &mut self.rendered_views,
app: cx.as_ref(),
};
paint_cx.paint(
root_view_id,
Vector2F::zero(),
RectF::new(Vector2F::zero(), window_size),
);
self.text_layout_cache.finish_frame();
if let Some(event) = self.last_mouse_moved_event.clone() {
self.dispatch_event(event, cx)
}
} else {
log::error!("could not find root_view_id for window {}", self.window_id);
}
scene
}
fn layout(&mut self, size: Vector2F, refreshing: bool, cx: &mut MutableAppContext) {
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
self.build_layout_context(refreshing, cx)
.layout(root_view_id, SizeConstraint::strict(size));
}
}
pub fn build_layout_context<'a>(
&'a mut self,
refreshing: bool,
cx: &'a mut MutableAppContext,
) -> LayoutContext<'a> {
LayoutContext {
rendered_views: &mut self.rendered_views,
parents: &mut self.parents,
refreshing,
font_cache: &self.font_cache,
font_system: cx.platform().fonts(),
text_layout_cache: &self.text_layout_cache,
asset_cache: &self.asset_cache,
view_stack: Vec::new(),
app: cx,
}
}
pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) {
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
match event {
Event::MouseMoved { .. } => {
self.last_mouse_moved_event = Some(event.clone());
}
Event::LeftMouseDragged { position } => {
self.last_mouse_moved_event = Some(Event::MouseMoved {
position,
left_mouse_down: true,
});
}
_ => {}
}
let mut event_cx = self.build_event_context(cx);
event_cx.dispatch_event(root_view_id, &event);
let invalidated_views = event_cx.invalidated_views;
let dispatch_directives = event_cx.dispatched_actions;
for view_id in invalidated_views {
cx.notify_view(self.window_id, view_id);
}
for directive in dispatch_directives {
cx.dispatch_action_any(self.window_id, &directive.path, directive.action.as_ref());
}
}
}
pub fn build_event_context<'a>(
&'a mut self,
cx: &'a mut MutableAppContext,
) -> EventContext<'a> {
EventContext {
rendered_views: &mut self.rendered_views,
dispatched_actions: Default::default(),
font_cache: &self.font_cache,
text_layout_cache: &self.text_layout_cache,
view_stack: Default::default(),
invalidated_views: Default::default(),
notify_count: 0,
app: cx,
}
}
pub fn debug_elements(&self, cx: &AppContext) -> Option<json::Value> {
cx.root_view_id(self.window_id)
.and_then(|root_view_id| self.rendered_views.get(&root_view_id))
.map(|root_element| {
root_element.debug(&DebugContext {
rendered_views: &self.rendered_views,
font_cache: &self.font_cache,
app: cx,
})
})
}
}
pub struct DispatchDirective {
pub path: Vec<usize>,
pub action: Box<dyn AnyAction>,
}
pub struct LayoutContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>,
parents: &'a mut HashMap<usize, usize>,
view_stack: Vec<usize>,
pub refreshing: bool,
pub font_cache: &'a Arc<FontCache>,
pub font_system: Arc<dyn FontSystem>,
pub text_layout_cache: &'a TextLayoutCache,
pub asset_cache: &'a AssetCache,
pub app: &'a mut MutableAppContext,
}
impl<'a> LayoutContext<'a> {
fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F {
if let Some(parent_id) = self.view_stack.last() {
self.parents.insert(view_id, *parent_id);
}
self.view_stack.push(view_id);
let mut rendered_view = self.rendered_views.remove(&view_id).unwrap();
let size = rendered_view.layout(constraint, self);
self.rendered_views.insert(view_id, rendered_view);
self.view_stack.pop();
size
}
}
impl<'a> Deref for LayoutContext<'a> {
type Target = MutableAppContext;
fn deref(&self) -> &Self::Target {
self.app
}
}
impl<'a> DerefMut for LayoutContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.app
}
}
impl<'a> ReadView for LayoutContext<'a> {
fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
self.app.read_view(handle)
}
}
impl<'a> ReadModel for LayoutContext<'a> {
fn read_model<T: Entity>(&self, handle: &ModelHandle<T>) -> &T {
self.app.read_model(handle)
}
}
pub struct PaintContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>,
pub scene: &'a mut Scene,
pub font_cache: &'a FontCache,
pub text_layout_cache: &'a TextLayoutCache,
pub app: &'a AppContext,
}
impl<'a> PaintContext<'a> {
fn paint(&mut self, view_id: usize, origin: Vector2F, visible_bounds: RectF) {
if let Some(mut tree) = self.rendered_views.remove(&view_id) {
tree.paint(origin, visible_bounds, self);
self.rendered_views.insert(view_id, tree);
}
}
}
impl<'a> Deref for PaintContext<'a> {
type Target = AppContext;
fn deref(&self) -> &Self::Target {
self.app
}
}
pub struct EventContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>,
dispatched_actions: Vec<DispatchDirective>,
pub font_cache: &'a FontCache,
pub text_layout_cache: &'a TextLayoutCache,
pub app: &'a mut MutableAppContext,
pub notify_count: usize,
view_stack: Vec<usize>,
invalidated_views: HashSet<usize>,
}
impl<'a> EventContext<'a> {
fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool {
if let Some(mut element) = self.rendered_views.remove(&view_id) {
self.view_stack.push(view_id);
let result = element.dispatch_event(event, self);
self.view_stack.pop();
self.rendered_views.insert(view_id, element);
result
} else {
false
}
}
pub fn dispatch_action<A: Action>(&mut self, action: A) {
self.dispatched_actions.push(DispatchDirective {
path: self.view_stack.clone(),
action: Box::new(action),
});
}
pub fn notify(&mut self) {
self.notify_count += 1;
if let Some(view_id) = self.view_stack.last() {
self.invalidated_views.insert(*view_id);
}
}
pub fn notify_count(&self) -> usize {
self.notify_count
}
}
impl<'a> Deref for EventContext<'a> {
type Target = MutableAppContext;
fn deref(&self) -> &Self::Target {
self.app
}
}
impl<'a> DerefMut for EventContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.app
}
}
pub struct DebugContext<'a> {
rendered_views: &'a HashMap<usize, ElementBox>,
pub font_cache: &'a FontCache,
pub app: &'a AppContext,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Axis {
Horizontal,
Vertical,
}
impl Axis {
pub fn invert(self) -> Self {
match self {
Self::Horizontal => Self::Vertical,
Self::Vertical => Self::Horizontal,
}
}
}
impl ToJson for Axis {
fn to_json(&self) -> serde_json::Value {
match self {
Axis::Horizontal => json!("horizontal"),
Axis::Vertical => json!("vertical"),
}
}
}
pub trait Vector2FExt {
fn along(self, axis: Axis) -> f32;
}
impl Vector2FExt for Vector2F {
fn along(self, axis: Axis) -> f32 {
match axis {
Axis::Horizontal => self.x(),
Axis::Vertical => self.y(),
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct SizeConstraint {
pub min: Vector2F,
pub max: Vector2F,
}
impl SizeConstraint {
pub fn new(min: Vector2F, max: Vector2F) -> Self {
Self { min, max }
}
pub fn strict(size: Vector2F) -> Self {
Self {
min: size,
max: size,
}
}
pub fn strict_along(axis: Axis, max: f32) -> Self {
match axis {
Axis::Horizontal => Self {
min: vec2f(max, 0.0),
max: vec2f(max, f32::INFINITY),
},
Axis::Vertical => Self {
min: vec2f(0.0, max),
max: vec2f(f32::INFINITY, max),
},
}
}
pub fn max_along(&self, axis: Axis) -> f32 {
match axis {
Axis::Horizontal => self.max.x(),
Axis::Vertical => self.max.y(),
}
}
pub fn min_along(&self, axis: Axis) -> f32 {
match axis {
Axis::Horizontal => self.min.x(),
Axis::Vertical => self.min.y(),
}
}
pub fn constrain(&self, size: Vector2F) -> Vector2F {
vec2f(
size.x().min(self.max.x()).max(self.min.x()),
size.y().min(self.max.y()).max(self.min.y()),
)
}
}
impl ToJson for SizeConstraint {
fn to_json(&self) -> serde_json::Value {
json!({
"min": self.min.to_json(),
"max": self.max.to_json(),
})
}
}
pub struct ChildView {
view_id: usize,
}
impl ChildView {
pub fn new(view_id: usize) -> Self {
Self { view_id }
}
}
impl Element for ChildView {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = cx.layout(self.view_id, constraint);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.paint(self.view_id, bounds.origin(), visible_bounds);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
cx.dispatch_event(self.view_id, event)
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value {
json!({
"type": "ChildView",
"view_id": self.view_id,
"bounds": bounds.to_json(),
"child": if let Some(view) = cx.rendered_views.get(&self.view_id) {
view.debug(cx)
} else {
json!(null)
}
})
}
}
#[cfg(test)]
mod tests {
// #[test]
// fn test_responder_chain() {
// let settings = settings_rx(None);
// let mut app = App::new().unwrap();
// let workspace = app.add_model(|cx| Workspace::new(Vec::new(), cx));
// let (window_id, workspace_view) =
// app.add_window(|cx| WorkspaceView::new(workspace.clone(), settings, cx));
// let invalidations = Rc::new(RefCell::new(Vec::new()));
// let invalidations_ = invalidations.clone();
// app.on_window_invalidated(window_id, move |invalidation, _| {
// invalidations_.borrow_mut().push(invalidation)
// });
// let active_pane_id = workspace_view.update(&mut app, |view, cx| {
// cx.focus(view.active_pane());
// view.active_pane().id()
// });
// app.update(|app| {
// let mut presenter = Presenter::new(
// window_id,
// Rc::new(FontCache::new()),
// Rc::new(AssetCache::new()),
// app,
// );
// for invalidation in invalidations.borrow().iter().cloned() {
// presenter.update(vec2f(1024.0, 768.0), 2.0, Some(invalidation), app);
// }
// assert_eq!(
// presenter.responder_chain(app.cx()).unwrap(),
// vec![workspace_view.id(), active_pane_id]
// );
// });
// }
}

422
crates/gpui/src/scene.rs Normal file
View file

@ -0,0 +1,422 @@
use serde::Deserialize;
use serde_json::json;
use std::{borrow::Cow, sync::Arc};
use crate::{
color::Color,
fonts::{FontId, GlyphId},
geometry::{rect::RectF, vector::Vector2F},
json::ToJson,
ImageData,
};
pub struct Scene {
scale_factor: f32,
stacking_contexts: Vec<StackingContext>,
active_stacking_context_stack: Vec<usize>,
}
struct StackingContext {
layers: Vec<Layer>,
active_layer_stack: Vec<usize>,
}
#[derive(Default)]
pub struct Layer {
clip_bounds: Option<RectF>,
quads: Vec<Quad>,
underlines: Vec<Quad>,
images: Vec<Image>,
shadows: Vec<Shadow>,
glyphs: Vec<Glyph>,
icons: Vec<Icon>,
paths: Vec<Path>,
}
#[derive(Default, Debug)]
pub struct Quad {
pub bounds: RectF,
pub background: Option<Color>,
pub border: Border,
pub corner_radius: f32,
}
#[derive(Debug)]
pub struct Shadow {
pub bounds: RectF,
pub corner_radius: f32,
pub sigma: f32,
pub color: Color,
}
#[derive(Debug)]
pub struct Glyph {
pub font_id: FontId,
pub font_size: f32,
pub id: GlyphId,
pub origin: Vector2F,
pub color: Color,
}
pub struct Icon {
pub bounds: RectF,
pub svg: usvg::Tree,
pub path: Cow<'static, str>,
pub color: Color,
}
#[derive(Clone, Copy, Default, Debug)]
pub struct Border {
pub width: f32,
pub color: Color,
pub overlay: bool,
pub top: bool,
pub right: bool,
pub bottom: bool,
pub left: bool,
}
impl<'de> Deserialize<'de> for Border {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct BorderData {
pub width: f32,
pub color: Color,
#[serde(default)]
pub overlay: bool,
#[serde(default)]
pub top: bool,
#[serde(default)]
pub right: bool,
#[serde(default)]
pub bottom: bool,
#[serde(default)]
pub left: bool,
}
let data = BorderData::deserialize(deserializer)?;
let mut border = Border {
width: data.width,
color: data.color,
overlay: data.overlay,
top: data.top,
bottom: data.bottom,
left: data.left,
right: data.right,
};
if !border.top && !border.bottom && !border.left && !border.right {
border.top = true;
border.bottom = true;
border.left = true;
border.right = true;
}
Ok(border)
}
}
#[derive(Debug)]
pub struct Path {
pub bounds: RectF,
pub color: Color,
pub vertices: Vec<PathVertex>,
}
#[derive(Debug)]
pub struct PathVertex {
pub xy_position: Vector2F,
pub st_position: Vector2F,
}
pub struct Image {
pub bounds: RectF,
pub border: Border,
pub corner_radius: f32,
pub data: Arc<ImageData>,
}
impl Scene {
pub fn new(scale_factor: f32) -> Self {
let stacking_context = StackingContext::new(None);
Scene {
scale_factor,
stacking_contexts: vec![stacking_context],
active_stacking_context_stack: vec![0],
}
}
pub fn scale_factor(&self) -> f32 {
self.scale_factor
}
pub fn layers(&self) -> impl Iterator<Item = &Layer> {
self.stacking_contexts.iter().flat_map(|s| &s.layers)
}
pub fn push_stacking_context(&mut self, clip_bounds: Option<RectF>) {
self.active_stacking_context_stack
.push(self.stacking_contexts.len());
self.stacking_contexts
.push(StackingContext::new(clip_bounds))
}
pub fn pop_stacking_context(&mut self) {
self.active_stacking_context_stack.pop();
assert!(!self.active_stacking_context_stack.is_empty());
}
pub fn push_layer(&mut self, clip_bounds: Option<RectF>) {
self.active_stacking_context().push_layer(clip_bounds);
}
pub fn pop_layer(&mut self) {
self.active_stacking_context().pop_layer();
}
pub fn push_quad(&mut self, quad: Quad) {
self.active_layer().push_quad(quad)
}
pub fn push_image(&mut self, image: Image) {
self.active_layer().push_image(image)
}
pub fn push_underline(&mut self, underline: Quad) {
self.active_layer().push_underline(underline)
}
pub fn push_shadow(&mut self, shadow: Shadow) {
self.active_layer().push_shadow(shadow)
}
pub fn push_glyph(&mut self, glyph: Glyph) {
self.active_layer().push_glyph(glyph)
}
pub fn push_icon(&mut self, icon: Icon) {
self.active_layer().push_icon(icon)
}
pub fn push_path(&mut self, path: Path) {
self.active_layer().push_path(path);
}
fn active_stacking_context(&mut self) -> &mut StackingContext {
let ix = *self.active_stacking_context_stack.last().unwrap();
&mut self.stacking_contexts[ix]
}
fn active_layer(&mut self) -> &mut Layer {
self.active_stacking_context().active_layer()
}
}
impl StackingContext {
fn new(clip_bounds: Option<RectF>) -> Self {
Self {
layers: vec![Layer::new(clip_bounds)],
active_layer_stack: vec![0],
}
}
fn active_layer(&mut self) -> &mut Layer {
&mut self.layers[*self.active_layer_stack.last().unwrap()]
}
fn push_layer(&mut self, clip_bounds: Option<RectF>) {
let parent_clip_bounds = self.active_layer().clip_bounds();
let clip_bounds = clip_bounds
.map(|clip_bounds| {
clip_bounds
.intersection(parent_clip_bounds.unwrap_or(clip_bounds))
.unwrap_or_else(|| {
if !clip_bounds.is_empty() {
log::warn!("specified clip bounds are disjoint from parent layer");
}
RectF::default()
})
})
.or(parent_clip_bounds);
let ix = self.layers.len();
self.layers.push(Layer::new(clip_bounds));
self.active_layer_stack.push(ix);
}
fn pop_layer(&mut self) {
self.active_layer_stack.pop().unwrap();
assert!(!self.active_layer_stack.is_empty());
}
}
impl Layer {
pub fn new(clip_bounds: Option<RectF>) -> Self {
Self {
clip_bounds,
quads: Vec::new(),
underlines: Vec::new(),
images: Vec::new(),
shadows: Vec::new(),
glyphs: Vec::new(),
icons: Vec::new(),
paths: Vec::new(),
}
}
pub fn clip_bounds(&self) -> Option<RectF> {
self.clip_bounds
}
fn push_quad(&mut self, quad: Quad) {
self.quads.push(quad);
}
pub fn quads(&self) -> &[Quad] {
self.quads.as_slice()
}
fn push_underline(&mut self, underline: Quad) {
self.underlines.push(underline);
}
pub fn underlines(&self) -> &[Quad] {
self.underlines.as_slice()
}
fn push_image(&mut self, image: Image) {
self.images.push(image);
}
pub fn images(&self) -> &[Image] {
self.images.as_slice()
}
fn push_shadow(&mut self, shadow: Shadow) {
self.shadows.push(shadow);
}
pub fn shadows(&self) -> &[Shadow] {
self.shadows.as_slice()
}
fn push_glyph(&mut self, glyph: Glyph) {
self.glyphs.push(glyph);
}
pub fn glyphs(&self) -> &[Glyph] {
self.glyphs.as_slice()
}
pub fn push_icon(&mut self, icon: Icon) {
self.icons.push(icon);
}
pub fn icons(&self) -> &[Icon] {
self.icons.as_slice()
}
fn push_path(&mut self, path: Path) {
if !path.bounds.is_empty() {
self.paths.push(path);
}
}
pub fn paths(&self) -> &[Path] {
self.paths.as_slice()
}
}
impl Border {
pub fn new(width: f32, color: Color) -> Self {
Self {
width,
color,
overlay: false,
top: false,
left: false,
bottom: false,
right: false,
}
}
pub fn all(width: f32, color: Color) -> Self {
Self {
width,
color,
overlay: false,
top: true,
left: true,
bottom: true,
right: true,
}
}
pub fn top(width: f32, color: Color) -> Self {
let mut border = Self::new(width, color);
border.top = true;
border
}
pub fn left(width: f32, color: Color) -> Self {
let mut border = Self::new(width, color);
border.left = true;
border
}
pub fn bottom(width: f32, color: Color) -> Self {
let mut border = Self::new(width, color);
border.bottom = true;
border
}
pub fn right(width: f32, color: Color) -> Self {
let mut border = Self::new(width, color);
border.right = true;
border
}
pub fn with_sides(mut self, top: bool, left: bool, bottom: bool, right: bool) -> Self {
self.top = top;
self.left = left;
self.bottom = bottom;
self.right = right;
self
}
pub fn top_width(&self) -> f32 {
if self.top {
self.width
} else {
0.0
}
}
pub fn left_width(&self) -> f32 {
if self.left {
self.width
} else {
0.0
}
}
}
impl ToJson for Border {
fn to_json(&self) -> serde_json::Value {
let mut value = json!({});
if self.top {
value["top"] = json!(self.width);
}
if self.right {
value["right"] = json!(self.width);
}
if self.bottom {
value["bottom"] = json!(self.width);
}
if self.left {
value["left"] = json!(self.width);
}
value
}
}

8
crates/gpui/src/test.rs Normal file
View file

@ -0,0 +1,8 @@
use ctor::ctor;
#[ctor]
fn init_logger() {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
}

View file

@ -0,0 +1,728 @@
use crate::{
color::Color,
fonts::{FontId, GlyphId},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
platform, scene, FontSystem, PaintContext,
};
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::SmallVec;
use std::{
borrow::Borrow,
collections::HashMap,
hash::{Hash, Hasher},
iter,
sync::Arc,
};
pub struct TextLayoutCache {
prev_frame: Mutex<HashMap<CacheKeyValue, Arc<LineLayout>>>,
curr_frame: RwLock<HashMap<CacheKeyValue, Arc<LineLayout>>>,
fonts: Arc<dyn platform::FontSystem>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct RunStyle {
pub color: Color,
pub font_id: FontId,
pub underline: bool,
}
impl TextLayoutCache {
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
Self {
prev_frame: Mutex::new(HashMap::new()),
curr_frame: RwLock::new(HashMap::new()),
fonts,
}
}
pub fn finish_frame(&self) {
let mut prev_frame = self.prev_frame.lock();
let mut curr_frame = self.curr_frame.write();
std::mem::swap(&mut *prev_frame, &mut *curr_frame);
curr_frame.clear();
}
pub fn layout_str<'a>(
&'a self,
text: &'a str,
font_size: f32,
runs: &'a [(usize, RunStyle)],
) -> Line {
let key = &CacheKeyRef {
text,
font_size: OrderedFloat(font_size),
runs,
} as &dyn CacheKey;
let curr_frame = self.curr_frame.upgradable_read();
if let Some(layout) = curr_frame.get(key) {
return Line::new(layout.clone(), runs);
}
let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
curr_frame.insert(key, layout.clone());
Line::new(layout.clone(), runs)
} else {
let layout = Arc::new(self.fonts.layout_line(text, font_size, runs));
let key = CacheKeyValue {
text: text.into(),
font_size: OrderedFloat(font_size),
runs: SmallVec::from(runs),
};
curr_frame.insert(key, layout.clone());
Line::new(layout, runs)
}
}
}
trait CacheKey {
fn key<'a>(&'a self) -> CacheKeyRef<'a>;
}
impl<'a> PartialEq for (dyn CacheKey + 'a) {
fn eq(&self, other: &dyn CacheKey) -> bool {
self.key() == other.key()
}
}
impl<'a> Eq for (dyn CacheKey + 'a) {}
impl<'a> Hash for (dyn CacheKey + 'a) {
fn hash<H: Hasher>(&self, state: &mut H) {
self.key().hash(state)
}
}
#[derive(Eq, PartialEq)]
struct CacheKeyValue {
text: String,
font_size: OrderedFloat<f32>,
runs: SmallVec<[(usize, RunStyle); 1]>,
}
impl CacheKey for CacheKeyValue {
fn key<'a>(&'a self) -> CacheKeyRef<'a> {
CacheKeyRef {
text: &self.text.as_str(),
font_size: self.font_size,
runs: self.runs.as_slice(),
}
}
}
impl Hash for CacheKeyValue {
fn hash<H: Hasher>(&self, state: &mut H) {
self.key().hash(state);
}
}
impl<'a> Borrow<dyn CacheKey + 'a> for CacheKeyValue {
fn borrow(&self) -> &(dyn CacheKey + 'a) {
self as &dyn CacheKey
}
}
#[derive(Copy, Clone)]
struct CacheKeyRef<'a> {
text: &'a str,
font_size: OrderedFloat<f32>,
runs: &'a [(usize, RunStyle)],
}
impl<'a> CacheKey for CacheKeyRef<'a> {
fn key<'b>(&'b self) -> CacheKeyRef<'b> {
*self
}
}
impl<'a> PartialEq for CacheKeyRef<'a> {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
&& self.font_size == other.font_size
&& self.runs.len() == other.runs.len()
&& self.runs.iter().zip(other.runs.iter()).all(
|((len_a, style_a), (len_b, style_b))| {
len_a == len_b && style_a.font_id == style_b.font_id
},
)
}
}
impl<'a> Hash for CacheKeyRef<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.text.hash(state);
self.font_size.hash(state);
for (len, style_id) in self.runs {
len.hash(state);
style_id.font_id.hash(state);
}
}
}
#[derive(Default, Debug)]
pub struct Line {
layout: Arc<LineLayout>,
style_runs: SmallVec<[(u32, Color, bool); 32]>,
}
#[derive(Default, Debug)]
pub struct LineLayout {
pub width: f32,
pub ascent: f32,
pub descent: f32,
pub runs: Vec<Run>,
pub len: usize,
pub font_size: f32,
}
#[derive(Debug)]
pub struct Run {
pub font_id: FontId,
pub glyphs: Vec<Glyph>,
}
#[derive(Debug)]
pub struct Glyph {
pub id: GlyphId,
pub position: Vector2F,
pub index: usize,
}
impl Line {
fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
let mut style_runs = SmallVec::new();
for (len, style) in runs {
style_runs.push((*len as u32, style.color, style.underline));
}
Self { layout, style_runs }
}
pub fn runs(&self) -> &[Run] {
&self.layout.runs
}
pub fn width(&self) -> f32 {
self.layout.width
}
pub fn x_for_index(&self, index: usize) -> f32 {
for run in &self.layout.runs {
for glyph in &run.glyphs {
if glyph.index == index {
return glyph.position.x();
}
}
}
self.layout.width
}
pub fn index_for_x(&self, x: f32) -> Option<usize> {
if x >= self.layout.width {
None
} else {
for run in self.layout.runs.iter().rev() {
for glyph in run.glyphs.iter().rev() {
if glyph.position.x() <= x {
return Some(glyph.index);
}
}
}
Some(0)
}
}
pub fn paint(
&self,
origin: Vector2F,
visible_bounds: RectF,
line_height: f32,
cx: &mut PaintContext,
) {
let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
let baseline_offset = vec2f(0., padding_top + self.layout.ascent);
let mut style_runs = self.style_runs.iter();
let mut run_end = 0;
let mut color = Color::black();
let mut underline_start = None;
for run in &self.layout.runs {
let max_glyph_width = cx
.font_cache
.bounding_box(run.font_id, self.layout.font_size)
.x();
for glyph in &run.glyphs {
let glyph_origin = origin + baseline_offset + glyph.position;
if glyph_origin.x() + max_glyph_width < visible_bounds.origin().x() {
continue;
}
if glyph_origin.x() > visible_bounds.upper_right().x() {
break;
}
if glyph.index >= run_end {
if let Some((run_len, run_color, run_underlined)) = style_runs.next() {
if let Some(underline_origin) = underline_start {
if !*run_underlined || *run_color != color {
cx.scene.push_underline(scene::Quad {
bounds: RectF::from_points(
underline_origin,
glyph_origin + vec2f(0., 1.),
),
background: Some(color),
border: Default::default(),
corner_radius: 0.,
});
underline_start = None;
}
}
if *run_underlined {
underline_start.get_or_insert(glyph_origin);
}
run_end += *run_len as usize;
color = *run_color;
} else {
run_end = self.layout.len;
color = Color::black();
if let Some(underline_origin) = underline_start.take() {
cx.scene.push_underline(scene::Quad {
bounds: RectF::from_points(
underline_origin,
glyph_origin + vec2f(0., 1.),
),
background: Some(color),
border: Default::default(),
corner_radius: 0.,
});
}
}
}
cx.scene.push_glyph(scene::Glyph {
font_id: run.font_id,
font_size: self.layout.font_size,
id: glyph.id,
origin: glyph_origin,
color,
});
}
}
if let Some(underline_start) = underline_start.take() {
let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.);
cx.scene.push_underline(scene::Quad {
bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)),
background: Some(color),
border: Default::default(),
corner_radius: 0.,
});
}
}
pub fn paint_wrapped(
&self,
origin: Vector2F,
visible_bounds: RectF,
line_height: f32,
boundaries: impl IntoIterator<Item = ShapedBoundary>,
cx: &mut PaintContext,
) {
let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
let baseline_origin = vec2f(0., padding_top + self.layout.ascent);
let mut boundaries = boundaries.into_iter().peekable();
let mut color_runs = self.style_runs.iter();
let mut color_end = 0;
let mut color = Color::black();
let mut glyph_origin = vec2f(0., 0.);
let mut prev_position = 0.;
for run in &self.layout.runs {
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
if boundaries.peek().map_or(false, |b| b.glyph_ix == glyph_ix) {
boundaries.next();
glyph_origin = vec2f(0., glyph_origin.y() + line_height);
} else {
glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
}
prev_position = glyph.position.x();
if glyph.index >= color_end {
if let Some(next_run) = color_runs.next() {
color_end += next_run.0 as usize;
color = next_run.1;
} else {
color_end = self.layout.len;
color = Color::black();
}
}
let glyph_bounds = RectF::new(
origin + glyph_origin,
cx.font_cache
.bounding_box(run.font_id, self.layout.font_size),
);
if glyph_bounds.intersects(visible_bounds) {
cx.scene.push_glyph(scene::Glyph {
font_id: run.font_id,
font_size: self.layout.font_size,
id: glyph.id,
origin: glyph_bounds.origin() + baseline_origin,
color,
});
}
}
}
}
}
impl Run {
pub fn glyphs(&self) -> &[Glyph] {
&self.glyphs
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Boundary {
pub ix: usize,
pub next_indent: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ShapedBoundary {
pub run_ix: usize,
pub glyph_ix: usize,
}
impl Boundary {
fn new(ix: usize, next_indent: u32) -> Self {
Self { ix, next_indent }
}
}
pub struct LineWrapper {
font_system: Arc<dyn FontSystem>,
pub(crate) font_id: FontId,
pub(crate) font_size: f32,
cached_ascii_char_widths: [f32; 128],
cached_other_char_widths: HashMap<char, f32>,
}
impl LineWrapper {
pub const MAX_INDENT: u32 = 256;
pub fn new(font_id: FontId, font_size: f32, font_system: Arc<dyn FontSystem>) -> Self {
Self {
font_system,
font_id,
font_size,
cached_ascii_char_widths: [f32::NAN; 128],
cached_other_char_widths: HashMap::new(),
}
}
pub fn wrap_line<'a>(
&'a mut self,
line: &'a str,
wrap_width: f32,
) -> impl Iterator<Item = Boundary> + 'a {
let mut width = 0.0;
let mut first_non_whitespace_ix = None;
let mut indent = None;
let mut last_candidate_ix = 0;
let mut last_candidate_width = 0.0;
let mut last_wrap_ix = 0;
let mut prev_c = '\0';
let mut char_indices = line.char_indices();
iter::from_fn(move || {
while let Some((ix, c)) = char_indices.next() {
if c == '\n' {
continue;
}
if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
}
if c != ' ' && first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(ix);
}
let char_width = self.width_for_char(c);
width += char_width;
if width > wrap_width && ix > last_wrap_ix {
if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
{
indent = Some(
Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
);
}
if last_candidate_ix > 0 {
last_wrap_ix = last_candidate_ix;
width -= last_candidate_width;
last_candidate_ix = 0;
} else {
last_wrap_ix = ix;
width = char_width;
}
let indent_width =
indent.map(|indent| indent as f32 * self.width_for_char(' '));
width += indent_width.unwrap_or(0.);
return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
}
prev_c = c;
}
None
})
}
pub fn wrap_shaped_line<'a>(
&'a mut self,
str: &'a str,
line: &'a Line,
wrap_width: f32,
) -> impl Iterator<Item = ShapedBoundary> + 'a {
let mut first_non_whitespace_ix = None;
let mut last_candidate_ix = None;
let mut last_candidate_x = 0.0;
let mut last_wrap_ix = ShapedBoundary {
run_ix: 0,
glyph_ix: 0,
};
let mut last_wrap_x = 0.;
let mut prev_c = '\0';
let mut glyphs = line
.runs()
.iter()
.enumerate()
.flat_map(move |(run_ix, run)| {
run.glyphs()
.iter()
.enumerate()
.map(move |(glyph_ix, glyph)| {
let character = str[glyph.index..].chars().next().unwrap();
(
ShapedBoundary { run_ix, glyph_ix },
character,
glyph.position.x(),
)
})
})
.peekable();
iter::from_fn(move || {
while let Some((ix, c, x)) = glyphs.next() {
if c == '\n' {
continue;
}
if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
last_candidate_ix = Some(ix);
last_candidate_x = x;
}
if c != ' ' && first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(ix);
}
let next_x = glyphs.peek().map_or(line.width(), |(_, _, x)| *x);
let width = next_x - last_wrap_x;
if width > wrap_width && ix > last_wrap_ix {
if let Some(last_candidate_ix) = last_candidate_ix.take() {
last_wrap_ix = last_candidate_ix;
last_wrap_x = last_candidate_x;
} else {
last_wrap_ix = ix;
last_wrap_x = x;
}
return Some(last_wrap_ix);
}
prev_c = c;
}
None
})
}
fn is_boundary(&self, prev: char, next: char) -> bool {
(prev == ' ') && (next != ' ')
}
#[inline(always)]
fn width_for_char(&mut self, c: char) -> f32 {
if (c as u32) < 128 {
let mut width = self.cached_ascii_char_widths[c as usize];
if width.is_nan() {
width = self.compute_width_for_char(c);
self.cached_ascii_char_widths[c as usize] = width;
}
width
} else {
let mut width = self
.cached_other_char_widths
.get(&c)
.copied()
.unwrap_or(f32::NAN);
if width.is_nan() {
width = self.compute_width_for_char(c);
self.cached_other_char_widths.insert(c, width);
}
width
}
}
fn compute_width_for_char(&self, c: char) -> f32 {
self.font_system
.layout_line(
&c.to_string(),
self.font_size,
&[(
1,
RunStyle {
font_id: self.font_id,
color: Default::default(),
underline: false,
},
)],
)
.width
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fonts::{Properties, Weight};
#[crate::test(self)]
fn test_wrap_line(cx: &mut crate::MutableAppContext) {
let font_cache = cx.font_cache().clone();
let font_system = cx.platform().fonts();
let family = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache.select_font(family, &Default::default()).unwrap();
let mut wrapper = LineWrapper::new(font_id, 16., font_system);
assert_eq!(
wrapper
.wrap_line("aa bbb cccc ddddd eeee", 72.0)
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(12, 0),
Boundary::new(18, 0)
],
);
assert_eq!(
wrapper
.wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0)
.collect::<Vec<_>>(),
&[
Boundary::new(4, 0),
Boundary::new(11, 0),
Boundary::new(18, 0)
],
);
assert_eq!(
wrapper.wrap_line(" aaaaaaa", 72.).collect::<Vec<_>>(),
&[
Boundary::new(7, 5),
Boundary::new(9, 5),
Boundary::new(11, 5),
]
);
assert_eq!(
wrapper
.wrap_line(" ", 72.)
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(14, 0),
Boundary::new(21, 0)
]
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaaaaaaaaa", 72.)
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(14, 3),
Boundary::new(18, 3),
Boundary::new(22, 3),
]
);
}
#[crate::test(self, retries = 5)]
fn test_wrap_shaped_line(cx: &mut crate::MutableAppContext) {
// This is failing intermittently on CI and we don't have time to figure it out
let font_cache = cx.font_cache().clone();
let font_system = cx.platform().fonts();
let text_layout_cache = TextLayoutCache::new(font_system.clone());
let family = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache.select_font(family, &Default::default()).unwrap();
let normal = RunStyle {
font_id,
color: Default::default(),
underline: false,
};
let bold = RunStyle {
font_id: font_cache
.select_font(
family,
&Properties {
weight: Weight::BOLD,
..Default::default()
},
)
.unwrap(),
color: Default::default(),
underline: false,
};
let text = "aa bbb cccc ddddd eeee";
let line = text_layout_cache.layout_str(
text,
16.0,
&[(4, normal), (5, bold), (6, normal), (1, bold), (7, normal)],
);
let mut wrapper = LineWrapper::new(font_id, 16., font_system);
assert_eq!(
wrapper
.wrap_shaped_line(&text, &line, 72.0)
.collect::<Vec<_>>(),
&[
ShapedBoundary {
run_ix: 1,
glyph_ix: 3
},
ShapedBoundary {
run_ix: 2,
glyph_ix: 3
},
ShapedBoundary {
run_ix: 4,
glyph_ix: 2
}
],
);
}
}

20
crates/gpui/src/util.rs Normal file
View file

@ -0,0 +1,20 @@
use smol::future::FutureExt;
use std::{future::Future, time::Duration};
pub fn post_inc(value: &mut usize) -> usize {
let prev = *value;
*value += 1;
prev
}
pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
where
F: Future<Output = T>,
{
let timer = async {
smol::Timer::after(timeout).await;
Err(())
};
let future = async move { Ok(f.await) };
timer.race(future).await
}

7
crates/gpui/src/views.rs Normal file
View file

@ -0,0 +1,7 @@
mod select;
pub use select::{ItemType, Select, SelectStyle};
pub fn init(cx: &mut super::MutableAppContext) {
select::init(cx);
}

View file

@ -0,0 +1,169 @@
use crate::{
action, elements::*, AppContext, Entity, MutableAppContext, RenderContext, View, ViewContext,
WeakViewHandle,
};
pub struct Select {
handle: WeakViewHandle<Self>,
render_item: Box<dyn Fn(usize, ItemType, bool, &AppContext) -> ElementBox>,
selected_item_ix: usize,
item_count: usize,
is_open: bool,
list_state: UniformListState,
build_style: Option<Box<dyn FnMut(&mut MutableAppContext) -> SelectStyle>>,
}
#[derive(Clone, Default)]
pub struct SelectStyle {
pub header: ContainerStyle,
pub menu: ContainerStyle,
}
pub enum ItemType {
Header,
Selected,
Unselected,
}
action!(ToggleSelect);
action!(SelectItem, usize);
pub enum Event {}
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Select::toggle);
cx.add_action(Select::select_item);
}
impl Select {
pub fn new<F: 'static + Fn(usize, ItemType, bool, &AppContext) -> ElementBox>(
item_count: usize,
cx: &mut ViewContext<Self>,
render_item: F,
) -> Self {
Self {
handle: cx.handle().downgrade(),
render_item: Box::new(render_item),
selected_item_ix: 0,
item_count,
is_open: false,
list_state: UniformListState::default(),
build_style: Default::default(),
}
}
pub fn with_style(
mut self,
f: impl 'static + FnMut(&mut MutableAppContext) -> SelectStyle,
) -> Self {
self.build_style = Some(Box::new(f));
self
}
pub fn set_item_count(&mut self, count: usize, cx: &mut ViewContext<Self>) {
self.item_count = count;
cx.notify();
}
fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext<Self>) {
self.is_open = !self.is_open;
cx.notify();
}
fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext<Self>) {
self.selected_item_ix = action.0;
self.is_open = false;
cx.notify();
}
pub fn selected_index(&self) -> usize {
self.selected_item_ix
}
}
impl Entity for Select {
type Event = Event;
}
impl View for Select {
fn ui_name() -> &'static str {
"Select"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if self.item_count == 0 {
return Empty::new().boxed();
}
enum Header {}
enum Item {}
let style = if let Some(build_style) = self.build_style.as_mut() {
(build_style)(cx)
} else {
Default::default()
};
let mut result = Flex::column().with_child(
MouseEventHandler::new::<Header, _, _, _>(self.handle.id(), cx, |mouse_state, cx| {
Container::new((self.render_item)(
self.selected_item_ix,
ItemType::Header,
mouse_state.hovered,
cx,
))
.with_style(style.header)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSelect))
.boxed(),
);
if self.is_open {
let handle = self.handle.clone();
result.add_child(
Overlay::new(
Container::new(
ConstrainedBox::new(
UniformList::new(
self.list_state.clone(),
self.item_count,
move |mut range, items, cx| {
let handle = handle.upgrade(cx).unwrap();
let this = handle.read(cx);
let selected_item_ix = this.selected_item_ix;
range.end = range.end.min(this.item_count);
items.extend(range.map(|ix| {
MouseEventHandler::new::<Item, _, _, _>(
(handle.id(), ix),
cx,
|mouse_state, cx| {
(handle.read(cx).render_item)(
ix,
if ix == selected_item_ix {
ItemType::Selected
} else {
ItemType::Unselected
},
mouse_state.hovered,
cx,
)
},
)
.on_click(move |cx| cx.dispatch_action(SelectItem(ix)))
.boxed()
}))
},
)
.boxed(),
)
.with_max_height(200.)
.boxed(),
)
.with_style(style.menu)
.boxed(),
)
.boxed(),
)
}
result.boxed()
}
}

View file

@ -0,0 +1,12 @@
[package]
name = "gpui_macros"
version = "0.1.0"
edition = "2018"
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"

View file

@ -0,0 +1,275 @@
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use std::mem;
use syn::{
parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, FnArg, ItemFn, Lit, Meta,
NestedMeta, Type,
};
#[proc_macro_attribute]
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
let mut namespace = format_ident!("gpui");
let args = syn::parse_macro_input!(args as AttributeArgs);
let mut max_retries = 0;
let mut num_iterations = 1;
let mut starting_seed = 0;
for arg in args {
match arg {
NestedMeta::Meta(Meta::Path(name))
if name.get_ident().map_or(false, |n| n == "self") =>
{
namespace = format_ident!("crate");
}
NestedMeta::Meta(Meta::NameValue(meta)) => {
let key_name = meta.path.get_ident().map(|i| i.to_string());
let result = (|| {
match key_name.as_ref().map(String::as_str) {
Some("retries") => max_retries = parse_int(&meta.lit)?,
Some("iterations") => num_iterations = parse_int(&meta.lit)?,
Some("seed") => starting_seed = parse_int(&meta.lit)?,
_ => {
return Err(TokenStream::from(
syn::Error::new(meta.path.span(), "invalid argument")
.into_compile_error(),
))
}
}
Ok(())
})();
if let Err(tokens) = result {
return tokens;
}
}
other => {
return TokenStream::from(
syn::Error::new_spanned(other, "invalid argument").into_compile_error(),
)
}
}
}
let mut inner_fn = parse_macro_input!(function as ItemFn);
if max_retries > 0 && num_iterations > 1 {
return TokenStream::from(
syn::Error::new_spanned(inner_fn, "retries and randomized iterations can't be mixed")
.into_compile_error(),
);
}
let inner_fn_attributes = mem::take(&mut inner_fn.attrs);
let inner_fn_name = format_ident!("_{}", inner_fn.sig.ident);
let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone());
let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() {
// Pass to the test function the number of app contexts that it needs,
// based on its parameter list.
let mut inner_fn_args = proc_macro2::TokenStream::new();
for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() {
if let FnArg::Typed(arg) = arg {
if let Type::Path(ty) = &*arg.ty {
let last_segment = ty.path.segments.last();
match last_segment.map(|s| s.ident.to_string()).as_deref() {
Some("TestAppContext") => {
let first_entity_id = ix * 100_000;
inner_fn_args.extend(quote!(
#namespace::TestAppContext::new(
foreground_platform.clone(),
platform.clone(),
foreground.clone(),
background.clone(),
font_cache.clone(),
#first_entity_id,
),
));
}
Some("StdRng") => {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed)));
}
_ => {
return TokenStream::from(
syn::Error::new_spanned(arg, "invalid argument")
.into_compile_error(),
)
}
}
} else {
return TokenStream::from(
syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
);
}
} else {
return TokenStream::from(
syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
);
}
}
parse_quote! {
#[test]
fn #outer_fn_name() {
#inner_fn
let is_randomized = #num_iterations > 1;
let mut num_iterations = #num_iterations as u64;
let mut starting_seed = #starting_seed as u64;
if is_randomized {
if let Ok(value) = std::env::var("SEED") {
starting_seed = value.parse().expect("invalid SEED variable");
}
if let Ok(value) = std::env::var("ITERATIONS") {
num_iterations = value.parse().expect("invalid ITERATIONS variable");
}
}
let mut atomic_seed = std::sync::atomic::AtomicU64::new(starting_seed as u64);
let mut retries = 0;
loop {
let result = std::panic::catch_unwind(|| {
let foreground_platform = std::rc::Rc::new(#namespace::platform::test::foreground_platform());
let platform = std::sync::Arc::new(#namespace::platform::test::platform());
let font_system = #namespace::Platform::fonts(platform.as_ref());
let font_cache = std::sync::Arc::new(#namespace::FontCache::new(font_system));
loop {
let seed = atomic_seed.load(std::sync::atomic::Ordering::SeqCst);
if seed >= starting_seed + num_iterations {
break;
}
if is_randomized {
dbg!(seed);
}
let (foreground, background) = #namespace::executor::deterministic(seed);
foreground.run(#inner_fn_name(#inner_fn_args));
atomic_seed.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
});
match result {
Ok(result) => {
break;
}
Err(error) => {
if retries < #max_retries {
retries += 1;
println!("retrying: attempt {}", retries);
} else {
if is_randomized {
eprintln!("failing seed: {}", atomic_seed.load(std::sync::atomic::Ordering::SeqCst));
}
std::panic::resume_unwind(error);
}
}
}
}
}
}
} else {
let mut inner_fn_args = proc_macro2::TokenStream::new();
for arg in inner_fn.sig.inputs.iter() {
if let FnArg::Typed(arg) = arg {
if let Type::Path(ty) = &*arg.ty {
let last_segment = ty.path.segments.last();
if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
}
} else {
inner_fn_args.extend(quote!(cx,));
}
} else {
return TokenStream::from(
syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
);
}
}
parse_quote! {
#[test]
fn #outer_fn_name() {
#inner_fn
let is_randomized = #num_iterations > 1;
let mut num_iterations = #num_iterations as u64;
let mut starting_seed = #starting_seed as u64;
if is_randomized {
if let Ok(value) = std::env::var("SEED") {
starting_seed = value.parse().expect("invalid SEED variable");
}
if let Ok(value) = std::env::var("ITERATIONS") {
num_iterations = value.parse().expect("invalid ITERATIONS variable");
}
}
let mut atomic_seed = std::sync::atomic::AtomicU64::new(starting_seed as u64);
let mut retries = 0;
loop {
let result = std::panic::catch_unwind(|| {
let foreground_platform = std::rc::Rc::new(#namespace::platform::test::foreground_platform());
let platform = std::sync::Arc::new(#namespace::platform::test::platform());
let font_system = #namespace::Platform::fonts(platform.as_ref());
let font_cache = std::sync::Arc::new(#namespace::FontCache::new(font_system));
loop {
let seed = atomic_seed.load(std::sync::atomic::Ordering::SeqCst);
if seed >= starting_seed + num_iterations {
break;
}
if is_randomized {
dbg!(seed);
}
let (foreground, background) = #namespace::executor::deterministic(seed);
let mut cx = #namespace::TestAppContext::new(
foreground_platform.clone(),
platform.clone(),
foreground.clone(),
background.clone(),
font_cache.clone(),
0,
);
cx.update(|cx| #inner_fn_name(#inner_fn_args));
atomic_seed.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
});
match result {
Ok(_) => {
break;
}
Err(error) => {
if retries < #max_retries {
retries += 1;
println!("retrying: attempt {}", retries);
} else {
if is_randomized {
eprintln!("failing seed: {}", atomic_seed.load(std::sync::atomic::Ordering::SeqCst));
}
std::panic::resume_unwind(error);
}
}
}
}
}
}
};
outer_fn.attrs.extend(inner_fn_attributes);
TokenStream::from(quote!(#outer_fn))
}
fn parse_int(literal: &Lit) -> Result<usize, TokenStream> {
let result = if let Lit::Int(int) = &literal {
int.base10_parse()
} else {
Err(syn::Error::new(literal.span(), "must be an integer"))
};
result.map_err(|err| TokenStream::from(err.into_compile_error()))
}

View file

@ -0,0 +1,24 @@
[package]
name = "rpc_client"
version = "0.1.0"
edition = "2018"
[features]
test-support = []
[dependencies]
anyhow = "1.0.38"
async-recursion = "0.3"
async-tungstenite = { version = "0.14", features = ["async-tls"] }
gpui = { path = "../gpui" }
lazy_static = "1.4.0"
log = "0.4"
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8.3"
smol = "1.2.5"
surf = "2.2"
thiserror = "1.0.29"
tiny_http = "0.8"
util = { path = "../util" }
zrpc = { path = "../zrpc" }

View file

@ -0,0 +1,763 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
use anyhow::{anyhow, Context, Result};
use async_recursion::async_recursion;
use async_tungstenite::tungstenite::{
error::Error as WebsocketError,
http::{Request, StatusCode},
};
use gpui::{AsyncAppContext, Entity, ModelContext, Task};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::{prelude::Stream, watch};
use rand::prelude::*;
use std::{
any::TypeId,
collections::HashMap,
convert::TryFrom,
fmt::Write as _,
future::Future,
sync::{Arc, Weak},
time::{Duration, Instant},
};
use surf::Url;
use thiserror::Error;
use util::ResultExt;
pub use zrpc::{proto, ConnectionId, PeerId, TypedEnvelope};
use zrpc::{
proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
Connection, Peer, Receipt,
};
lazy_static! {
static ref ZED_SERVER_URL: String =
std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev:443".to_string());
static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
}
pub struct Client {
peer: Arc<Peer>,
state: RwLock<ClientState>,
authenticate:
Option<Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>>>,
establish_connection: Option<
Box<
dyn 'static
+ Send
+ Sync
+ Fn(
&Credentials,
&AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>>,
>,
>,
}
#[derive(Error, Debug)]
pub enum EstablishConnectionError {
#[error("upgrade required")]
UpgradeRequired,
#[error("unauthorized")]
Unauthorized,
#[error("{0}")]
Other(#[from] anyhow::Error),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Http(#[from] async_tungstenite::tungstenite::http::Error),
}
impl From<WebsocketError> for EstablishConnectionError {
fn from(error: WebsocketError) -> Self {
if let WebsocketError::Http(response) = &error {
match response.status() {
StatusCode::UNAUTHORIZED => return EstablishConnectionError::Unauthorized,
StatusCode::UPGRADE_REQUIRED => return EstablishConnectionError::UpgradeRequired,
_ => {}
}
}
EstablishConnectionError::Other(error.into())
}
}
impl EstablishConnectionError {
pub fn other(error: impl Into<anyhow::Error> + Send + Sync) -> Self {
Self::Other(error.into())
}
}
#[derive(Copy, Clone, Debug)]
pub enum Status {
SignedOut,
UpgradeRequired,
Authenticating,
Connecting,
ConnectionError,
Connected { connection_id: ConnectionId },
ConnectionLost,
Reauthenticating,
Reconnecting,
ReconnectionError { next_reconnection: Instant },
}
struct ClientState {
credentials: Option<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
model_handlers: HashMap<
(TypeId, u64),
Box<dyn Send + Sync + FnMut(Box<dyn AnyTypedEnvelope>, &mut AsyncAppContext)>,
>,
_maintain_connection: Option<Task<()>>,
heartbeat_interval: Duration,
}
#[derive(Clone)]
pub struct Credentials {
pub user_id: u64,
pub access_token: String,
}
impl Default for ClientState {
fn default() -> Self {
Self {
credentials: None,
status: watch::channel_with(Status::SignedOut),
entity_id_extractors: Default::default(),
model_handlers: Default::default(),
_maintain_connection: None,
heartbeat_interval: Duration::from_secs(5),
}
}
}
pub struct Subscription {
client: Weak<Client>,
id: (TypeId, u64),
}
impl Drop for Subscription {
fn drop(&mut self) {
if let Some(client) = self.client.upgrade() {
drop(
client
.state
.write()
.model_handlers
.remove(&self.id)
.unwrap(),
);
}
}
}
impl Client {
pub fn new() -> Arc<Self> {
Arc::new(Self {
peer: Peer::new(),
state: Default::default(),
authenticate: None,
establish_connection: None,
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn override_authenticate<F>(&mut self, authenticate: F) -> &mut Self
where
F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>,
{
self.authenticate = Some(Box::new(authenticate));
self
}
#[cfg(any(test, feature = "test-support"))]
pub fn override_establish_connection<F>(&mut self, connect: F) -> &mut Self
where
F: 'static
+ Send
+ Sync
+ Fn(&Credentials, &AsyncAppContext) -> Task<Result<Connection, EstablishConnectionError>>,
{
self.establish_connection = Some(Box::new(connect));
self
}
pub fn user_id(&self) -> Option<u64> {
self.state
.read()
.credentials
.as_ref()
.map(|credentials| credentials.user_id)
}
pub fn status(&self) -> watch::Receiver<Status> {
self.state.read().status.1.clone()
}
fn set_status(self: &Arc<Self>, status: Status, cx: &AsyncAppContext) {
let mut state = self.state.write();
*state.status.0.borrow_mut() = status;
match status {
Status::Connected { .. } => {
let heartbeat_interval = state.heartbeat_interval;
let this = self.clone();
let foreground = cx.foreground();
state._maintain_connection = Some(cx.foreground().spawn(async move {
loop {
foreground.timer(heartbeat_interval).await;
let _ = this.request(proto::Ping {}).await;
}
}));
}
Status::ConnectionLost => {
let this = self.clone();
let foreground = cx.foreground();
let heartbeat_interval = state.heartbeat_interval;
state._maintain_connection = Some(cx.spawn(|cx| async move {
let mut rng = StdRng::from_entropy();
let mut delay = Duration::from_millis(100);
while let Err(error) = this.authenticate_and_connect(&cx).await {
log::error!("failed to connect {}", error);
this.set_status(
Status::ReconnectionError {
next_reconnection: Instant::now() + delay,
},
&cx,
);
foreground.timer(delay).await;
delay = delay
.mul_f32(rng.gen_range(1.0..=2.0))
.min(heartbeat_interval);
}
}));
}
Status::SignedOut | Status::UpgradeRequired => {
state._maintain_connection.take();
}
_ => {}
}
}
pub fn subscribe<T, M, F>(
self: &Arc<Self>,
cx: &mut ModelContext<M>,
mut handler: F,
) -> Subscription
where
T: EnvelopedMessage,
M: Entity,
F: 'static
+ Send
+ Sync
+ FnMut(&mut M, TypedEnvelope<T>, Arc<Self>, &mut ModelContext<M>) -> Result<()>,
{
let subscription_id = (TypeId::of::<T>(), Default::default());
let client = self.clone();
let mut state = self.state.write();
let model = cx.handle().downgrade();
let prev_extractor = state
.entity_id_extractors
.insert(subscription_id.0, Box::new(|_| Default::default()));
if prev_extractor.is_some() {
panic!("registered a handler for the same entity twice")
}
state.model_handlers.insert(
subscription_id,
Box::new(move |envelope, cx| {
if let Some(model) = model.upgrade(cx) {
let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap();
model.update(cx, |model, cx| {
if let Err(error) = handler(model, *envelope, client.clone(), cx) {
log::error!("error handling message: {}", error)
}
});
}
}),
);
Subscription {
client: Arc::downgrade(self),
id: subscription_id,
}
}
pub fn subscribe_to_entity<T, M, F>(
self: &Arc<Self>,
remote_id: u64,
cx: &mut ModelContext<M>,
mut handler: F,
) -> Subscription
where
T: EntityMessage,
M: Entity,
F: 'static
+ Send
+ Sync
+ FnMut(&mut M, TypedEnvelope<T>, Arc<Self>, &mut ModelContext<M>) -> Result<()>,
{
let subscription_id = (TypeId::of::<T>(), remote_id);
let client = self.clone();
let mut state = self.state.write();
let model = cx.handle().downgrade();
state
.entity_id_extractors
.entry(subscription_id.0)
.or_insert_with(|| {
Box::new(|envelope| {
let envelope = envelope
.as_any()
.downcast_ref::<TypedEnvelope<T>>()
.unwrap();
envelope.payload.remote_entity_id()
})
});
let prev_handler = state.model_handlers.insert(
subscription_id,
Box::new(move |envelope, cx| {
if let Some(model) = model.upgrade(cx) {
let envelope = envelope.into_any().downcast::<TypedEnvelope<T>>().unwrap();
model.update(cx, |model, cx| {
if let Err(error) = handler(model, *envelope, client.clone(), cx) {
log::error!("error handling message: {}", error)
}
});
}
}),
);
if prev_handler.is_some() {
panic!("registered a handler for the same entity twice")
}
Subscription {
client: Arc::downgrade(self),
id: subscription_id,
}
}
#[async_recursion(?Send)]
pub async fn authenticate_and_connect(
self: &Arc<Self>,
cx: &AsyncAppContext,
) -> anyhow::Result<()> {
let was_disconnected = match *self.status().borrow() {
Status::SignedOut => true,
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
false
}
Status::Connected { .. }
| Status::Connecting { .. }
| Status::Reconnecting { .. }
| Status::Authenticating
| Status::Reauthenticating => return Ok(()),
Status::UpgradeRequired => return Err(EstablishConnectionError::UpgradeRequired)?,
};
if was_disconnected {
self.set_status(Status::Authenticating, cx);
} else {
self.set_status(Status::Reauthenticating, cx)
}
let mut used_keychain = false;
let credentials = self.state.read().credentials.clone();
let credentials = if let Some(credentials) = credentials {
credentials
} else if let Some(credentials) = read_credentials_from_keychain(cx) {
used_keychain = true;
credentials
} else {
let credentials = match self.authenticate(&cx).await {
Ok(credentials) => credentials,
Err(err) => {
self.set_status(Status::ConnectionError, cx);
return Err(err);
}
};
credentials
};
if was_disconnected {
self.set_status(Status::Connecting, cx);
} else {
self.set_status(Status::Reconnecting, cx);
}
match self.establish_connection(&credentials, cx).await {
Ok(conn) => {
log::info!("connected to rpc address {}", *ZED_SERVER_URL);
self.state.write().credentials = Some(credentials.clone());
if !used_keychain && IMPERSONATE_LOGIN.is_none() {
write_credentials_to_keychain(&credentials, cx).log_err();
}
self.set_connection(conn, cx).await;
Ok(())
}
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
if used_keychain {
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(cx).await
} else {
self.set_status(Status::ConnectionError, cx);
Err(EstablishConnectionError::Unauthorized)?
}
}
Err(EstablishConnectionError::UpgradeRequired) => {
self.set_status(Status::UpgradeRequired, cx);
Err(EstablishConnectionError::UpgradeRequired)?
}
Err(error) => {
self.set_status(Status::ConnectionError, cx);
Err(error)?
}
}
}
async fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await;
cx.foreground()
.spawn({
let mut cx = cx.clone();
let this = self.clone();
async move {
while let Some(message) = incoming.recv().await {
let mut state = this.state.write();
if let Some(extract_entity_id) =
state.entity_id_extractors.get(&message.payload_type_id())
{
let payload_type_id = message.payload_type_id();
let entity_id = (extract_entity_id)(message.as_ref());
let handler_key = (payload_type_id, entity_id);
if let Some(mut handler) = state.model_handlers.remove(&handler_key) {
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
let start_time = Instant::now();
log::info!("RPC client message {}", message.payload_type_name());
(handler)(message, &mut cx);
log::info!(
"RPC message handled. duration:{:?}",
start_time.elapsed()
);
this.state
.write()
.model_handlers
.insert(handler_key, handler);
} else {
log::info!("unhandled message {}", message.payload_type_name());
}
} else {
log::info!("unhandled message {}", message.payload_type_name());
}
}
}
})
.detach();
self.set_status(Status::Connected { connection_id }, cx);
let handle_io = cx.background().spawn(handle_io);
let this = self.clone();
let cx = cx.clone();
cx.foreground()
.spawn(async move {
match handle_io.await {
Ok(()) => this.set_status(Status::SignedOut, &cx),
Err(err) => {
log::error!("connection error: {:?}", err);
this.set_status(Status::ConnectionLost, &cx);
}
}
})
.detach();
}
fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<Credentials>> {
if let Some(callback) = self.authenticate.as_ref() {
callback(cx)
} else {
self.authenticate_with_browser(cx)
}
}
fn establish_connection(
self: &Arc<Self>,
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> {
if let Some(callback) = self.establish_connection.as_ref() {
callback(credentials, cx)
} else {
self.establish_websocket_connection(credentials, cx)
}
}
fn establish_websocket_connection(
self: &Arc<Self>,
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> {
let request = Request::builder()
.header(
"Authorization",
format!("{} {}", credentials.user_id, credentials.access_token),
)
.header("X-Zed-Protocol-Version", zrpc::PROTOCOL_VERSION);
cx.background().spawn(async move {
if let Some(host) = ZED_SERVER_URL.strip_prefix("https://") {
let stream = smol::net::TcpStream::connect(host).await?;
let request = request.uri(format!("wss://{}/rpc", host)).body(())?;
let (stream, _) =
async_tungstenite::async_tls::client_async_tls(request, stream).await?;
Ok(Connection::new(stream))
} else if let Some(host) = ZED_SERVER_URL.strip_prefix("http://") {
let stream = smol::net::TcpStream::connect(host).await?;
let request = request.uri(format!("ws://{}/rpc", host)).body(())?;
let (stream, _) = async_tungstenite::client_async(request, stream).await?;
Ok(Connection::new(stream))
} else {
Err(anyhow!("invalid server url: {}", *ZED_SERVER_URL))?
}
})
}
pub fn authenticate_with_browser(
self: &Arc<Self>,
cx: &AsyncAppContext,
) -> Task<Result<Credentials>> {
let platform = cx.platform();
let executor = cx.background();
executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the
// zed server to encrypt the user's access token, so that it can'be intercepted by
// any other app running on the user's device.
let (public_key, private_key) =
zrpc::auth::keypair().expect("failed to generate keypair for auth");
let public_key_string =
String::try_from(public_key).expect("failed to serialize public key for auth");
// Start an HTTP server to receive the redirect from Zed's sign-in page.
let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
let port = server.server_addr().port();
// Open the Zed sign-in page in the user's browser, with query parameters that indicate
// that the user is signing in from a Zed app running on the same device.
let mut url = format!(
"{}/sign_in?native_app_port={}&native_app_public_key={}",
*ZED_SERVER_URL, port, public_key_string
);
if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
log::info!("impersonating user @{}", impersonate_login);
write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
}
platform.open_url(&url);
// Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
// access token from the query params.
//
// TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
// custom URL scheme instead of this local HTTP server.
let (user_id, access_token) = executor
.spawn(async move {
if let Some(req) = server.recv_timeout(Duration::from_secs(10 * 60))? {
let path = req.url();
let mut user_id = None;
let mut access_token = None;
let url = Url::parse(&format!("http://example.com{}", path))
.context("failed to parse login notification url")?;
for (key, value) in url.query_pairs() {
if key == "access_token" {
access_token = Some(value.to_string());
} else if key == "user_id" {
user_id = Some(value.to_string());
}
}
req.respond(
tiny_http::Response::from_string(LOGIN_RESPONSE).with_header(
tiny_http::Header::from_bytes("Content-Type", "text/html").unwrap(),
),
)
.context("failed to respond to login http request")?;
Ok((
user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?,
access_token
.ok_or_else(|| anyhow!("missing access_token parameter"))?,
))
} else {
Err(anyhow!("didn't receive login redirect"))
}
})
.await?;
let access_token = private_key
.decrypt_string(&access_token)
.context("failed to decrypt access token")?;
platform.activate(true);
Ok(Credentials {
user_id: user_id.parse()?,
access_token,
})
})
}
pub async fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
let conn_id = self.connection_id()?;
self.peer.disconnect(conn_id).await;
self.set_status(Status::SignedOut, cx);
Ok(())
}
fn connection_id(&self) -> Result<ConnectionId> {
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
Ok(connection_id)
} else {
Err(anyhow!("not connected"))
}
}
pub async fn send<T: EnvelopedMessage>(&self, message: T) -> Result<()> {
self.peer.send(self.connection_id()?, message).await
}
pub async fn request<T: RequestMessage>(&self, request: T) -> Result<T::Response> {
self.peer.request(self.connection_id()?, request).await
}
pub fn respond<T: RequestMessage>(
&self,
receipt: Receipt<T>,
response: T::Response,
) -> impl Future<Output = Result<()>> {
self.peer.respond(receipt, response)
}
}
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
if IMPERSONATE_LOGIN.is_some() {
return None;
}
let (user_id, access_token) = cx
.platform()
.read_credentials(&ZED_SERVER_URL)
.log_err()
.flatten()?;
Some(Credentials {
user_id: user_id.parse().ok()?,
access_token: String::from_utf8(access_token).ok()?,
})
}
fn write_credentials_to_keychain(credentials: &Credentials, cx: &AsyncAppContext) -> Result<()> {
cx.platform().write_credentials(
&ZED_SERVER_URL,
&credentials.user_id.to_string(),
credentials.access_token.as_bytes(),
)
}
const WORKTREE_URL_PREFIX: &'static str = "zed://worktrees/";
pub fn encode_worktree_url(id: u64, access_token: &str) -> String {
format!("{}{}/{}", WORKTREE_URL_PREFIX, id, access_token)
}
pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> {
let path = url.trim().strip_prefix(WORKTREE_URL_PREFIX)?;
let mut parts = path.split('/');
let id = parts.next()?.parse::<u64>().ok()?;
let access_token = parts.next()?;
if access_token.is_empty() {
return None;
}
Some((id, access_token.to_string()))
}
const LOGIN_RESPONSE: &'static str = "
<!DOCTYPE html>
<html>
<script>window.close();</script>
</html>
";
#[cfg(test)]
mod tests {
use super::*;
use crate::test::FakeServer;
use gpui::TestAppContext;
#[gpui::test(iterations = 10)]
async fn test_heartbeat(cx: TestAppContext) {
cx.foreground().forbid_parking();
let user_id = 5;
let mut client = Client::new();
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
cx.foreground().advance_clock(Duration::from_secs(10));
let ping = server.receive::<proto::Ping>().await.unwrap();
server.respond(ping.receipt(), proto::Ack {}).await;
cx.foreground().advance_clock(Duration::from_secs(10));
let ping = server.receive::<proto::Ping>().await.unwrap();
server.respond(ping.receipt(), proto::Ack {}).await;
client.disconnect(&cx.to_async()).await.unwrap();
assert!(server.receive::<proto::Ping>().await.is_err());
}
#[gpui::test(iterations = 10)]
async fn test_reconnection(cx: TestAppContext) {
cx.foreground().forbid_parking();
let user_id = 5;
let mut client = Client::new();
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
let mut status = client.status();
assert!(matches!(
status.recv().await,
Some(Status::Connected { .. })
));
assert_eq!(server.auth_count(), 1);
server.forbid_connections();
server.disconnect().await;
while !matches!(status.recv().await, Some(Status::ReconnectionError { .. })) {}
server.allow_connections();
cx.foreground().advance_clock(Duration::from_secs(10));
while !matches!(status.recv().await, Some(Status::Connected { .. })) {}
assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting
server.forbid_connections();
server.disconnect().await;
while !matches!(status.recv().await, Some(Status::ReconnectionError { .. })) {}
// Clear cached credentials after authentication fails
server.roll_access_token();
server.allow_connections();
cx.foreground().advance_clock(Duration::from_secs(10));
assert_eq!(server.auth_count(), 1);
cx.foreground().advance_clock(Duration::from_secs(10));
while !matches!(status.recv().await, Some(Status::Connected { .. })) {}
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
}
#[test]
fn test_encode_and_decode_worktree_url() {
let url = encode_worktree_url(5, "deadbeef");
assert_eq!(decode_worktree_url(&url), Some((5, "deadbeef".to_string())));
assert_eq!(
decode_worktree_url(&format!("\n {}\t", url)),
Some((5, "deadbeef".to_string()))
);
assert_eq!(decode_worktree_url("not://the-right-format"), None);
}
}

View file

@ -0,0 +1,156 @@
use super::*;
use std::sync::atomic::Ordering::SeqCst;
use super::Client;
use gpui::TestAppContext;
use parking_lot::Mutex;
use postage::{mpsc, prelude::Stream};
use std::sync::{
atomic::{AtomicBool, AtomicUsize},
Arc,
};
use zrpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
pub struct FakeServer {
peer: Arc<Peer>,
incoming: Mutex<Option<mpsc::Receiver<Box<dyn proto::AnyTypedEnvelope>>>>,
connection_id: Mutex<Option<ConnectionId>>,
forbid_connections: AtomicBool,
auth_count: AtomicUsize,
access_token: AtomicUsize,
user_id: u64,
}
impl FakeServer {
pub async fn for_client(
client_user_id: u64,
client: &mut Arc<Client>,
cx: &TestAppContext,
) -> Arc<Self> {
let server = Arc::new(Self {
peer: Peer::new(),
incoming: Default::default(),
connection_id: Default::default(),
forbid_connections: Default::default(),
auth_count: Default::default(),
access_token: Default::default(),
user_id: client_user_id,
});
Arc::get_mut(client)
.unwrap()
.override_authenticate({
let server = server.clone();
move |cx| {
server.auth_count.fetch_add(1, SeqCst);
let access_token = server.access_token.load(SeqCst).to_string();
cx.spawn(move |_| async move {
Ok(Credentials {
user_id: client_user_id,
access_token,
})
})
}
})
.override_establish_connection({
let server = server.clone();
move |credentials, cx| {
let credentials = credentials.clone();
cx.spawn({
let server = server.clone();
move |cx| async move { server.establish_connection(&credentials, &cx).await }
})
}
});
client
.authenticate_and_connect(&cx.to_async())
.await
.unwrap();
server
}
pub async fn disconnect(&self) {
self.peer.disconnect(self.connection_id()).await;
self.connection_id.lock().take();
self.incoming.lock().take();
}
async fn establish_connection(
&self,
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Result<Connection, EstablishConnectionError> {
assert_eq!(credentials.user_id, self.user_id);
if self.forbid_connections.load(SeqCst) {
Err(EstablishConnectionError::Other(anyhow!(
"server is forbidding connections"
)))?
}
if credentials.access_token != self.access_token.load(SeqCst).to_string() {
Err(EstablishConnectionError::Unauthorized)?
}
let (client_conn, server_conn, _) = Connection::in_memory();
let (connection_id, io, incoming) = self.peer.add_connection(server_conn).await;
cx.background().spawn(io).detach();
*self.incoming.lock() = Some(incoming);
*self.connection_id.lock() = Some(connection_id);
Ok(client_conn)
}
pub fn auth_count(&self) -> usize {
self.auth_count.load(SeqCst)
}
pub fn roll_access_token(&self) {
self.access_token.fetch_add(1, SeqCst);
}
pub fn forbid_connections(&self) {
self.forbid_connections.store(true, SeqCst);
}
pub fn allow_connections(&self) {
self.forbid_connections.store(false, SeqCst);
}
pub async fn send<T: proto::EnvelopedMessage>(&self, message: T) {
self.peer.send(self.connection_id(), message).await.unwrap();
}
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
let message = self
.incoming
.lock()
.as_mut()
.expect("not connected")
.recv()
.await
.ok_or_else(|| anyhow!("other half hung up"))?;
let type_name = message.payload_type_name();
Ok(*message
.into_any()
.downcast::<TypedEnvelope<M>>()
.unwrap_or_else(|_| {
panic!(
"fake server received unexpected message type: {:?}",
type_name
);
}))
}
pub async fn respond<T: proto::RequestMessage>(
&self,
receipt: Receipt<T>,
response: T::Response,
) {
self.peer.respond(receipt, response).await.unwrap()
}
fn connection_id(&self) -> ConnectionId {
self.connection_id.lock().expect("not connected")
}
}

View file

@ -0,0 +1,11 @@
DATABASE_URL = "postgres://postgres@localhost/zed"
SESSION_SECRET = "6E1GS6IQNOLIBKWMEVWF1AFO4H78KNU8"
HTTP_PORT = 8080
# Available at https://github.com/organizations/zed-industries/settings/apps/zed-local-development
GITHUB_APP_ID = 115633
GITHUB_CLIENT_ID = "Iv1.768076c9becc75c4"
GITHUB_CLIENT_SECRET = ""
GITHUB_PRIVATE_KEY = """\
"""

41
crates/server/.env.toml Normal file
View file

@ -0,0 +1,41 @@
# Prod database: CAREFUL!
# DATABASE_URL = "postgres://postgres:f71db7645055488d666f3c26392113104706af1f24d2cf15@zed-db.internal:5432/zed"
HTTP_PORT = 8080
DATABASE_URL = "postgres://postgres@localhost/zed"
SESSION_SECRET = "6E1GS6IQNOLIBKWMEVWF1AFO4H78KNU8"
# Available at https://github.com/organizations/zed-industries/settings/apps/zed-local-development
GITHUB_APP_ID = 115633
GITHUB_CLIENT_ID = "Iv1.768076c9becc75c4"
GITHUB_CLIENT_SECRET = "3592ffff1ecda9773a3df7b0e75375bfbe7992fc"
GITHUB_PRIVATE_KEY = """\
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAtt0O2t69ksn2zX5ucHpflNRoqdh342OOwrazLA6GS8Kp2hWM
NwLzymm2s8k1e2F7sAVYNHJvUPZCvM/xYuVMNpx33fVr00Tni2ATNJKS2lvCEBC0
nTUKxXQImF82IQadg41o+81gofR3zt2UM7iDRMPbmn/aZe7K8vvFEERawSfKEMv3
RqAzqt0fBDYvwHonje0Y7/5IAO5GDMd9kDE3w034ckwtyFAJDjRGYN5kVoRlua+Q
aIHoBkJ/jUAsS4kWqZt/r6hbrAcgok7Iv2RoapfgNTPeJKEe0pAagz1orqbrm9Qk
WBeAToTXl4YTfQruMNVyN2/5azqLnS8Urg2jHQIDAQABAoIBAF9TVY8bVk/TIOl2
4zOXV4RKRlVkFvtexukSPMzWtYOA8vJREUsMKvJ1sVx/o3WyF7xmzNhqX0UhWyD6
dadMSTKe1o3Khm8YGGw7pUdesVdLRhsB2mWpZPgRyPlFiP4maK5PZU7+fUVwH5Sj
RcLAiQ2r3CrqQ3unw/xu6wfT2kueBMJz6DBCx3y5wwEyrR7b+8ZGrjUy9BelzKti
yIT3OLWhilwho8l03Dg72SCSskotVMcywtc7SMr5PCILL7QANdJDhEO8FP4BysHx
6wlFwpfIPnNHN/RN1Dnnut5F64nPu//6vUs9DR9c34FzDp0SR2hJ98PLYn3uyD5b
6oOcZrECgYEA3QXrezpLwkZN2wS6r6vmNyEKSIevjpQpuFEzGSapJRJbGiP5/C+l
DfTmYud6Ld5YrL7xIQuf6SQWyO8WZkKA6D15VBdsFzM0pzhNGNGUgZYiTQ6rdh83
5mL8l9IqzT5LD5RRXTj2CO7SB5iuyp8PrPyGCCVhILYJP+a4e4kHwEsCgYEA0803
oF/QBhfKC3n/7xbRTeT4PcAHra+X84rY+KkyP1/qJXMRbCumpvTL6kZg7Jv2I3hG
SaRK7mGhi0/omVn9aEgn4E7UKmE2ZhVbigTiqnPdYoH/hmrbQ5Z7SVaT/MNzGuKQ
QZOmASgsZEjqSX7extXDzKOGD/AzMp3iWElUGTcCgYAOoT+vDnLJT0IEB1IcIrLA
X22A04ppU6FXU/if55E2pPpmxo7bhIPWYqmFTnEl7BvOg20OlOhm1D612i2PY0OJ
G9iWGl7LQlZv4ygnRmggE8H9e8UZsoNOuqqhmgW/RCpPw6+HDigq+zPn0NFxFApD
lwuAKok9Uw9VrX30n2Nl9QKBgAG7c/ED15e1Khnd7ZHvBdc1QDKBF478GKoNQKkH
+Tk7d5bG0iWoVbyX0/MekDxfKiwwF6MSjOpWMhQJm0VlzwTDUlArVODj2qYLFqyS
TahHOlBL7+MRjKmI2YlIA/3VO2PE5pkitADeaz6GuiPPvdKyfN93lukaddC8KdW/
A8kRAoGBAJdU+sTC1zfP+tbgArzf4rU5qEknserAH+GE6C/Otn134WBEyqSgd2Jb
JpJsl2l/X/8Wfwh+SJQbhvDoY4ApMKlgLFBAIY/p2UcpEdUL2juec/F6+qGnBncQ
4I+MKiVfixBM9p66Afybiskh3a/RvXK+/6NNOVtVYaSd7aSIrq9W
-----END RSA PRIVATE KEY-----
"""

62
crates/server/Cargo.toml Normal file
View file

@ -0,0 +1,62 @@
[package]
authors = ["Nathan Sobo <nathan@warp.dev>"]
default-run = "zed-server"
edition = "2018"
name = "zed-server"
version = "0.1.0"
[[bin]]
name = "zed-server"
[[bin]]
name = "seed"
required-features = ["seed-support"]
[dependencies]
anyhow = "1.0.40"
async-std = { version = "1.8.0", features = ["attributes"] }
async-trait = "0.1.50"
async-tungstenite = "0.14"
base64 = "0.13"
clap = "=3.0.0-beta.2"
comrak = "0.10"
either = "1.6"
envy = "0.4.2"
futures = "0.3"
handlebars = "3.5"
http-auth-basic = "0.1.3"
jwt-simple = "0.10.0"
lipsum = { version = "0.8", optional = true }
oauth2 = { version = "4.0.0", default_features = false }
oauth2-surf = "0.1.1"
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8"
rust-embed = { version = "6.2", features = ["include-exclude"] }
scrypt = "0.7"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.9"
surf = "2.2.0"
tide = "0.16.0"
tide-compress = "0.9.0"
time = "0.2"
toml = "0.5.8"
zrpc = { path = "../zrpc" }
[dependencies.async-sqlx-session]
version = "0.3.0"
features = ["pg", "rustls"]
default-features = false
[dependencies.sqlx]
version = "0.5.2"
features = ["runtime-async-std-rustls", "postgres", "time", "uuid"]
[dev-dependencies]
gpui = { path = "../gpui" }
lazy_static = "1.4"
serde_json = { version = "1.0.64", features = ["preserve_order"] }
zed = { path = "../zed", features = ["test-support"] }
[features]
seed-support = ["lipsum"]

2
crates/server/Procfile Normal file
View file

@ -0,0 +1,2 @@
web: ./target/release/zed-server
release: ./target/release/sqlx migrate run

Some files were not shown because too many files have changed in this diff Show more