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
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user