Move all crates to a top-level crates folder
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
co-authored by
Max Brunsfeld
parent
d768224182
commit
fdfed3d7db
@@ -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"
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "clock"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
zrpc = { path = "../zrpc" }
|
||||
@@ -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, "}}")
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "fuzzy"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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γdδ", "αβγδ/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γdδ", 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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/build
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 (_) {}
|
||||
@@ -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());
|
||||
*/
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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))))))
|
||||
@@ -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), ")"),
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "tree-sitter-context-predicate",
|
||||
"main": "bindings/node",
|
||||
"devDependencies": {
|
||||
"tree-sitter-cli": "^0.19.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"nan": "^2.14.0"
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()})
|
||||
}
|
||||
}
|
||||
@@ -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)})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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>>()
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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()})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#include <dispatch/dispatch.h>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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.);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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]
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
use ctor::ctor;
|
||||
|
||||
#[ctor]
|
||||
fn init_logger() {
|
||||
env_logger::builder()
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.init();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod select;
|
||||
|
||||
pub use select::{ItemType, Select, SelectStyle};
|
||||
|
||||
pub fn init(cx: &mut super::MutableAppContext) {
|
||||
select::init(cx);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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" }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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 = """\
|
||||
"""
|
||||
@@ -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-----
|
||||
"""
|
||||
@@ -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"]
|
||||
@@ -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
Reference in New Issue
Block a user