Remove 2 suffix from gpui

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-03 12:59:39 -08:00
parent 3c81dda8e2
commit f5ba22659b
225 changed files with 8511 additions and 41063 deletions

242
crates/gpui/src/action.rs Normal file
View file

@ -0,0 +1,242 @@
use crate::SharedString;
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
pub use no_action::NoAction;
use serde_json::json;
use std::any::{Any, TypeId};
/// Actions are used to implement keyboard-driven UI.
/// When you declare an action, you can bind keys to the action in the keymap and
/// listeners for that action in the element tree.
///
/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct
/// action for each listed action name in the given namespace.
/// ```rust
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
/// ```
/// More complex data types can also be actions, providing they implement Clone, PartialEq,
/// and serde_derive::Deserialize.
/// Use `impl_actions!` to automatically implement the action in the given namespace.
/// ```
/// #[derive(Clone, PartialEq, serde_derive::Deserialize)]
/// pub struct SelectNext {
/// pub replace_newest: bool,
/// }
/// impl_actions!(editor, [SelectNext]);
/// ```
///
/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
/// macro, which only generates the code needed to register your action before `main`.
///
/// ```
/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
/// pub struct Paste {
/// pub content: SharedString,
/// }
///
/// impl gpui::Action for Paste {
/// ///...
/// }
/// register_action!(Paste);
/// ```
pub trait Action: 'static {
fn boxed_clone(&self) -> Box<dyn Action>;
fn as_any(&self) -> &dyn Any;
fn partial_eq(&self, action: &dyn Action) -> bool;
fn name(&self) -> &str;
fn debug_name() -> &'static str
where
Self: Sized;
fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
where
Self: Sized;
}
impl std::fmt::Debug for dyn Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Action")
.field("name", &self.name())
.finish()
}
}
impl dyn Action {
pub fn type_id(&self) -> TypeId {
self.as_any().type_id()
}
}
type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
pub(crate) struct ActionRegistry {
builders_by_name: HashMap<SharedString, ActionBuilder>,
names_by_type_id: HashMap<TypeId, SharedString>,
all_names: Vec<SharedString>, // So we can return a static slice.
}
impl Default for ActionRegistry {
fn default() -> Self {
let mut this = ActionRegistry {
builders_by_name: Default::default(),
names_by_type_id: Default::default(),
all_names: Default::default(),
};
this.load_actions();
this
}
}
/// This type must be public so that our macros can build it in other crates.
/// But this is an implementation detail and should not be used directly.
#[doc(hidden)]
pub type MacroActionBuilder = fn() -> ActionData;
/// This type must be public so that our macros can build it in other crates.
/// But this is an implementation detail and should not be used directly.
#[doc(hidden)]
pub struct ActionData {
pub name: &'static str,
pub type_id: TypeId,
pub build: ActionBuilder,
}
/// This constant must be public to be accessible from other crates.
/// But it's existence is an implementation detail and should not be used directly.
#[doc(hidden)]
#[linkme::distributed_slice]
pub static __GPUI_ACTIONS: [MacroActionBuilder];
impl ActionRegistry {
/// Load all registered actions into the registry.
pub(crate) fn load_actions(&mut self) {
for builder in __GPUI_ACTIONS {
let action = builder();
//todo(remove)
let name: SharedString = action.name.into();
self.builders_by_name.insert(name.clone(), action.build);
self.names_by_type_id.insert(action.type_id, name.clone());
self.all_names.push(name);
}
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
let name = self
.names_by_type_id
.get(type_id)
.ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
.clone();
self.build_action(&name, None)
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
pub fn build_action(
&self,
name: &str,
params: Option<serde_json::Value>,
) -> Result<Box<dyn Action>> {
let build_action = self
.builders_by_name
.get(name)
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
(build_action)(params.unwrap_or_else(|| json!({})))
.with_context(|| format!("Attempting to build action {}", name))
}
pub fn all_action_names(&self) -> &[SharedString] {
self.all_names.as_slice()
}
}
/// Defines unit structs that can be used as actions.
/// To use more complex data types as actions, use `impl_actions!`
#[macro_export]
macro_rules! actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize)]
#[serde(crate = "gpui::serde")]
pub struct $name;
gpui::__impl_action!($namespace, $name,
fn build(_: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(Box::new(Self))
}
);
gpui::register_action!($name);
)*
};
}
/// Implements the Action trait for any struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize
#[macro_export]
macro_rules! impl_actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
gpui::__impl_action!($namespace, $name,
fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
}
);
gpui::register_action!($name);
)*
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __impl_action {
($namespace:path, $name:ident, $build:item) => {
impl gpui::Action for $name {
fn name(&self) -> &'static str
{
concat!(
stringify!($namespace),
"::",
stringify!($name),
)
}
// todo!() why is this needed in addition to name?
fn debug_name() -> &'static str
where
Self: ::std::marker::Sized
{
concat!(
stringify!($namespace),
"::",
stringify!($name),
)
}
$build
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
action
.as_any()
.downcast_ref::<Self>()
.map_or(false, |a| self == a)
}
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
::std::boxed::Box::new(self.clone())
}
fn as_any(&self) -> &dyn ::std::any::Any {
self
}
}
};
}
mod no_action {
use crate as gpui;
actions!(zed, [NoAction]);
}

File diff suppressed because it is too large Load diff

View file

@ -1,120 +0,0 @@
use std::any::{Any, TypeId};
use crate::TypeTag;
pub trait Action: 'static {
fn id(&self) -> TypeId;
fn namespace(&self) -> &'static str;
fn name(&self) -> &'static str;
fn as_any(&self) -> &dyn Any;
fn type_tag(&self) -> TypeTag;
fn boxed_clone(&self) -> Box<dyn Action>;
fn eq(&self, other: &dyn Action) -> bool;
fn qualified_name() -> &'static str
where
Self: Sized;
fn from_json_str(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
where
Self: Sized;
}
impl std::fmt::Debug for dyn Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Action")
.field("namespace", &self.namespace())
.field("name", &self.name())
.finish()
}
}
/// Define a set of unit struct types that all implement the `Action` trait.
///
/// The first argument is a namespace that will be associated with each of
/// the given action types, to ensure that they have globally unique
/// qualified names for use in keymap files.
#[macro_export]
macro_rules! actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct $name;
$crate::__impl_action! {
$namespace,
$name,
fn from_json_str(_: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
Ok(Box::new(Self))
}
}
)*
};
}
/// Implement the `Action` trait for a set of existing types.
///
/// The first argument is a namespace that will be associated with each of
/// the given action types, to ensure that they have globally unique
/// qualified names for use in keymap files.
#[macro_export]
macro_rules! impl_actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
$crate::__impl_action! {
$namespace,
$name,
fn from_json_str(json: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
Ok(Box::new($crate::serde_json::from_value::<Self>(json)?))
}
}
)*
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __impl_action {
($namespace:path, $name:ident, $from_json_fn:item) => {
impl $crate::action::Action for $name {
fn namespace(&self) -> &'static str {
stringify!($namespace)
}
fn name(&self) -> &'static str {
stringify!($name)
}
fn qualified_name() -> &'static str {
concat!(
stringify!($namespace),
"::",
stringify!($name),
)
}
fn id(&self) -> std::any::TypeId {
std::any::TypeId::of::<$name>()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn boxed_clone(&self) -> Box<dyn $crate::Action> {
Box::new(self.clone())
}
fn eq(&self, other: &dyn $crate::Action) -> bool {
if let Some(other) = other.as_any().downcast_ref::<Self>() {
self == other
} else {
false
}
}
fn type_tag(&self) -> $crate::TypeTag {
$crate::TypeTag::new::<Self>()
}
$from_json_fn
}
};
}

View file

@ -0,0 +1,330 @@
use crate::{
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View,
ViewContext, VisualContext, WindowContext, WindowHandle,
};
use anyhow::{anyhow, Context as _};
use derive_more::{Deref, DerefMut};
use std::{future::Future, rc::Weak};
#[derive(Clone)]
pub struct AsyncAppContext {
pub(crate) app: Weak<AppCell>,
pub(crate) background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
}
impl Context for AsyncAppContext {
type Result<T> = Result<T>;
fn new_model<T: 'static>(
&mut self,
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
) -> Self::Result<Model<T>>
where
T: 'static,
{
let app = self
.app
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let mut app = app.borrow_mut();
Ok(app.new_model(build_model))
}
fn update_model<T: 'static, R>(
&mut self,
handle: &Model<T>,
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> Self::Result<R> {
let app = self
.app
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let mut app = app.borrow_mut();
Ok(app.update_model(handle, update))
}
fn read_model<T, R>(
&self,
handle: &Model<T>,
callback: impl FnOnce(&T, &AppContext) -> R,
) -> Self::Result<R>
where
T: 'static,
{
let app = self.app.upgrade().context("app was released")?;
let lock = app.borrow();
Ok(lock.read_model(handle, callback))
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
{
let app = self.app.upgrade().context("app was released")?;
let mut lock = app.borrow_mut();
lock.update_window(window, f)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static,
{
let app = self.app.upgrade().context("app was released")?;
let lock = app.borrow();
lock.read_window(window, read)
}
}
impl AsyncAppContext {
pub fn refresh(&mut self) -> Result<()> {
let app = self
.app
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let mut lock = app.borrow_mut();
lock.refresh();
Ok(())
}
pub fn background_executor(&self) -> &BackgroundExecutor {
&self.background_executor
}
pub fn foreground_executor(&self) -> &ForegroundExecutor {
&self.foreground_executor
}
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result<R> {
let app = self
.app
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let mut lock = app.borrow_mut();
Ok(f(&mut lock))
}
pub fn open_window<V>(
&self,
options: crate::WindowOptions,
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
) -> Result<WindowHandle<V>>
where
V: 'static + Render,
{
let app = self
.app
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let mut lock = app.borrow_mut();
Ok(lock.open_window(options, build_root_view))
}
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
where
Fut: Future<Output = R> + 'static,
R: 'static,
{
self.foreground_executor.spawn(f(self.clone()))
}
pub fn has_global<G: 'static>(&self) -> Result<bool> {
let app = self
.app
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let app = app.borrow_mut();
Ok(app.has_global::<G>())
}
pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result<R> {
let app = self
.app
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let app = app.borrow_mut();
Ok(read(app.global(), &app))
}
pub fn try_read_global<G: 'static, R>(
&self,
read: impl FnOnce(&G, &AppContext) -> R,
) -> Option<R> {
let app = self.app.upgrade()?;
let app = app.borrow_mut();
Some(read(app.try_global()?, &app))
}
pub fn update_global<G: 'static, R>(
&mut self,
update: impl FnOnce(&mut G, &mut AppContext) -> R,
) -> Result<R> {
let app = self
.app
.upgrade()
.ok_or_else(|| anyhow!("app was released"))?;
let mut app = app.borrow_mut();
Ok(app.update_global(update))
}
}
#[derive(Clone, Deref, DerefMut)]
pub struct AsyncWindowContext {
#[deref]
#[deref_mut]
app: AsyncAppContext,
window: AnyWindowHandle,
}
impl AsyncWindowContext {
pub fn window_handle(&self) -> AnyWindowHandle {
self.window
}
pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
Self { app, window }
}
pub fn update<R>(
&mut self,
update: impl FnOnce(AnyView, &mut WindowContext) -> R,
) -> Result<R> {
self.app.update_window(self.window, update)
}
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) {
self.window.update(self, |_, cx| cx.on_next_frame(f)).ok();
}
pub fn read_global<G: 'static, R>(
&mut self,
read: impl FnOnce(&G, &WindowContext) -> R,
) -> Result<R> {
self.window.update(self, |_, cx| read(cx.global(), cx))
}
pub fn update_global<G, R>(
&mut self,
update: impl FnOnce(&mut G, &mut WindowContext) -> R,
) -> Result<R>
where
G: 'static,
{
self.window.update(self, |_, cx| cx.update_global(update))
}
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task<R>
where
Fut: Future<Output = R> + 'static,
R: 'static,
{
self.foreground_executor.spawn(f(self.clone()))
}
}
impl Context for AsyncWindowContext {
type Result<T> = Result<T>;
fn new_model<T>(
&mut self,
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
) -> Result<Model<T>>
where
T: 'static,
{
self.window.update(self, |_, cx| cx.new_model(build_model))
}
fn update_model<T: 'static, R>(
&mut self,
handle: &Model<T>,
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> Result<R> {
self.window
.update(self, |_, cx| cx.update_model(handle, update))
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<T>
where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
{
self.app.update_window(window, update)
}
fn read_model<T, R>(
&self,
handle: &Model<T>,
read: impl FnOnce(&T, &AppContext) -> R,
) -> Self::Result<R>
where
T: 'static,
{
self.app.read_model(handle, read)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static,
{
self.app.read_window(window, read)
}
}
impl VisualContext for AsyncWindowContext {
fn new_view<V>(
&mut self,
build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
V: 'static + Render,
{
self.window
.update(self, |_, cx| cx.new_view(build_view_state))
}
fn update_view<V: 'static, R>(
&mut self,
view: &View<V>,
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
) -> Self::Result<R> {
self.window
.update(self, |_, cx| cx.update_view(view, update))
}
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
V: 'static + Render,
{
self.window
.update(self, |_, cx| cx.replace_root_view(build_view))
}
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: FocusableView,
{
self.window.update(self, |_, cx| {
view.read(cx).focus_handle(cx).clone().focus(cx);
})
}
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: crate::ManagedView,
{
self.window
.update(self, |_, cx| view.update(cx, |_, cx| cx.emit(DismissEvent)))
}
}

View file

@ -1,164 +0,0 @@
use collections::{BTreeMap, HashMap, HashSet};
use parking_lot::Mutex;
use std::sync::Arc;
use std::{hash::Hash, sync::Weak};
pub struct CallbackCollection<K: Clone + Hash + Eq, F> {
internal: Arc<Mutex<Mapping<K, F>>>,
}
pub struct Subscription<K: Clone + Hash + Eq, F> {
key: K,
id: usize,
mapping: Option<Weak<Mutex<Mapping<K, F>>>>,
}
struct Mapping<K, F> {
callbacks: HashMap<K, BTreeMap<usize, F>>,
dropped_subscriptions: HashMap<K, HashSet<usize>>,
}
impl<K: Hash + Eq, F> Mapping<K, F> {
fn clear_dropped_state(&mut self, key: &K, subscription_id: usize) -> bool {
if let Some(subscriptions) = self.dropped_subscriptions.get_mut(&key) {
subscriptions.remove(&subscription_id)
} else {
false
}
}
}
impl<K, F> Default for Mapping<K, F> {
fn default() -> Self {
Self {
callbacks: Default::default(),
dropped_subscriptions: Default::default(),
}
}
}
impl<K: Clone + Hash + Eq, F> Clone for CallbackCollection<K, F> {
fn clone(&self) -> Self {
Self {
internal: self.internal.clone(),
}
}
}
impl<K: Clone + Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
fn default() -> Self {
CallbackCollection {
internal: Arc::new(Mutex::new(Default::default())),
}
}
}
impl<K: Clone + Hash + Eq + Copy, F> CallbackCollection<K, F> {
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.internal.lock().callbacks.is_empty()
}
pub fn subscribe(&mut self, key: K, subscription_id: usize) -> Subscription<K, F> {
Subscription {
key,
id: subscription_id,
mapping: Some(Arc::downgrade(&self.internal)),
}
}
pub fn add_callback(&mut self, key: K, subscription_id: usize, callback: F) {
let mut this = self.internal.lock();
// If this callback's subscription was dropped before the callback was
// added, then just drop the callback.
if this.clear_dropped_state(&key, subscription_id) {
return;
}
this.callbacks
.entry(key)
.or_default()
.insert(subscription_id, callback);
}
pub fn remove(&mut self, key: K) {
// Drop these callbacks after releasing the lock, in case one of them
// owns a subscription to this callback collection.
let mut this = self.internal.lock();
let callbacks = this.callbacks.remove(&key);
this.dropped_subscriptions.remove(&key);
drop(this);
drop(callbacks);
}
pub fn emit<C>(&mut self, key: K, mut call_callback: C)
where
C: FnMut(&mut F) -> bool,
{
let callbacks = self.internal.lock().callbacks.remove(&key);
if let Some(callbacks) = callbacks {
for (subscription_id, mut callback) in callbacks {
// If this callback's subscription was dropped while invoking an
// earlier callback, then just drop the callback.
let mut this = self.internal.lock();
if this.clear_dropped_state(&key, subscription_id) {
continue;
}
drop(this);
let alive = call_callback(&mut callback);
// If this callback's subscription was dropped while invoking the callback
// itself, or if the callback returns false, then just drop the callback.
let mut this = self.internal.lock();
if this.clear_dropped_state(&key, subscription_id) || !alive {
continue;
}
this.callbacks
.entry(key)
.or_default()
.insert(subscription_id, callback);
}
}
}
}
impl<K: Clone + Hash + Eq, F> Subscription<K, F> {
pub fn id(&self) -> usize {
self.id
}
pub fn detach(&mut self) {
self.mapping.take();
}
}
impl<K: Clone + Hash + Eq, F> Drop for Subscription<K, F> {
fn drop(&mut self) {
if let Some(mapping) = self.mapping.as_ref().and_then(|mapping| mapping.upgrade()) {
let mut mapping = mapping.lock();
// If the callback is present in the mapping, then just remove it.
if let Some(callbacks) = mapping.callbacks.get_mut(&self.key) {
let callback = callbacks.remove(&self.id);
if callback.is_some() {
drop(mapping);
drop(callback);
return;
}
}
// If this subscription's callback is not present, then either it has been
// temporarily removed during emit, or it has not yet been added. Record
// that this subscription has been dropped so that the callback can be
// removed later.
mapping
.dropped_subscriptions
.entry(self.key.clone())
.or_default()
.insert(self.id);
}
}
}

View file

@ -0,0 +1,592 @@
use crate::{private::Sealed, AppContext, Context, Entity, ModelContext};
use anyhow::{anyhow, Result};
use derive_more::{Deref, DerefMut};
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use slotmap::{SecondaryMap, SlotMap};
use std::{
any::{type_name, Any, TypeId},
fmt::{self, Display},
hash::{Hash, Hasher},
marker::PhantomData,
mem,
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc, Weak,
},
thread::panicking,
};
slotmap::new_key_type! { pub struct EntityId; }
impl EntityId {
pub fn as_u64(self) -> u64 {
self.0.as_ffi()
}
}
impl Display for EntityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_u64())
}
}
pub(crate) struct EntityMap {
entities: SecondaryMap<EntityId, Box<dyn Any>>,
ref_counts: Arc<RwLock<EntityRefCounts>>,
}
struct EntityRefCounts {
counts: SlotMap<EntityId, AtomicUsize>,
dropped_entity_ids: Vec<EntityId>,
}
impl EntityMap {
pub fn new() -> Self {
Self {
entities: SecondaryMap::new(),
ref_counts: Arc::new(RwLock::new(EntityRefCounts {
counts: SlotMap::with_key(),
dropped_entity_ids: Vec::new(),
})),
}
}
/// Reserve a slot for an entity, which you can subsequently use with `insert`.
pub fn reserve<T: 'static>(&self) -> Slot<T> {
let id = self.ref_counts.write().counts.insert(1.into());
Slot(Model::new(id, Arc::downgrade(&self.ref_counts)))
}
/// Insert an entity into a slot obtained by calling `reserve`.
pub fn insert<T>(&mut self, slot: Slot<T>, entity: T) -> Model<T>
where
T: 'static,
{
let model = slot.0;
self.entities.insert(model.entity_id, Box::new(entity));
model
}
/// Move an entity to the stack.
#[track_caller]
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
self.assert_valid_context(model);
let entity = Some(self.entities.remove(model.entity_id).unwrap_or_else(|| {
panic!(
"Circular entity lease of {}. Is it already being updated?",
std::any::type_name::<T>()
)
}));
Lease {
model,
entity,
entity_type: PhantomData,
}
}
/// Return an entity after moving it to the stack.
pub fn end_lease<T>(&mut self, mut lease: Lease<T>) {
self.entities
.insert(lease.model.entity_id, lease.entity.take().unwrap());
}
pub fn read<T: 'static>(&self, model: &Model<T>) -> &T {
self.assert_valid_context(model);
self.entities[model.entity_id].downcast_ref().unwrap()
}
fn assert_valid_context(&self, model: &AnyModel) {
debug_assert!(
Weak::ptr_eq(&model.entity_map, &Arc::downgrade(&self.ref_counts)),
"used a model with the wrong context"
);
}
pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
let mut ref_counts = self.ref_counts.write();
let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
dropped_entity_ids
.into_iter()
.map(|entity_id| {
let count = ref_counts.counts.remove(entity_id).unwrap();
debug_assert_eq!(
count.load(SeqCst),
0,
"dropped an entity that was referenced"
);
(entity_id, self.entities.remove(entity_id).unwrap())
})
.collect()
}
}
pub struct Lease<'a, T> {
entity: Option<Box<dyn Any>>,
pub model: &'a Model<T>,
entity_type: PhantomData<T>,
}
impl<'a, T: 'static> core::ops::Deref for Lease<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.entity.as_ref().unwrap().downcast_ref().unwrap()
}
}
impl<'a, T: 'static> core::ops::DerefMut for Lease<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.entity.as_mut().unwrap().downcast_mut().unwrap()
}
}
impl<'a, T> Drop for Lease<'a, T> {
fn drop(&mut self) {
if self.entity.is_some() && !panicking() {
panic!("Leases must be ended with EntityMap::end_lease")
}
}
}
#[derive(Deref, DerefMut)]
pub struct Slot<T>(Model<T>);
pub struct AnyModel {
pub(crate) entity_id: EntityId,
pub(crate) entity_type: TypeId,
entity_map: Weak<RwLock<EntityRefCounts>>,
}
impl AnyModel {
fn new(id: EntityId, entity_type: TypeId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self {
Self {
entity_id: id,
entity_type,
entity_map,
}
}
pub fn entity_id(&self) -> EntityId {
self.entity_id
}
pub fn entity_type(&self) -> TypeId {
self.entity_type
}
pub fn downgrade(&self) -> AnyWeakModel {
AnyWeakModel {
entity_id: self.entity_id,
entity_type: self.entity_type,
entity_ref_counts: self.entity_map.clone(),
}
}
pub fn downcast<T: 'static>(self) -> Result<Model<T>, AnyModel> {
if TypeId::of::<T>() == self.entity_type {
Ok(Model {
any_model: self,
entity_type: PhantomData,
})
} else {
Err(self)
}
}
}
impl Clone for AnyModel {
fn clone(&self) -> Self {
if let Some(entity_map) = self.entity_map.upgrade() {
let entity_map = entity_map.read();
let count = entity_map
.counts
.get(self.entity_id)
.expect("detected over-release of a model");
let prev_count = count.fetch_add(1, SeqCst);
assert_ne!(prev_count, 0, "Detected over-release of a model.");
}
Self {
entity_id: self.entity_id,
entity_type: self.entity_type,
entity_map: self.entity_map.clone(),
}
}
}
impl Drop for AnyModel {
fn drop(&mut self) {
if let Some(entity_map) = self.entity_map.upgrade() {
let entity_map = entity_map.upgradable_read();
let count = entity_map
.counts
.get(self.entity_id)
.expect("detected over-release of a handle.");
let prev_count = count.fetch_sub(1, SeqCst);
assert_ne!(prev_count, 0, "Detected over-release of a model.");
if prev_count == 1 {
// We were the last reference to this entity, so we can remove it.
let mut entity_map = RwLockUpgradableReadGuard::upgrade(entity_map);
entity_map.dropped_entity_ids.push(self.entity_id);
}
}
}
}
impl<T> From<Model<T>> for AnyModel {
fn from(model: Model<T>) -> Self {
model.any_model
}
}
impl Hash for AnyModel {
fn hash<H: Hasher>(&self, state: &mut H) {
self.entity_id.hash(state);
}
}
impl PartialEq for AnyModel {
fn eq(&self, other: &Self) -> bool {
self.entity_id == other.entity_id
}
}
impl Eq for AnyModel {}
impl std::fmt::Debug for AnyModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AnyModel")
.field("entity_id", &self.entity_id.as_u64())
.finish()
}
}
#[derive(Deref, DerefMut)]
pub struct Model<T> {
#[deref]
#[deref_mut]
pub(crate) any_model: AnyModel,
pub(crate) entity_type: PhantomData<T>,
}
unsafe impl<T> Send for Model<T> {}
unsafe impl<T> Sync for Model<T> {}
impl<T> Sealed for Model<T> {}
impl<T: 'static> Entity<T> for Model<T> {
type Weak = WeakModel<T>;
fn entity_id(&self) -> EntityId {
self.any_model.entity_id
}
fn downgrade(&self) -> Self::Weak {
WeakModel {
any_model: self.any_model.downgrade(),
entity_type: self.entity_type,
}
}
fn upgrade_from(weak: &Self::Weak) -> Option<Self>
where
Self: Sized,
{
Some(Model {
any_model: weak.any_model.upgrade()?,
entity_type: weak.entity_type,
})
}
}
impl<T: 'static> Model<T> {
fn new(id: EntityId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self
where
T: 'static,
{
Self {
any_model: AnyModel::new(id, TypeId::of::<T>(), entity_map),
entity_type: PhantomData,
}
}
/// Downgrade the this to a weak model reference
pub fn downgrade(&self) -> WeakModel<T> {
// Delegate to the trait implementation to keep behavior in one place.
// This method was included to improve method resolution in the presence of
// the Model's deref
Entity::downgrade(self)
}
/// Convert this into a dynamically typed model.
pub fn into_any(self) -> AnyModel {
self.any_model
}
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
cx.entities.read(self)
}
pub fn read_with<R, C: Context>(
&self,
cx: &C,
f: impl FnOnce(&T, &AppContext) -> R,
) -> C::Result<R> {
cx.read_model(self, f)
}
/// Update the entity referenced by this model with the given function.
///
/// The update function receives a context appropriate for its environment.
/// When updating in an `AppContext`, it receives a `ModelContext`.
/// When updating an a `WindowContext`, it receives a `ViewContext`.
pub fn update<C, R>(
&self,
cx: &mut C,
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> C::Result<R>
where
C: Context,
{
cx.update_model(self, update)
}
}
impl<T> Clone for Model<T> {
fn clone(&self) -> Self {
Self {
any_model: self.any_model.clone(),
entity_type: self.entity_type,
}
}
}
impl<T> std::fmt::Debug for Model<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Model {{ entity_id: {:?}, entity_type: {:?} }}",
self.any_model.entity_id,
type_name::<T>()
)
}
}
impl<T> Hash for Model<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.any_model.hash(state);
}
}
impl<T> PartialEq for Model<T> {
fn eq(&self, other: &Self) -> bool {
self.any_model == other.any_model
}
}
impl<T> Eq for Model<T> {}
impl<T> PartialEq<WeakModel<T>> for Model<T> {
fn eq(&self, other: &WeakModel<T>) -> bool {
self.any_model.entity_id() == other.entity_id()
}
}
#[derive(Clone)]
pub struct AnyWeakModel {
pub(crate) entity_id: EntityId,
entity_type: TypeId,
entity_ref_counts: Weak<RwLock<EntityRefCounts>>,
}
impl AnyWeakModel {
pub fn entity_id(&self) -> EntityId {
self.entity_id
}
pub fn is_upgradable(&self) -> bool {
let ref_count = self
.entity_ref_counts
.upgrade()
.and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst)))
.unwrap_or(0);
ref_count > 0
}
pub fn upgrade(&self) -> Option<AnyModel> {
let ref_counts = &self.entity_ref_counts.upgrade()?;
let ref_counts = ref_counts.read();
let ref_count = ref_counts.counts.get(self.entity_id)?;
// entity_id is in dropped_entity_ids
if ref_count.load(SeqCst) == 0 {
return None;
}
ref_count.fetch_add(1, SeqCst);
Some(AnyModel {
entity_id: self.entity_id,
entity_type: self.entity_type,
entity_map: self.entity_ref_counts.clone(),
})
}
}
impl<T> From<WeakModel<T>> for AnyWeakModel {
fn from(model: WeakModel<T>) -> Self {
model.any_model
}
}
impl Hash for AnyWeakModel {
fn hash<H: Hasher>(&self, state: &mut H) {
self.entity_id.hash(state);
}
}
impl PartialEq for AnyWeakModel {
fn eq(&self, other: &Self) -> bool {
self.entity_id == other.entity_id
}
}
impl Eq for AnyWeakModel {}
#[derive(Deref, DerefMut)]
pub struct WeakModel<T> {
#[deref]
#[deref_mut]
any_model: AnyWeakModel,
entity_type: PhantomData<T>,
}
unsafe impl<T> Send for WeakModel<T> {}
unsafe impl<T> Sync for WeakModel<T> {}
impl<T> Clone for WeakModel<T> {
fn clone(&self) -> Self {
Self {
any_model: self.any_model.clone(),
entity_type: self.entity_type,
}
}
}
impl<T: 'static> WeakModel<T> {
/// Upgrade this weak model reference into a strong model reference
pub fn upgrade(&self) -> Option<Model<T>> {
// Delegate to the trait implementation to keep behavior in one place.
Model::upgrade_from(self)
}
/// Update the entity referenced by this model with the given function if
/// the referenced entity still exists. Returns an error if the entity has
/// been released.
pub fn update<C, R>(
&self,
cx: &mut C,
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> Result<R>
where
C: Context,
Result<C::Result<R>>: crate::Flatten<R>,
{
crate::Flatten::flatten(
self.upgrade()
.ok_or_else(|| anyhow!("entity release"))
.map(|this| cx.update_model(&this, update)),
)
}
/// Reads the entity referenced by this model with the given function if
/// the referenced entity still exists. Returns an error if the entity has
/// been released.
pub fn read_with<C, R>(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result<R>
where
C: Context,
Result<C::Result<R>>: crate::Flatten<R>,
{
crate::Flatten::flatten(
self.upgrade()
.ok_or_else(|| anyhow!("entity release"))
.map(|this| cx.read_model(&this, read)),
)
}
}
impl<T> Hash for WeakModel<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.any_model.hash(state);
}
}
impl<T> PartialEq for WeakModel<T> {
fn eq(&self, other: &Self) -> bool {
self.any_model == other.any_model
}
}
impl<T> Eq for WeakModel<T> {}
impl<T> PartialEq<Model<T>> for WeakModel<T> {
fn eq(&self, other: &Model<T>) -> bool {
self.entity_id() == other.any_model.entity_id()
}
}
#[cfg(test)]
mod test {
use crate::EntityMap;
struct TestEntity {
pub i: i32,
}
#[test]
fn test_entity_map_slot_assignment_before_cleanup() {
// Tests that slots are not re-used before take_dropped.
let mut entity_map = EntityMap::new();
let slot = entity_map.reserve::<TestEntity>();
entity_map.insert(slot, TestEntity { i: 1 });
let slot = entity_map.reserve::<TestEntity>();
entity_map.insert(slot, TestEntity { i: 2 });
let dropped = entity_map.take_dropped();
assert_eq!(dropped.len(), 2);
assert_eq!(
dropped
.into_iter()
.map(|(_, entity)| entity.downcast::<TestEntity>().unwrap().i)
.collect::<Vec<i32>>(),
vec![1, 2],
);
}
#[test]
fn test_entity_map_weak_upgrade_before_cleanup() {
// Tests that weak handles are not upgraded before take_dropped
let mut entity_map = EntityMap::new();
let slot = entity_map.reserve::<TestEntity>();
let handle = entity_map.insert(slot, TestEntity { i: 1 });
let weak = handle.downgrade();
drop(handle);
let strong = weak.upgrade();
assert_eq!(strong, None);
let dropped = entity_map.take_dropped();
assert_eq!(dropped.len(), 1);
assert_eq!(
dropped
.into_iter()
.map(|(_, entity)| entity.downcast::<TestEntity>().unwrap().i)
.collect::<Vec<i32>>(),
vec![1],
);
}
}

View file

@ -1,99 +0,0 @@
use crate::{platform::ForegroundPlatform, Action, App, AppContext};
pub struct Menu<'a> {
pub name: &'a str,
pub items: Vec<MenuItem<'a>>,
}
pub enum MenuItem<'a> {
Separator,
Submenu(Menu<'a>),
Action {
name: &'a str,
action: Box<dyn Action>,
os_action: Option<OsAction>,
},
}
impl<'a> MenuItem<'a> {
pub fn separator() -> Self {
Self::Separator
}
pub fn submenu(menu: Menu<'a>) -> Self {
Self::Submenu(menu)
}
pub fn action(name: &'a str, action: impl Action) -> Self {
Self::Action {
name,
action: Box::new(action),
os_action: None,
}
}
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
Self::Action {
name,
action: Box::new(action),
os_action: Some(os_action),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum OsAction {
Cut,
Copy,
Paste,
SelectAll,
Undo,
Redo,
}
impl AppContext {
pub fn set_menus(&mut self, menus: Vec<Menu>) {
self.foreground_platform
.set_menus(menus, &self.keystroke_matcher);
}
}
pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform, app: &App) {
foreground_platform.on_will_open_menu(Box::new({
let cx = app.0.clone();
move || {
let mut cx = cx.borrow_mut();
cx.keystroke_matcher.clear_pending();
}
}));
foreground_platform.on_validate_menu_command(Box::new({
let cx = app.0.clone();
move |action| {
let cx = cx.borrow_mut();
!cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action)
}
}));
foreground_platform.on_menu_command(Box::new({
let cx = app.0.clone();
move |action| {
let mut cx = cx.borrow_mut();
if let Some(main_window) = cx.active_window() {
let dispatched = main_window
.update(&mut *cx, |cx| {
if let Some(view_id) = cx.focused_view_id() {
cx.dispatch_action(Some(view_id), action);
true
} else {
false
}
})
.unwrap_or(false);
if dispatched {
return;
}
}
cx.dispatch_global_action_any(action);
}
}));
}

View file

@ -0,0 +1,273 @@
use crate::{
AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId,
EventEmitter, Model, Subscription, Task, View, WeakModel, WindowContext, WindowHandle,
};
use anyhow::Result;
use derive_more::{Deref, DerefMut};
use futures::FutureExt;
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut},
future::Future,
};
#[derive(Deref, DerefMut)]
pub struct ModelContext<'a, T> {
#[deref]
#[deref_mut]
app: &'a mut AppContext,
model_state: WeakModel<T>,
}
impl<'a, T: 'static> ModelContext<'a, T> {
pub(crate) fn new(app: &'a mut AppContext, model_state: WeakModel<T>) -> Self {
Self { app, model_state }
}
pub fn entity_id(&self) -> EntityId {
self.model_state.entity_id
}
pub fn handle(&self) -> Model<T> {
self.weak_model()
.upgrade()
.expect("The entity must be alive if we have a model context")
}
pub fn weak_model(&self) -> WeakModel<T> {
self.model_state.clone()
}
pub fn observe<W, E>(
&mut self,
entity: &E,
mut on_notify: impl FnMut(&mut T, E, &mut ModelContext<'_, T>) + 'static,
) -> Subscription
where
T: 'static,
W: 'static,
E: Entity<W>,
{
let this = self.weak_model();
self.app.observe_internal(entity, move |e, cx| {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| on_notify(this, e, cx));
true
} else {
false
}
})
}
pub fn subscribe<T2, E, Evt>(
&mut self,
entity: &E,
mut on_event: impl FnMut(&mut T, E, &Evt, &mut ModelContext<'_, T>) + 'static,
) -> Subscription
where
T: 'static,
T2: 'static + EventEmitter<Evt>,
E: Entity<T2>,
Evt: 'static,
{
let this = self.weak_model();
self.app.subscribe_internal(entity, move |e, event, cx| {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| on_event(this, e, event, cx));
true
} else {
false
}
})
}
pub fn on_release(
&mut self,
on_release: impl FnOnce(&mut T, &mut AppContext) + 'static,
) -> Subscription
where
T: 'static,
{
let (subscription, activate) = self.app.release_listeners.insert(
self.model_state.entity_id,
Box::new(move |this, cx| {
let this = this.downcast_mut().expect("invalid entity type");
on_release(this, cx);
}),
);
activate();
subscription
}
pub fn observe_release<T2, E>(
&mut self,
entity: &E,
on_release: impl FnOnce(&mut T, &mut T2, &mut ModelContext<'_, T>) + 'static,
) -> Subscription
where
T: Any,
T2: 'static,
E: Entity<T2>,
{
let entity_id = entity.entity_id();
let this = self.weak_model();
let (subscription, activate) = self.app.release_listeners.insert(
entity_id,
Box::new(move |entity, cx| {
let entity = entity.downcast_mut().expect("invalid entity type");
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| on_release(this, entity, cx));
}
}),
);
activate();
subscription
}
pub fn observe_global<G: 'static>(
&mut self,
mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + 'static,
) -> Subscription
where
T: 'static,
{
let handle = self.weak_model();
let (subscription, activate) = self.global_observers.insert(
TypeId::of::<G>(),
Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()),
);
self.defer(move |_| activate());
subscription
}
pub fn on_app_quit<Fut>(
&mut self,
mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + 'static,
) -> Subscription
where
Fut: 'static + Future<Output = ()>,
T: 'static,
{
let handle = self.weak_model();
let (subscription, activate) = self.app.quit_observers.insert(
(),
Box::new(move |cx| {
let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
async move {
if let Some(future) = future {
future.await;
}
}
.boxed_local()
}),
);
activate();
subscription
}
pub fn notify(&mut self) {
if self
.app
.pending_notifications
.insert(self.model_state.entity_id)
{
self.app.pending_effects.push_back(Effect::Notify {
emitter: self.model_state.entity_id,
});
}
}
pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: 'static,
{
let mut global = self.app.lease_global::<G>();
let result = f(&mut global, self);
self.app.end_global_lease(global);
result
}
pub fn spawn<Fut, R>(&self, f: impl FnOnce(WeakModel<T>, AsyncAppContext) -> Fut) -> Task<R>
where
T: 'static,
Fut: Future<Output = R> + 'static,
R: 'static,
{
let this = self.weak_model();
self.app.spawn(|cx| f(this, cx))
}
}
impl<'a, T> ModelContext<'a, T> {
pub fn emit<Evt>(&mut self, event: Evt)
where
T: EventEmitter<Evt>,
Evt: 'static,
{
self.app.pending_effects.push_back(Effect::Emit {
emitter: self.model_state.entity_id,
event_type: TypeId::of::<Evt>(),
event: Box::new(event),
});
}
}
impl<'a, T> Context for ModelContext<'a, T> {
type Result<U> = U;
fn new_model<U: 'static>(
&mut self,
build_model: impl FnOnce(&mut ModelContext<'_, U>) -> U,
) -> Model<U> {
self.app.new_model(build_model)
}
fn update_model<U: 'static, R>(
&mut self,
handle: &Model<U>,
update: impl FnOnce(&mut U, &mut ModelContext<'_, U>) -> R,
) -> R {
self.app.update_model(handle, update)
}
fn update_window<R, F>(&mut self, window: AnyWindowHandle, update: F) -> Result<R>
where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> R,
{
self.app.update_window(window, update)
}
fn read_model<U, R>(
&self,
handle: &Model<U>,
read: impl FnOnce(&U, &AppContext) -> R,
) -> Self::Result<R>
where
U: 'static,
{
self.app.read_model(handle, read)
}
fn read_window<U, R>(
&self,
window: &WindowHandle<U>,
read: impl FnOnce(View<U>, &AppContext) -> R,
) -> Result<R>
where
U: 'static,
{
self.app.read_window(window, read)
}
}
impl<T> Borrow<AppContext> for ModelContext<'_, T> {
fn borrow(&self) -> &AppContext {
self.app
}
}
impl<T> BorrowMut<AppContext> for ModelContext<'_, T> {
fn borrow_mut(&mut self) -> &mut AppContext {
self.app
}
}

View file

@ -1,220 +0,0 @@
#[cfg(any(test, feature = "test-support"))]
use std::sync::Arc;
use lazy_static::lazy_static;
#[cfg(any(test, feature = "test-support"))]
use parking_lot::Mutex;
use collections::{hash_map::Entry, HashMap, HashSet};
#[cfg(any(test, feature = "test-support"))]
use crate::util::post_inc;
use crate::{AnyWindowHandle, ElementStateId};
lazy_static! {
static ref LEAK_BACKTRACE: bool =
std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty());
}
struct ElementStateRefCount {
ref_count: usize,
frame_id: usize,
}
#[derive(Default)]
pub struct RefCounts {
entity_counts: HashMap<usize, usize>,
element_state_counts: HashMap<ElementStateId, ElementStateRefCount>,
dropped_models: HashSet<usize>,
dropped_views: HashSet<(AnyWindowHandle, usize)>,
dropped_element_states: HashSet<ElementStateId>,
#[cfg(any(test, feature = "test-support"))]
pub leak_detector: Arc<Mutex<LeakDetector>>,
}
impl RefCounts {
#[cfg(any(test, feature = "test-support"))]
pub fn new(leak_detector: Arc<Mutex<LeakDetector>>) -> Self {
Self {
#[cfg(any(test, feature = "test-support"))]
leak_detector,
..Default::default()
}
}
pub fn inc_model(&mut self, model_id: usize) {
match self.entity_counts.entry(model_id) {
Entry::Occupied(mut entry) => {
*entry.get_mut() += 1;
}
Entry::Vacant(entry) => {
entry.insert(1);
self.dropped_models.remove(&model_id);
}
}
}
pub fn inc_view(&mut self, window: AnyWindowHandle, view_id: usize) {
match self.entity_counts.entry(view_id) {
Entry::Occupied(mut entry) => *entry.get_mut() += 1,
Entry::Vacant(entry) => {
entry.insert(1);
self.dropped_views.remove(&(window, view_id));
}
}
}
pub fn inc_element_state(&mut self, id: ElementStateId, frame_id: usize) {
match self.element_state_counts.entry(id) {
Entry::Occupied(mut entry) => {
let entry = entry.get_mut();
if entry.frame_id == frame_id || entry.ref_count >= 2 {
panic!("used the same element state more than once in the same frame");
}
entry.ref_count += 1;
entry.frame_id = frame_id;
}
Entry::Vacant(entry) => {
entry.insert(ElementStateRefCount {
ref_count: 1,
frame_id,
});
self.dropped_element_states.remove(&id);
}
}
}
pub fn dec_model(&mut self, model_id: usize) {
let count = self.entity_counts.get_mut(&model_id).unwrap();
*count -= 1;
if *count == 0 {
self.entity_counts.remove(&model_id);
self.dropped_models.insert(model_id);
}
}
pub fn dec_view(&mut self, window: AnyWindowHandle, view_id: usize) {
let count = self.entity_counts.get_mut(&view_id).unwrap();
*count -= 1;
if *count == 0 {
self.entity_counts.remove(&view_id);
self.dropped_views.insert((window, view_id));
}
}
pub fn dec_element_state(&mut self, id: ElementStateId) {
let entry = self.element_state_counts.get_mut(&id).unwrap();
entry.ref_count -= 1;
if entry.ref_count == 0 {
self.element_state_counts.remove(&id);
self.dropped_element_states.insert(id);
}
}
pub fn is_entity_alive(&self, entity_id: usize) -> bool {
self.entity_counts.contains_key(&entity_id)
}
pub fn take_dropped(
&mut self,
) -> (
HashSet<usize>,
HashSet<(AnyWindowHandle, usize)>,
HashSet<ElementStateId>,
) {
(
std::mem::take(&mut self.dropped_models),
std::mem::take(&mut self.dropped_views),
std::mem::take(&mut self.dropped_element_states),
)
}
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Default)]
pub struct LeakDetector {
next_handle_id: usize,
#[allow(clippy::type_complexity)]
handle_backtraces: HashMap<
usize,
(
Option<&'static str>,
HashMap<usize, Option<backtrace::Backtrace>>,
),
>,
}
#[cfg(any(test, feature = "test-support"))]
impl LeakDetector {
pub fn handle_created(&mut self, type_name: Option<&'static str>, entity_id: usize) -> usize {
let handle_id = post_inc(&mut self.next_handle_id);
let entry = self.handle_backtraces.entry(entity_id).or_default();
let backtrace = if *LEAK_BACKTRACE {
Some(backtrace::Backtrace::new_unresolved())
} else {
None
};
if let Some(type_name) = type_name {
entry.0.get_or_insert(type_name);
}
entry.1.insert(handle_id, backtrace);
handle_id
}
pub fn handle_dropped(&mut self, entity_id: usize, handle_id: usize) {
if let Some((_, backtraces)) = self.handle_backtraces.get_mut(&entity_id) {
assert!(backtraces.remove(&handle_id).is_some());
if backtraces.is_empty() {
self.handle_backtraces.remove(&entity_id);
}
}
}
pub fn assert_dropped(&mut self, entity_id: usize) {
if let Some((type_name, backtraces)) = self.handle_backtraces.get_mut(&entity_id) {
for trace in backtraces.values_mut().flatten() {
trace.resolve();
eprintln!("{:?}", crate::util::CwdBacktrace(trace));
}
let hint = if *LEAK_BACKTRACE {
""
} else {
" set LEAK_BACKTRACE=1 for more information"
};
panic!(
"{} handles to {} {} still exist{}",
backtraces.len(),
type_name.unwrap_or("entity"),
entity_id,
hint
);
}
}
pub fn detect(&mut self) {
let mut found_leaks = false;
for (id, (type_name, backtraces)) in self.handle_backtraces.iter_mut() {
eprintln!(
"leaked {} handles to {} {}",
backtraces.len(),
type_name.unwrap_or("entity"),
id
);
for trace in backtraces.values_mut().flatten() {
trace.resolve();
eprintln!("{:?}", crate::util::CwdBacktrace(trace));
}
found_leaks = true;
}
let hint = if *LEAK_BACKTRACE {
""
} else {
" set LEAK_BACKTRACE=1 for more information"
};
assert!(!found_leaks, "detected leaked handles{}", hint);
}
}

View file

@ -1,661 +0,0 @@
use crate::{
executor,
geometry::vector::Vector2F,
keymap_matcher::{Binding, Keystroke},
platform,
platform::{Event, InputHandler, KeyDownEvent, Platform},
Action, AnyWindowHandle, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache,
Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
WeakHandle, WindowContext, WindowHandle,
};
use collections::BTreeMap;
use futures::Future;
use itertools::Itertools;
use parking_lot::{Mutex, RwLock};
use smallvec::SmallVec;
use smol::stream::StreamExt;
use std::{
any::Any,
cell::RefCell,
mem,
path::PathBuf,
rc::Rc,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::Duration,
};
use super::{
ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts,
};
#[derive(Clone)]
pub struct TestAppContext {
cx: Rc<RefCell<AppContext>>,
foreground_platform: Rc<platform::test::ForegroundPlatform>,
condition_duration: Option<Duration>,
pub function_name: String,
assertion_context: AssertionContextManager,
}
impl TestAppContext {
pub fn new(
foreground_platform: Rc<platform::test::ForegroundPlatform>,
platform: Arc<dyn Platform>,
foreground: Rc<executor::Foreground>,
background: Arc<executor::Background>,
font_cache: Arc<FontCache>,
leak_detector: Arc<Mutex<LeakDetector>>,
first_entity_id: usize,
function_name: String,
) -> Self {
let mut cx = AppContext::new(
foreground,
background,
platform,
foreground_platform.clone(),
font_cache,
util::http::FakeHttpClient::with_404_response(),
RefCounts::new(leak_detector),
(),
);
cx.next_id = first_entity_id;
let cx = TestAppContext {
cx: Rc::new(RefCell::new(cx)),
foreground_platform,
condition_duration: None,
function_name,
assertion_context: AssertionContextManager::new(),
};
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
cx
}
pub fn dispatch_action<A: Action>(&mut self, window: AnyWindowHandle, action: A) {
self.update_window(window, |window| {
window.dispatch_action(window.focused_view_id(), &action);
})
.expect("window not found");
}
pub fn available_actions(
&self,
window: AnyWindowHandle,
view_id: usize,
) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
self.read_window(window, |cx| cx.available_actions(view_id))
.unwrap_or_default()
}
pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
self.update(|cx| cx.dispatch_global_action_any(&action));
}
pub fn dispatch_keystroke(
&mut self,
window: AnyWindowHandle,
keystroke: Keystroke,
is_held: bool,
) {
let handled = window.update(self, |cx| {
if cx.dispatch_keystroke(&keystroke) {
return true;
}
if cx.dispatch_event(
Event::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held,
}),
false,
) {
return true;
}
false
});
if !handled && !keystroke.cmd && !keystroke.ctrl {
WindowInputHandler {
app: self.cx.clone(),
window,
}
.replace_text_in_range(None, &keystroke.key)
}
}
pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
&self,
window: AnyWindowHandle,
callback: F,
) -> Option<T> {
self.cx.borrow().read_window(window, callback)
}
pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
&mut self,
window: AnyWindowHandle,
callback: F,
) -> Option<T> {
self.cx.borrow_mut().update_window(window, callback)
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where
T: Entity,
F: FnOnce(&mut ModelContext<T>) -> T,
{
self.cx.borrow_mut().add_model(build_model)
}
pub fn add_window<V, F>(&mut self, build_root_view: F) -> WindowHandle<V>
where
V: View,
F: FnOnce(&mut ViewContext<V>) -> V,
{
let window = self
.cx
.borrow_mut()
.add_window(Default::default(), build_root_view);
window.simulate_activation(self);
window
}
pub fn observe_global<E, F>(&mut self, callback: F) -> Subscription
where
E: Any,
F: 'static + FnMut(&mut AppContext),
{
self.cx.borrow_mut().observe_global::<E, F>(callback)
}
pub fn set_global<T: 'static>(&mut self, state: T) {
self.cx.borrow_mut().set_global(state);
}
pub fn subscribe_global<E, F>(&mut self, callback: F) -> Subscription
where
E: Any,
F: 'static + FnMut(&E, &mut AppContext),
{
self.cx.borrow_mut().subscribe_global(callback)
}
pub fn windows(&self) -> Vec<AnyWindowHandle> {
self.cx.borrow().windows().collect()
}
pub fn remove_all_windows(&mut self) {
self.update(|cx| cx.windows.clear());
}
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
callback(&*self.cx.borrow())
}
pub fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, callback: F) -> T {
let mut state = self.cx.borrow_mut();
// Don't increment pending flushes in order for effects to be flushed before the callback
// completes, which is helpful in tests.
let result = callback(&mut *state);
// Flush effects after the callback just in case there are any. This can happen in edge
// cases such as the closure dropping handles.
state.flush_effects();
result
}
pub fn to_async(&self) -> AsyncAppContext {
AsyncAppContext(self.cx.clone())
}
pub fn font_cache(&self) -> Arc<FontCache> {
self.cx.borrow().font_cache.clone()
}
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
self.foreground_platform.clone()
}
pub fn platform(&self) -> Arc<dyn platform::Platform> {
self.cx.borrow().platform.clone()
}
pub fn foreground(&self) -> Rc<executor::Foreground> {
self.cx.borrow().foreground().clone()
}
pub fn background(&self) -> Arc<executor::Background> {
self.cx.borrow().background().clone()
}
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
where
F: FnOnce(AsyncAppContext) -> Fut,
Fut: 'static + Future<Output = T>,
T: 'static,
{
let foreground = self.foreground();
let future = f(self.to_async());
let cx = self.to_async();
foreground.spawn(async move {
let result = future.await;
cx.0.borrow_mut().flush_effects();
result
})
}
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
self.foreground_platform.simulate_new_path_selection(result);
}
pub fn did_prompt_for_new_path(&self) -> bool {
self.foreground_platform.as_ref().did_prompt_for_new_path()
}
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
self.cx.borrow().leak_detector()
}
pub fn assert_dropped(&self, handle: impl WeakHandle) {
self.cx
.borrow()
.leak_detector()
.lock()
.assert_dropped(handle.id())
}
/// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about
/// where the stray handles were created.
pub fn drop_last<T, W: WeakHandle, H: Handle<T, Weak = W>>(&mut self, handle: H) {
let weak = handle.downgrade();
self.update(|_| drop(handle));
self.assert_dropped(weak);
}
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
self.condition_duration = duration;
}
pub fn condition_duration(&self) -> Duration {
self.condition_duration.unwrap_or_else(|| {
if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
}
})
}
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
self.update(|cx| {
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
let expected_content = expected_content.map(|content| content.to_owned());
assert_eq!(actual_content, expected_content);
})
}
pub fn add_assertion_context(&self, context: String) -> ContextHandle {
self.assertion_context.add_context(context)
}
pub fn assertion_context(&self) -> String {
self.assertion_context.context()
}
}
impl BorrowAppContext for TestAppContext {
fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
self.cx.borrow().read_with(f)
}
fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, f: F) -> T {
self.cx.borrow_mut().update(f)
}
}
impl BorrowWindowContext for TestAppContext {
type Result<T> = T;
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
self.cx
.borrow()
.read_window(window, f)
.expect("window was closed")
}
fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
where
F: FnOnce(&WindowContext) -> Option<T>,
{
BorrowWindowContext::read_window(self, window, f)
}
fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
&mut self,
window: AnyWindowHandle,
f: F,
) -> T {
self.cx
.borrow_mut()
.update_window(window, f)
.expect("window was closed")
}
fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
where
F: FnOnce(&mut WindowContext) -> Option<T>,
{
BorrowWindowContext::update_window(self, window, f)
}
}
impl<T: Entity> ModelHandle<T> {
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.unbounded_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
let executor = cx.background().clone();
async move {
executor.start_waiting();
let notification = crate::util::timeout(duration, rx.next())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
where
T::Event: Clone,
{
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.subscribe(self, move |_, event, _| {
tx.unbounded_send(event.clone()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
cx.foreground.start_waiting();
async move {
let event = crate::util::timeout(duration, rx.next())
.await
.expect("next event timed out");
drop(subscription);
event.expect("model dropped while test was waiting for its next event")
}
}
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscriptions = (
cx.observe(self, {
let tx = tx.clone();
move |_, _| {
tx.unbounded_send(()).ok();
}
}),
cx.subscribe(self, {
move |_, _, _| {
tx.unbounded_send(()).ok();
}
}),
);
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
crate::util::timeout(duration, async move {
loop {
{
let cx = cx.borrow();
let cx = &*cx;
if predicate(
handle
.upgrade(cx)
.expect("model dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.next()
.await
.expect("model dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
impl AnyWindowHandle {
pub fn has_pending_prompt(&self, cx: &mut TestAppContext) -> bool {
let window = self.platform_window_mut(cx);
let prompts = window.pending_prompts.borrow_mut();
!prompts.is_empty()
}
pub fn current_title(&self, cx: &mut TestAppContext) -> Option<String> {
self.platform_window_mut(cx).title.clone()
}
pub fn simulate_close(&self, cx: &mut TestAppContext) -> bool {
let handler = self.platform_window_mut(cx).should_close_handler.take();
if let Some(mut handler) = handler {
let should_close = handler();
self.platform_window_mut(cx).should_close_handler = Some(handler);
should_close
} else {
false
}
}
pub fn simulate_resize(&self, size: Vector2F, cx: &mut TestAppContext) {
let mut window = self.platform_window_mut(cx);
window.size = size;
let mut handlers = mem::take(&mut window.resize_handlers);
drop(window);
for handler in &mut handlers {
handler();
}
self.platform_window_mut(cx).resize_handlers = handlers;
}
pub fn is_edited(&self, cx: &mut TestAppContext) -> bool {
self.platform_window_mut(cx).edited
}
pub fn simulate_prompt_answer(&self, answer: usize, cx: &mut TestAppContext) {
use postage::prelude::Sink as _;
let mut done_tx = self
.platform_window_mut(cx)
.pending_prompts
.borrow_mut()
.pop_front()
.expect("prompt was not called");
done_tx.try_send(answer).ok();
}
fn platform_window_mut<'a>(
&self,
cx: &'a mut TestAppContext,
) -> std::cell::RefMut<'a, platform::test::Window> {
std::cell::RefMut::map(cx.cx.borrow_mut(), |state| {
let window = state.windows.get_mut(&self).unwrap();
let test_window = window
.platform_window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
test_window
})
}
}
impl<T: View> ViewHandle<T> {
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (mut tx, mut rx) = postage::mpsc::channel(1);
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.try_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.recv())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (tx, mut rx) = postage::mpsc::channel(1024);
let timeout_duration = cx.condition_duration();
let mut cx = cx.cx.borrow_mut();
let subscriptions = (
cx.observe(self, {
let mut tx = tx.clone();
move |_, _| {
tx.blocking_send(()).ok();
}
}),
cx.subscribe(self, {
let mut tx = tx.clone();
move |_, _, _| {
tx.blocking_send(()).ok();
}
}),
);
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
async move {
crate::util::timeout(timeout_duration, async move {
loop {
{
let cx = cx.borrow();
let cx = &*cx;
if predicate(
handle
.upgrade(cx)
.expect("view dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.recv()
.await
.expect("view dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
/// Tracks string context to be printed when assertions fail.
/// Often this is done by storing a context string in the manager and returning the handle.
#[derive(Clone)]
pub struct AssertionContextManager {
id: Arc<AtomicUsize>,
contexts: Arc<RwLock<BTreeMap<usize, String>>>,
}
impl AssertionContextManager {
pub fn new() -> Self {
Self {
id: Arc::new(AtomicUsize::new(0)),
contexts: Arc::new(RwLock::new(BTreeMap::new())),
}
}
pub fn add_context(&self, context: String) -> ContextHandle {
let id = self.id.fetch_add(1, Ordering::Relaxed);
let mut contexts = self.contexts.write();
contexts.insert(id, context);
ContextHandle {
id,
manager: self.clone(),
}
}
pub fn context(&self) -> String {
let contexts = self.contexts.read();
format!("\n{}\n", contexts.values().join("\n"))
}
}
/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
/// the state that was set initially for the failure can be printed in the error message
pub struct ContextHandle {
id: usize,
manager: AssertionContextManager,
}
impl Drop for ContextHandle {
fn drop(&mut self) {
let mut contexts = self.manager.contexts.write();
contexts.remove(&self.id);
}
}

View file

@ -0,0 +1,737 @@
use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor,
InputEvent, IntoElement, KeyDownEvent, Keystroke, Model, ModelContext, Pixels, Platform,
PlatformWindow, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
TestWindowHandlers, TextSystem, View, ViewContext, VisualContext, WindowBounds, WindowContext,
WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
use std::{future::Future, mem, ops::Deref, rc::Rc, sync::Arc, time::Duration};
#[derive(Clone)]
pub struct TestAppContext {
pub app: Rc<AppCell>,
pub background_executor: BackgroundExecutor,
pub foreground_executor: ForegroundExecutor,
pub dispatcher: TestDispatcher,
pub test_platform: Rc<TestPlatform>,
text_system: Arc<TextSystem>,
}
impl Context for TestAppContext {
type Result<T> = T;
fn new_model<T: 'static>(
&mut self,
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
) -> Self::Result<Model<T>>
where
T: 'static,
{
let mut app = self.app.borrow_mut();
app.new_model(build_model)
}
fn update_model<T: 'static, R>(
&mut self,
handle: &Model<T>,
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> Self::Result<R> {
let mut app = self.app.borrow_mut();
app.update_model(handle, update)
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
{
let mut lock = self.app.borrow_mut();
lock.update_window(window, f)
}
fn read_model<T, R>(
&self,
handle: &Model<T>,
read: impl FnOnce(&T, &AppContext) -> R,
) -> Self::Result<R>
where
T: 'static,
{
let app = self.app.borrow();
app.read_model(handle, read)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static,
{
let app = self.app.borrow();
app.read_window(window, read)
}
}
impl TestAppContext {
pub fn new(dispatcher: TestDispatcher) -> Self {
let arc_dispatcher = Arc::new(dispatcher.clone());
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
let asset_source = Arc::new(());
let http_client = util::http::FakeHttpClient::with_404_response();
let text_system = Arc::new(TextSystem::new(platform.text_system()));
Self {
app: AppContext::new(platform.clone(), asset_source, http_client),
background_executor,
foreground_executor,
dispatcher: dispatcher.clone(),
test_platform: platform,
text_system,
}
}
pub fn new_app(&self) -> TestAppContext {
Self::new(self.dispatcher.clone())
}
pub fn quit(&self) {
self.app.borrow_mut().shutdown();
}
pub fn refresh(&mut self) -> Result<()> {
let mut app = self.app.borrow_mut();
app.refresh();
Ok(())
}
pub fn executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
}
pub fn foreground_executor(&self) -> &ForegroundExecutor {
&self.foreground_executor
}
pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> R {
let mut cx = self.app.borrow_mut();
cx.update(f)
}
pub fn read<R>(&self, f: impl FnOnce(&AppContext) -> R) -> R {
let cx = self.app.borrow();
f(&*cx)
}
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
where
F: FnOnce(&mut ViewContext<V>) -> V,
V: 'static + Render,
{
let mut cx = self.app.borrow_mut();
cx.open_window(WindowOptions::default(), |cx| cx.new_view(build_window))
}
pub fn add_empty_window(&mut self) -> AnyWindowHandle {
let mut cx = self.app.borrow_mut();
cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {}))
.any_handle
}
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
where
F: FnOnce(&mut ViewContext<V>) -> V,
V: 'static + Render,
{
let mut cx = self.app.borrow_mut();
let window = cx.open_window(WindowOptions::default(), |cx| cx.new_view(build_window));
drop(cx);
let view = window.root_view(self).unwrap();
let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
// it might be nice to try and cleanup these at the end of each test.
(view, Box::leak(cx))
}
pub fn text_system(&self) -> &Arc<TextSystem> {
&self.text_system
}
pub fn write_to_clipboard(&self, item: ClipboardItem) {
self.test_platform.write_to_clipboard(item)
}
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.test_platform.read_from_clipboard()
}
pub fn simulate_new_path_selection(
&self,
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
) {
self.test_platform.simulate_new_path_selection(select_path);
}
pub fn simulate_prompt_answer(&self, button_ix: usize) {
self.test_platform.simulate_prompt_answer(button_ix);
}
pub fn has_pending_prompt(&self) -> bool {
self.test_platform.has_pending_prompt()
}
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
let (mut handlers, scale_factor) = self
.app
.borrow_mut()
.update_window(window_handle, |_, cx| {
let platform_window = cx.window.platform_window.as_test().unwrap();
let scale_factor = platform_window.scale_factor();
match &mut platform_window.bounds {
WindowBounds::Fullscreen | WindowBounds::Maximized => {
platform_window.bounds = WindowBounds::Fixed(Bounds {
origin: Point::default(),
size: size.map(|pixels| f64::from(pixels).into()),
});
}
WindowBounds::Fixed(bounds) => {
bounds.size = size.map(|pixels| f64::from(pixels).into());
}
}
(
mem::take(&mut platform_window.handlers.lock().resize),
scale_factor,
)
})
.unwrap();
for handler in &mut handlers {
handler(size, scale_factor);
}
self.app
.borrow_mut()
.update_window(window_handle, |_, cx| {
let platform_window = cx.window.platform_window.as_test().unwrap();
platform_window.handlers.lock().resize = handlers;
})
.unwrap();
}
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
where
Fut: Future<Output = R> + 'static,
R: 'static,
{
self.foreground_executor.spawn(f(self.to_async()))
}
pub fn has_global<G: 'static>(&self) -> bool {
let app = self.app.borrow();
app.has_global::<G>()
}
pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
let app = self.app.borrow();
read(app.global(), &app)
}
pub fn try_read_global<G: 'static, R>(
&self,
read: impl FnOnce(&G, &AppContext) -> R,
) -> Option<R> {
let lock = self.app.borrow();
Some(read(lock.try_global()?, &lock))
}
pub fn set_global<G: 'static>(&mut self, global: G) {
let mut lock = self.app.borrow_mut();
lock.set_global(global);
}
pub fn update_global<G: 'static, R>(
&mut self,
update: impl FnOnce(&mut G, &mut AppContext) -> R,
) -> R {
let mut lock = self.app.borrow_mut();
lock.update_global(update)
}
pub fn to_async(&self) -> AsyncAppContext {
AsyncAppContext {
app: Rc::downgrade(&self.app),
background_executor: self.background_executor.clone(),
foreground_executor: self.foreground_executor.clone(),
}
}
pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
where
A: Action,
{
window
.update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
.unwrap();
self.background_executor.run_until_parked()
}
/// simulate_keystrokes takes a space-separated list of keys to type.
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
/// will run backspace on the current editor through the command palette.
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
for keystroke in keystrokes
.split(" ")
.map(Keystroke::parse)
.map(Result::unwrap)
{
self.dispatch_keystroke(window, keystroke.into(), false);
}
self.background_executor.run_until_parked()
}
/// simulate_input takes a string of text to type.
/// cx.simulate_input("abc")
/// will type abc into your current editor.
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
self.dispatch_keystroke(window, keystroke.into(), false);
}
self.background_executor.run_until_parked()
}
pub fn dispatch_keystroke(
&mut self,
window: AnyWindowHandle,
keystroke: Keystroke,
is_held: bool,
) {
let keystroke2 = keystroke.clone();
let handled = window
.update(self, |_, cx| {
cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held }))
})
.is_ok_and(|handled| handled);
if handled {
return;
}
let input_handler = self.update_test_window(window, |window| window.input_handler.clone());
let Some(input_handler) = input_handler else {
panic!(
"dispatch_keystroke {:?} failed to dispatch action or input",
&keystroke2
);
};
let text = keystroke2.ime_key.unwrap_or(keystroke2.key);
input_handler.lock().replace_text_in_range(None, &text);
}
pub fn update_test_window<R>(
&mut self,
window: AnyWindowHandle,
f: impl FnOnce(&mut TestWindow) -> R,
) -> R {
window
.update(self, |_, cx| {
f(cx.window
.platform_window
.as_any_mut()
.downcast_mut::<TestWindow>()
.unwrap())
})
.unwrap()
}
pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
let (tx, rx) = futures::channel::mpsc::unbounded();
self.update(|cx| {
cx.observe(entity, {
let tx = tx.clone();
move |_, _| {
let _ = tx.unbounded_send(());
}
})
.detach();
cx.observe_release(entity, move |_, _| tx.close_channel())
.detach()
});
rx
}
pub fn events<Evt, T: 'static + EventEmitter<Evt>>(
&mut self,
entity: &Model<T>,
) -> futures::channel::mpsc::UnboundedReceiver<Evt>
where
Evt: 'static + Clone,
{
let (tx, rx) = futures::channel::mpsc::unbounded();
entity
.update(self, |_, cx: &mut ModelContext<T>| {
cx.subscribe(entity, move |_model, _handle, event, _cx| {
let _ = tx.unbounded_send(event.clone());
})
})
.detach();
rx
}
pub async fn condition<T: 'static>(
&mut self,
model: &Model<T>,
mut predicate: impl FnMut(&mut T, &mut ModelContext<T>) -> bool,
) {
let timer = self.executor().timer(Duration::from_secs(3));
let mut notifications = self.notifications(model);
use futures::FutureExt as _;
use smol::future::FutureExt as _;
async {
loop {
if model.update(self, &mut predicate) {
return Ok(());
}
if notifications.next().await.is_none() {
bail!("model dropped")
}
}
}
.race(timer.map(|_| Err(anyhow!("condition timed out"))))
.await
.unwrap();
}
}
impl<T: Send> Model<T> {
pub fn next_event<Evt>(&self, cx: &mut TestAppContext) -> Evt
where
Evt: Send + Clone + 'static,
T: EventEmitter<Evt>,
{
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let _subscription = self.update(cx, |_, cx| {
cx.subscribe(self, move |_, _, event, _| {
tx.unbounded_send(event.clone()).ok();
})
});
// Run other tasks until the event is emitted.
loop {
match rx.try_next() {
Ok(Some(event)) => return event,
Ok(None) => panic!("model was dropped"),
Err(_) => {
if !cx.executor().tick() {
break;
}
}
}
}
panic!("no event received")
}
}
impl<V: 'static> View<V> {
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (mut tx, mut rx) = postage::mpsc::channel(1);
let mut cx = cx.app.app.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.try_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.recv())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
}
impl<V> View<V> {
pub fn condition<Evt>(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&V, &AppContext) -> bool,
) -> impl Future<Output = ()>
where
Evt: 'static,
V: EventEmitter<Evt>,
{
use postage::prelude::{Sink as _, Stream as _};
let (tx, mut rx) = postage::mpsc::channel(1024);
let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration();
let mut cx = cx.app.borrow_mut();
let subscriptions = (
cx.observe(self, {
let mut tx = tx.clone();
move |_, _| {
tx.blocking_send(()).ok();
}
}),
cx.subscribe(self, {
let mut tx = tx.clone();
move |_, _: &Evt, _| {
tx.blocking_send(()).ok();
}
}),
);
let cx = cx.this.upgrade().unwrap();
let handle = self.downgrade();
async move {
crate::util::timeout(timeout_duration, async move {
loop {
{
let cx = cx.borrow();
let cx = &*cx;
if predicate(
handle
.upgrade()
.expect("view dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
// todo!(start_waiting)
// cx.borrow().foreground_executor().start_waiting();
rx.recv()
.await
.expect("view dropped with pending condition");
// cx.borrow().foreground_executor().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
use derive_more::{Deref, DerefMut};
#[derive(Deref, DerefMut)]
pub struct VisualTestContext<'a> {
#[deref]
#[deref_mut]
cx: &'a mut TestAppContext,
window: AnyWindowHandle,
}
impl<'a> VisualTestContext<'a> {
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
}
pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self {
Self { cx, window }
}
pub fn run_until_parked(&self) {
self.cx.background_executor.run_until_parked();
}
pub fn dispatch_action<A>(&mut self, action: A)
where
A: Action,
{
self.cx.dispatch_action(self.window, action)
}
pub fn window_title(&mut self) -> Option<String> {
self.cx
.update_window(self.window, |_, cx| {
cx.window.platform_window.as_test().unwrap().title.clone()
})
.unwrap()
}
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
self.cx.simulate_keystrokes(self.window, keystrokes)
}
pub fn simulate_input(&mut self, input: &str) {
self.cx.simulate_input(self.window, input)
}
pub fn simulate_activation(&mut self) {
self.simulate_window_events(&mut |handlers| {
handlers
.active_status_change
.iter_mut()
.for_each(|f| f(true));
})
}
pub fn simulate_deactivation(&mut self) {
self.simulate_window_events(&mut |handlers| {
handlers
.active_status_change
.iter_mut()
.for_each(|f| f(false));
})
}
fn simulate_window_events(&mut self, f: &mut dyn FnMut(&mut TestWindowHandlers)) {
let handlers = self
.cx
.update_window(self.window, |_, cx| {
cx.window
.platform_window
.as_test()
.unwrap()
.handlers
.clone()
})
.unwrap();
f(&mut *handlers.lock());
}
}
impl<'a> Context for VisualTestContext<'a> {
type Result<T> = <TestAppContext as Context>::Result<T>;
fn new_model<T: 'static>(
&mut self,
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
) -> Self::Result<Model<T>> {
self.cx.new_model(build_model)
}
fn update_model<T, R>(
&mut self,
handle: &Model<T>,
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> Self::Result<R>
where
T: 'static,
{
self.cx.update_model(handle, update)
}
fn read_model<T, R>(
&self,
handle: &Model<T>,
read: impl FnOnce(&T, &AppContext) -> R,
) -> Self::Result<R>
where
T: 'static,
{
self.cx.read_model(handle, read)
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
{
self.cx.update_window(window, f)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static,
{
self.cx.read_window(window, read)
}
}
impl<'a> VisualContext for VisualTestContext<'a> {
fn new_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
V: 'static + Render,
{
self.window
.update(self.cx, |_, cx| cx.new_view(build_view))
.unwrap()
}
fn update_view<V: 'static, R>(
&mut self,
view: &View<V>,
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
) -> Self::Result<R> {
self.window
.update(self.cx, |_, cx| cx.update_view(view, update))
.unwrap()
}
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
V: 'static + Render,
{
self.window
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
.unwrap()
}
fn focus_view<V: crate::FocusableView>(&mut self, view: &View<V>) -> Self::Result<()> {
self.window
.update(self.cx, |_, cx| {
view.read(cx).focus_handle(cx).clone().focus(cx)
})
.unwrap()
}
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: crate::ManagedView,
{
self.window
.update(self.cx, |_, cx| {
view.update(cx, |_, cx| cx.emit(crate::DismissEvent))
})
.unwrap()
}
}
impl AnyWindowHandle {
pub fn build_view<V: Render + 'static>(
&self,
cx: &mut TestAppContext,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> View<V> {
self.update(cx, |_, cx| cx.new_view(build_view)).unwrap()
}
}
pub struct EmptyView {}
impl Render for EmptyView {
fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> impl IntoElement {
div()
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,90 +0,0 @@
use std::{cell::RefCell, ops::Range, rc::Rc};
use pathfinder_geometry::rect::RectF;
use crate::{platform::InputHandler, window::WindowContext, AnyView, AnyWindowHandle, AppContext};
pub struct WindowInputHandler {
pub app: Rc<RefCell<AppContext>>,
pub window: AnyWindowHandle,
}
impl WindowInputHandler {
fn read_focused_view<T, F>(&self, f: F) -> Option<T>
where
F: FnOnce(&dyn AnyView, &WindowContext) -> T,
{
// Input-related application hooks are sometimes called by the OS during
// a call to a window-manipulation API, like prompting the user for file
// paths. In that case, the AppContext will already be borrowed, so any
// InputHandler methods need to fail gracefully.
//
// See https://github.com/zed-industries/community/issues/444
let mut app = self.app.try_borrow_mut().ok()?;
self.window.update_optional(&mut *app, |cx| {
let view_id = cx.window.focused_view_id?;
let view = cx.views.get(&(self.window, view_id))?;
let result = f(view.as_ref(), &cx);
Some(result)
})
}
fn update_focused_view<T, F>(&mut self, f: F) -> Option<T>
where
F: FnOnce(&mut dyn AnyView, &mut WindowContext, usize) -> T,
{
let mut app = self.app.try_borrow_mut().ok()?;
self.window
.update(&mut *app, |cx| {
let view_id = cx.window.focused_view_id?;
cx.update_any_view(view_id, |view, cx| f(view, cx, view_id))
})
.flatten()
}
}
impl InputHandler for WindowInputHandler {
fn text_for_range(&self, range: Range<usize>) -> Option<String> {
self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx))
.flatten()
}
fn selected_text_range(&self) -> Option<Range<usize>> {
self.read_focused_view(|view, cx| view.selected_text_range(cx))
.flatten()
}
fn replace_text_in_range(&mut self, range: Option<Range<usize>>, text: &str) {
self.update_focused_view(|view, cx, view_id| {
view.replace_text_in_range(range, text, cx, view_id);
});
}
fn marked_text_range(&self) -> Option<Range<usize>> {
self.read_focused_view(|view, cx| view.marked_text_range(cx))
.flatten()
}
fn unmark_text(&mut self) {
self.update_focused_view(|view, cx, view_id| {
view.unmark_text(cx, view_id);
});
}
fn replace_and_mark_text_in_range(
&mut self,
range: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
) {
self.update_focused_view(|view, cx, view_id| {
view.replace_and_mark_text_in_range(range, new_text, new_selected_range, cx, view_id);
});
}
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
self.window.read_optional_with(&*self.app.borrow(), |cx| {
cx.rect_for_text_range(range_utf16)
})
}
}

202
crates/gpui/src/arena.rs Normal file
View file

@ -0,0 +1,202 @@
use std::{
alloc,
cell::Cell,
ops::{Deref, DerefMut},
ptr,
rc::Rc,
};
struct ArenaElement {
value: *mut u8,
drop: unsafe fn(*mut u8),
}
impl Drop for ArenaElement {
#[inline(always)]
fn drop(&mut self) {
unsafe {
(self.drop)(self.value);
}
}
}
pub struct Arena {
start: *mut u8,
end: *mut u8,
offset: *mut u8,
elements: Vec<ArenaElement>,
valid: Rc<Cell<bool>>,
}
impl Arena {
pub fn new(size_in_bytes: usize) -> Self {
unsafe {
let layout = alloc::Layout::from_size_align(size_in_bytes, 1).unwrap();
let start = alloc::alloc(layout);
let end = start.add(size_in_bytes);
Self {
start,
end,
offset: start,
elements: Vec::new(),
valid: Rc::new(Cell::new(true)),
}
}
}
pub fn clear(&mut self) {
self.valid.set(false);
self.valid = Rc::new(Cell::new(true));
self.elements.clear();
self.offset = self.start;
}
#[inline(always)]
pub fn alloc<T>(&mut self, f: impl FnOnce() -> T) -> ArenaBox<T> {
#[inline(always)]
unsafe fn inner_writer<T, F>(ptr: *mut T, f: F)
where
F: FnOnce() -> T,
{
ptr::write(ptr, f());
}
unsafe fn drop<T>(ptr: *mut u8) {
std::ptr::drop_in_place(ptr.cast::<T>());
}
unsafe {
let layout = alloc::Layout::new::<T>().pad_to_align();
let next_offset = self.offset.add(layout.size());
assert!(next_offset <= self.end);
let result = ArenaBox {
ptr: self.offset.cast(),
valid: self.valid.clone(),
};
inner_writer(result.ptr, f);
self.elements.push(ArenaElement {
value: self.offset,
drop: drop::<T>,
});
self.offset = next_offset;
result
}
}
}
impl Drop for Arena {
fn drop(&mut self) {
self.clear();
}
}
pub struct ArenaBox<T: ?Sized> {
ptr: *mut T,
valid: Rc<Cell<bool>>,
}
impl<T: ?Sized> ArenaBox<T> {
#[inline(always)]
pub fn map<U: ?Sized>(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaBox<U> {
ArenaBox {
ptr: f(&mut self),
valid: self.valid,
}
}
fn validate(&self) {
assert!(
self.valid.get(),
"attempted to dereference an ArenaRef after its Arena was cleared"
);
}
}
impl<T: ?Sized> Deref for ArenaBox<T> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &Self::Target {
self.validate();
unsafe { &*self.ptr }
}
}
impl<T: ?Sized> DerefMut for ArenaBox<T> {
#[inline(always)]
fn deref_mut(&mut self) -> &mut Self::Target {
self.validate();
unsafe { &mut *self.ptr }
}
}
pub struct ArenaRef<T: ?Sized>(ArenaBox<T>);
impl<T: ?Sized> From<ArenaBox<T>> for ArenaRef<T> {
fn from(value: ArenaBox<T>) -> Self {
ArenaRef(value)
}
}
impl<T: ?Sized> Clone for ArenaRef<T> {
fn clone(&self) -> Self {
Self(ArenaBox {
ptr: self.0.ptr,
valid: self.0.valid.clone(),
})
}
}
impl<T: ?Sized> Deref for ArenaRef<T> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &Self::Target {
self.0.deref()
}
}
#[cfg(test)]
mod tests {
use std::{cell::Cell, rc::Rc};
use super::*;
#[test]
fn test_arena() {
let mut arena = Arena::new(1024);
let a = arena.alloc(|| 1u64);
let b = arena.alloc(|| 2u32);
let c = arena.alloc(|| 3u16);
let d = arena.alloc(|| 4u8);
assert_eq!(*a, 1);
assert_eq!(*b, 2);
assert_eq!(*c, 3);
assert_eq!(*d, 4);
arena.clear();
let a = arena.alloc(|| 5u64);
let b = arena.alloc(|| 6u32);
let c = arena.alloc(|| 7u16);
let d = arena.alloc(|| 8u8);
assert_eq!(*a, 5);
assert_eq!(*b, 6);
assert_eq!(*c, 7);
assert_eq!(*d, 8);
// Ensure drop gets called.
let dropped = Rc::new(Cell::new(false));
struct DropGuard(Rc<Cell<bool>>);
impl Drop for DropGuard {
fn drop(&mut self) {
self.0.set(true);
}
}
arena.alloc(|| DropGuard(dropped.clone()));
arena.clear();
assert!(dropped.get());
}
}

View file

@ -1,12 +1,16 @@
use anyhow::{anyhow, Result};
use image::ImageFormat;
use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
use crate::ImageData;
use crate::{size, DevicePixels, Result, SharedString, Size};
use anyhow::anyhow;
use image::{Bgra, ImageBuffer};
use std::{
borrow::Cow,
fmt,
hash::Hash,
sync::atomic::{AtomicUsize, Ordering::SeqCst},
};
pub trait AssetSource: 'static + Send + Sync {
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
fn list(&self, path: &str) -> Vec<Cow<'static, str>>;
fn list(&self, path: &str) -> Result<Vec<SharedString>>;
}
impl AssetSource for () {
@ -17,49 +21,44 @@ impl AssetSource for () {
))
}
fn list(&self, _: &str) -> Vec<Cow<'static, str>> {
vec![]
fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
Ok(vec![])
}
}
pub struct AssetCache {
source: Box<dyn AssetSource>,
svgs: RefCell<HashMap<String, usvg::Tree>>,
pngs: RefCell<HashMap<String, Arc<ImageData>>>,
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ImageId(usize);
pub struct ImageData {
pub id: ImageId,
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
}
impl AssetCache {
pub fn new(source: impl AssetSource) -> Self {
impl ImageData {
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Self {
source: Box::new(source),
svgs: RefCell::new(HashMap::new()),
pngs: RefCell::new(HashMap::new()),
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
data,
}
}
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)
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn png(&self, path: &str) -> Result<Arc<ImageData>> {
let mut pngs = self.pngs.borrow_mut();
if let Some(png) = pngs.get(path) {
Ok(png.clone())
} else {
let bytes = self.source.load(path)?;
let image = ImageData::new(
image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(),
);
pngs.insert(path.to_string(), image.clone());
Ok(image)
}
pub fn size(&self) -> Size<DevicePixels> {
let (width, height) = self.data.dimensions();
size(width.into(), height.into())
}
}
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()
}
}

View file

@ -1,42 +0,0 @@
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()
}
}

View file

@ -1,65 +1,223 @@
use std::{
borrow::Cow,
fmt,
ops::{Deref, DerefMut},
};
use anyhow::bail;
use serde::de::{self, Deserialize, Deserializer, Visitor};
use std::fmt;
use crate::json::ToJson;
use pathfinder_color::{ColorF, ColorU};
use schemars::JsonSchema;
use serde::{
de::{self, Unexpected},
Deserialize, Deserializer,
};
use serde_json::json;
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
#[repr(transparent)]
pub struct Color(#[schemars(with = "String")] pub ColorU);
pub fn color(rgba: u32) -> Color {
Color::from_u32(rgba)
pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
let b = (hex & 0xFF) as f32 / 255.0;
Rgba { r, g, b, a: 1.0 }.into()
}
pub fn rgb(r: f32, g: f32, b: f32) -> Color {
Color(ColorF::new(r, g, b, 1.).to_u8())
pub fn rgba(hex: u32) -> Rgba {
let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
let b = ((hex >> 8) & 0xFF) as f32 / 255.0;
let a = (hex & 0xFF) as f32 / 255.0;
Rgba { r, g, b, a }
}
pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
Color(ColorF::new(r, g, b, a).to_u8())
#[derive(PartialEq, Clone, Copy, Default)]
pub struct Rgba {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
pub fn transparent_black() -> Color {
Color(ColorU::transparent_black())
impl fmt::Debug for Rgba {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "rgba({:#010x})", u32::from(*self))
}
}
pub fn black() -> Color {
Color(ColorU::black())
impl Rgba {
pub fn blend(&self, other: Rgba) -> Self {
if other.a >= 1.0 {
other
} else if other.a <= 0.0 {
return *self;
} else {
return Rgba {
r: (self.r * (1.0 - other.a)) + (other.r * other.a),
g: (self.g * (1.0 - other.a)) + (other.g * other.a),
b: (self.b * (1.0 - other.a)) + (other.b * other.a),
a: self.a,
};
}
}
}
pub fn white() -> Color {
Color(ColorU::white())
impl From<Rgba> for u32 {
fn from(rgba: Rgba) -> Self {
let r = (rgba.r * 255.0) as u32;
let g = (rgba.g * 255.0) as u32;
let b = (rgba.b * 255.0) as u32;
let a = (rgba.a * 255.0) as u32;
(r << 24) | (g << 16) | (b << 8) | a
}
}
pub fn red() -> Color {
color(0xff0000ff)
struct RgbaVisitor;
impl<'de> Visitor<'de> for RgbaVisitor {
type Value = Rgba;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
Rgba::try_from(value).map_err(E::custom)
}
}
pub fn green() -> Color {
color(0x00ff00ff)
impl<'de> Deserialize<'de> for Rgba {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(RgbaVisitor)
}
}
pub fn blue() -> Color {
color(0x0000ffff)
impl From<Hsla> for Rgba {
fn from(color: Hsla) -> Self {
let h = color.h;
let s = color.s;
let l = color.l;
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
let m = l - c / 2.0;
let cm = c + m;
let xm = x + m;
let (r, g, b) = match (h * 6.0).floor() as i32 {
0 | 6 => (cm, xm, m),
1 => (xm, cm, m),
2 => (m, cm, xm),
3 => (m, xm, cm),
4 => (xm, m, cm),
_ => (cm, m, xm),
};
Rgba {
r,
g,
b,
a: color.a,
}
}
}
pub fn yellow() -> Color {
color(0xffff00ff)
impl TryFrom<&'_ str> for Rgba {
type Error = anyhow::Error;
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
const RGB: usize = "rgb".len();
const RGBA: usize = "rgba".len();
const RRGGBB: usize = "rrggbb".len();
const RRGGBBAA: usize = "rrggbbaa".len();
const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
let Some(("", hex)) = value.trim().split_once('#') else {
bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
};
let (r, g, b, a) = match hex.len() {
RGB | RGBA => {
let r = u8::from_str_radix(&hex[0..1], 16)?;
let g = u8::from_str_radix(&hex[1..2], 16)?;
let b = u8::from_str_radix(&hex[2..3], 16)?;
let a = if hex.len() == RGBA {
u8::from_str_radix(&hex[3..4], 16)?
} else {
0xf
};
/// Duplicates a given hex digit.
/// E.g., `0xf` -> `0xff`.
const fn duplicate(value: u8) -> u8 {
value << 4 | value
}
(duplicate(r), duplicate(g), duplicate(b), duplicate(a))
}
RRGGBB | RRGGBBAA => {
let r = u8::from_str_radix(&hex[0..2], 16)?;
let g = u8::from_str_radix(&hex[2..4], 16)?;
let b = u8::from_str_radix(&hex[4..6], 16)?;
let a = if hex.len() == RRGGBBAA {
u8::from_str_radix(&hex[6..8], 16)?
} else {
0xff
};
(r, g, b, a)
}
_ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
};
Ok(Rgba {
r: r as f32 / 255.,
g: g as f32 / 255.,
b: b as f32 / 255.,
a: a as f32 / 255.,
})
}
}
impl Color {
pub fn transparent_black() -> Self {
transparent_black()
#[derive(Default, Copy, Clone, Debug)]
#[repr(C)]
pub struct Hsla {
pub h: f32,
pub s: f32,
pub l: f32,
pub a: f32,
}
impl PartialEq for Hsla {
fn eq(&self, other: &Self) -> bool {
self.h
.total_cmp(&other.h)
.then(self.s.total_cmp(&other.s))
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
.is_eq()
}
}
impl PartialOrd for Hsla {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// SAFETY: The total ordering relies on this always being Some()
Some(
self.h
.total_cmp(&other.h)
.then(self.s.total_cmp(&other.s))
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))),
)
}
}
impl Ord for Hsla {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// SAFETY: The partial comparison is a total comparison
unsafe { self.partial_cmp(other).unwrap_unchecked() }
}
}
impl Hsla {
pub fn to_rgb(self) -> Rgba {
self.into()
}
pub fn red() -> Self {
red()
}
pub fn green() -> Self {
green()
}
pub fn blue() -> Self {
blue()
}
pub fn black() -> Self {
@ -70,107 +228,230 @@ impl Color {
white()
}
pub fn red() -> Self {
Color::from_u32(0xff0000ff)
}
pub fn green() -> Self {
Color::from_u32(0x00ff00ff)
}
pub fn blue() -> Self {
Color::from_u32(0x0000ffff)
}
pub fn yellow() -> Self {
Color::from_u32(0xffff00ff)
}
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))
}
pub fn blend(source: Color, dest: Color) -> Color {
// Skip blending if we don't need it.
if source.a == 255 {
return source;
} else if source.a == 0 {
return dest;
}
let source = source.0.to_f32();
let dest = dest.0.to_f32();
let a = source.a() + (dest.a() * (1. - source.a()));
let r = ((source.r() * source.a()) + (dest.r() * dest.a() * (1. - source.a()))) / a;
let g = ((source.g() * source.a()) + (dest.g() * dest.a() * (1. - source.a()))) / a;
let b = ((source.b() * source.a()) + (dest.b() * dest.a() * (1. - source.a()))) / a;
Self(ColorF::new(r, g, b, a).to_u8())
}
pub fn fade_out(&mut self, fade: f32) {
let fade = fade.clamp(0., 1.);
self.0.a = (self.0.a as f32 * (1. - fade)) as u8;
pub fn transparent_black() -> Self {
transparent_black()
}
}
impl<'de> Deserialize<'de> for Color {
impl Eq for Hsla {}
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
Hsla {
h: h.clamp(0., 1.),
s: s.clamp(0., 1.),
l: l.clamp(0., 1.),
a: a.clamp(0., 1.),
}
}
pub fn black() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 1.,
}
}
pub fn transparent_black() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.,
}
}
pub fn white() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 1.,
a: 1.,
}
}
pub fn red() -> Hsla {
Hsla {
h: 0.,
s: 1.,
l: 0.5,
a: 1.,
}
}
pub fn blue() -> Hsla {
Hsla {
h: 0.6,
s: 1.,
l: 0.5,
a: 1.,
}
}
pub fn green() -> Hsla {
Hsla {
h: 0.33,
s: 1.,
l: 0.5,
a: 1.,
}
}
pub fn yellow() -> Hsla {
Hsla {
h: 0.16,
s: 1.,
l: 0.5,
a: 1.,
}
}
impl Hsla {
/// Returns true if the HSLA color is fully transparent, false otherwise.
pub fn is_transparent(&self) -> bool {
self.a == 0.0
}
/// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
///
/// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
/// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
/// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
///
/// Assumptions:
/// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
/// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing it's own alpha value.
/// - RGB color components are contained in the range [0, 1].
/// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
pub fn blend(self, other: Hsla) -> Hsla {
let alpha = other.a;
if alpha >= 1.0 {
other
} else if alpha <= 0.0 {
return self;
} else {
let converted_self = Rgba::from(self);
let converted_other = Rgba::from(other);
let blended_rgb = converted_self.blend(converted_other);
return Hsla::from(blended_rgb);
}
}
/// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
/// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
pub fn fade_out(&mut self, factor: f32) {
self.a *= 1.0 - factor.clamp(0., 1.);
}
}
// impl From<Hsla> for Rgba {
// fn from(value: Hsla) -> Self {
// let h = value.h;
// let s = value.s;
// let l = value.l;
// let c = (1 - |2L - 1|) X s
// }
// }
impl From<Rgba> for Hsla {
fn from(color: Rgba) -> Self {
let r = color.r;
let g = color.g;
let b = color.b;
let max = r.max(g.max(b));
let min = r.min(g.min(b));
let delta = max - min;
let l = (max + min) / 2.0;
let s = if l == 0.0 || l == 1.0 {
0.0
} else if l < 0.5 {
delta / (2.0 * l)
} else {
delta / (2.0 - 2.0 * l)
};
let h = if delta == 0.0 {
0.0
} else if max == r {
((g - b) / delta).rem_euclid(6.0) / 6.0
} else if max == g {
((b - r) / delta + 2.0) / 6.0
} else {
((r - g) / delta + 4.0) / 6.0
};
Hsla {
h,
s,
l,
a: color.a,
}
}
}
impl<'de> Deserialize<'de> for Hsla {
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]",
))
// First, deserialize it into Rgba
let rgba = Rgba::deserialize(deserializer)?;
// Then, use the From<Rgba> for Hsla implementation to convert it
Ok(Hsla::from(rgba))
}
}
impl From<u32> for Color {
fn from(value: u32) -> Self {
Self(ColorU::from_u32(value))
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
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
))
}
}
use super::*;
impl Deref for Color {
type Target = ColorU;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[test]
fn test_deserialize_three_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
impl DerefMut for Color {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
assert_eq!(actual, rgba(0xff0099ff))
}
}
impl fmt::Debug for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
#[test]
fn test_deserialize_four_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_six_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_eight_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
assert_eq!(actual, rgba(0xf5f5f5ff))
}
#[test]
fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
assert_eq!(actual, rgba(0xdeadbeef))
}
}

View file

@ -1 +0,0 @@

501
crates/gpui/src/element.rs Normal file
View file

@ -0,0 +1,501 @@
use crate::{
ArenaBox, AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size,
ViewContext, WindowContext, ELEMENT_ARENA,
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
use std::{any::Any, fmt::Debug};
/// Implemented by types that participate in laying out and painting the contents of a window.
/// Elements form a tree and are laid out according to web-based layout rules.
/// Rather than calling methods on implementers of this trait directly, you'll usually call `into_any` to convert them into an AnyElement, which manages state internally.
/// You can create custom elements by implementing this trait.
pub trait Element: 'static + IntoElement {
type State: 'static;
fn request_layout(
&mut self,
state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State);
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
fn into_any(self) -> AnyElement {
AnyElement::new(self)
}
}
/// Implemented by any type that can be converted into an element.
pub trait IntoElement: Sized {
/// The specific type of element into which the implementing type is converted.
type Element: Element;
/// The [ElementId] of self once converted into an [Element].
/// If present, the resulting element's state will be carried across frames.
fn element_id(&self) -> Option<ElementId>;
/// Convert self into a type that implements [Element].
fn into_element(self) -> Self::Element;
/// Convert self into a dynamically-typed [AnyElement].
fn into_any_element(self) -> AnyElement {
self.into_element().into_any()
}
/// Convert into an element, then draw in the current window at the given origin.
/// The provided available space is provided to the layout engine to determine the size of the root element.
/// Once the element is drawn, its associated element staet is yielded to the given callback.
fn draw_and_update_state<T, R>(
self,
origin: Point<Pixels>,
available_space: Size<T>,
cx: &mut WindowContext,
f: impl FnOnce(&mut <Self::Element as Element>::State, &mut WindowContext) -> R,
) -> R
where
T: Clone + Default + Debug + Into<AvailableSpace>,
{
let element = self.into_element();
let element_id = element.element_id();
let element = DrawableElement {
element: Some(element),
phase: ElementDrawPhase::Start,
};
let frame_state =
DrawableElement::draw(element, origin, available_space.map(Into::into), cx);
if let Some(mut frame_state) = frame_state {
f(&mut frame_state, cx)
} else {
cx.with_element_state(element_id.unwrap(), |element_state, cx| {
let mut element_state = element_state.unwrap();
let result = f(&mut element_state, cx);
(result, element_state)
})
}
}
/// Convert self to another type by calling the given closure. Useful in rendering code.
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
where
Self: Sized,
U: IntoElement,
{
f(self)
}
/// Conditionally chain onto self with the given closure. Useful in rendering code.
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| if condition { then(this) } else { this })
}
/// Conditionally chain onto self with the given closure if the given option is Some.
/// The contents of the option are provided to the closure.
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| {
if let Some(value) = option {
then(this, value)
} else {
this
}
})
}
}
pub trait Render: 'static + Sized {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
}
/// You can derive [IntoElement] on any type that implements this trait.
/// It is used to allow views to be expressed in terms of abstract data.
pub trait RenderOnce: 'static {
fn render(self, cx: &mut WindowContext) -> impl IntoElement;
}
pub trait ParentElement {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
fn child(mut self, child: impl IntoElement) -> Self
where
Self: Sized,
{
self.children_mut().push(child.into_element().into_any());
self
}
fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
where
Self: Sized,
{
self.children_mut()
.extend(children.into_iter().map(|child| child.into_any_element()));
self
}
}
pub struct Component<C: RenderOnce>(Option<C>);
impl<C: RenderOnce> Component<C> {
pub fn new(component: C) -> Self {
Component(Some(component))
}
}
impl<C: RenderOnce> Element for Component<C> {
type State = AnyElement;
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let mut element = self.0.take().unwrap().render(cx).into_any_element();
let layout_id = element.request_layout(cx);
(layout_id, element)
}
fn paint(&mut self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
element.paint(cx)
}
}
impl<C: RenderOnce> IntoElement for Component<C> {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
None
}
fn into_element(self) -> Self::Element {
self
}
}
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
trait ElementObject {
fn element_id(&self) -> Option<ElementId>;
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId;
fn paint(&mut self, cx: &mut WindowContext);
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Size<Pixels>;
fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
);
}
pub struct DrawableElement<E: Element> {
element: Option<E>,
phase: ElementDrawPhase<E::State>,
}
#[derive(Default)]
enum ElementDrawPhase<S> {
#[default]
Start,
LayoutRequested {
layout_id: LayoutId,
frame_state: Option<S>,
},
LayoutComputed {
layout_id: LayoutId,
available_space: Size<AvailableSpace>,
frame_state: Option<S>,
},
}
/// A wrapper around an implementer of [Element] that allows it to be drawn in a window.
impl<E: Element> DrawableElement<E> {
fn new(element: E) -> Self {
DrawableElement {
element: Some(element),
phase: ElementDrawPhase::Start,
}
}
fn element_id(&self) -> Option<ElementId> {
self.element.as_ref()?.element_id()
}
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id()
{
let layout_id = cx.with_element_state(id, |element_state, cx| {
self.element
.as_mut()
.unwrap()
.request_layout(element_state, cx)
});
(layout_id, None)
} else {
let (layout_id, frame_state) = self.element.as_mut().unwrap().request_layout(None, cx);
(layout_id, Some(frame_state))
};
self.phase = ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
};
layout_id
}
fn paint(mut self, cx: &mut WindowContext) -> Option<E::State> {
match self.phase {
ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
}
| ElementDrawPhase::LayoutComputed {
layout_id,
frame_state,
..
} => {
let bounds = cx.layout_bounds(layout_id);
if let Some(mut frame_state) = frame_state {
self.element
.take()
.unwrap()
.paint(bounds, &mut frame_state, cx);
Some(frame_state)
} else {
let element_id = self
.element
.as_ref()
.unwrap()
.element_id()
.expect("if we don't have frame state, we should have element state");
cx.with_element_state(element_id, |element_state, cx| {
let mut element_state = element_state.unwrap();
self.element
.take()
.unwrap()
.paint(bounds, &mut element_state, cx);
((), element_state)
});
None
}
}
_ => panic!("must call layout before paint"),
}
}
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Size<Pixels> {
if matches!(&self.phase, ElementDrawPhase::Start) {
self.request_layout(cx);
}
let layout_id = match &mut self.phase {
ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
} => {
cx.compute_layout(*layout_id, available_space);
let layout_id = *layout_id;
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
available_space,
frame_state: frame_state.take(),
};
layout_id
}
ElementDrawPhase::LayoutComputed {
layout_id,
available_space: prev_available_space,
..
} => {
if available_space != *prev_available_space {
cx.compute_layout(*layout_id, available_space);
*prev_available_space = available_space;
}
*layout_id
}
_ => panic!("cannot measure after painting"),
};
cx.layout_bounds(layout_id).size
}
fn draw(
mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Option<E::State> {
self.measure(available_space, cx);
cx.with_absolute_element_offset(origin, |cx| self.paint(cx))
}
}
impl<E> ElementObject for Option<DrawableElement<E>>
where
E: Element,
E::State: 'static,
{
fn element_id(&self) -> Option<ElementId> {
self.as_ref().unwrap().element_id()
}
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
DrawableElement::request_layout(self.as_mut().unwrap(), cx)
}
fn paint(&mut self, cx: &mut WindowContext) {
DrawableElement::paint(self.take().unwrap(), cx);
}
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Size<Pixels> {
DrawableElement::measure(self.as_mut().unwrap(), available_space, cx)
}
fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) {
DrawableElement::draw(self.take().unwrap(), origin, available_space, cx);
}
}
pub struct AnyElement(ArenaBox<dyn ElementObject>);
impl AnyElement {
pub fn new<E>(element: E) -> Self
where
E: 'static + Element,
E::State: Any,
{
let element = ELEMENT_ARENA
.with_borrow_mut(|arena| arena.alloc(|| Some(DrawableElement::new(element))))
.map(|element| element as &mut dyn ElementObject);
AnyElement(element)
}
pub fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
self.0.request_layout(cx)
}
pub fn paint(&mut self, cx: &mut WindowContext) {
self.0.paint(cx)
}
/// Initializes this element and performs layout within the given available space to determine its size.
pub fn measure(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) -> Size<Pixels> {
self.0.measure(available_space, cx)
}
/// Initializes this element and performs layout in the available space, then paints it at the given origin.
pub fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut WindowContext,
) {
self.0.draw(origin, available_space, cx)
}
pub fn inner_id(&self) -> Option<ElementId> {
self.0.element_id()
}
}
impl Element for AnyElement {
type State = ();
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let layout_id = self.request_layout(cx);
(layout_id, ())
}
fn paint(&mut self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
self.paint(cx)
}
}
impl IntoElement for AnyElement {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
None
}
fn into_element(self) -> Self::Element {
self
}
fn into_any_element(self) -> AnyElement {
self
}
}
/// The empty element, which renders nothing.
pub type Empty = ();
impl IntoElement for () {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
None
}
fn into_element(self) -> Self::Element {
self
}
}
impl Element for () {
type State = ();
fn request_layout(
&mut self,
_state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
(cx.request_layout(&crate::Style::default(), None), ())
}
fn paint(
&mut self,
_bounds: Bounds<Pixels>,
_state: &mut Self::State,
_cx: &mut WindowContext,
) {
}
}

View file

@ -1,740 +0,0 @@
mod align;
mod canvas;
mod clipped;
mod component;
mod constrained_box;
mod container;
mod empty;
mod expanded;
mod flex;
mod hook;
mod image;
mod keystroke_label;
mod label;
mod list;
mod mouse_event_handler;
mod overlay;
mod resizable;
mod stack;
mod svg;
mod text;
mod tooltip;
mod uniform_list;
pub use self::{
align::*, canvas::*, component::*, constrained_box::*, container::*, empty::*, flex::*,
hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
resizable::*, stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
};
pub use crate::window::ChildView;
use self::{clipped::Clipped, expanded::Expanded};
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json, Action, Entity, SizeConstraint, TypeTag, View, ViewContext, WeakViewHandle,
WindowContext,
};
use anyhow::{anyhow, Result};
use core::panic;
use json::ToJson;
use std::{
any::{type_name, Any},
borrow::Cow,
mem,
ops::Range,
};
pub trait Element<V: 'static>: 'static {
type LayoutState;
type PaintState;
fn view_name(&self) -> &'static str {
type_name::<V>()
}
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState);
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
layout: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState;
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
bounds: RectF,
visible_bounds: RectF,
layout: &Self::LayoutState,
paint: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF>;
fn metadata(&self) -> Option<&dyn Any> {
None
}
fn debug(
&self,
bounds: RectF,
layout: &Self::LayoutState,
paint: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value;
fn into_any(self) -> AnyElement<V>
where
Self: 'static + Sized,
{
AnyElement {
state: Box::new(ElementState::Init { element: self }),
name: None,
}
}
fn into_any_named(self, name: impl Into<Cow<'static, str>>) -> AnyElement<V>
where
Self: 'static + Sized,
{
AnyElement {
state: Box::new(ElementState::Init { element: self }),
name: Some(name.into()),
}
}
fn into_root_element(self, cx: &ViewContext<V>) -> RootElement<V>
where
Self: 'static + Sized,
{
RootElement {
element: self.into_any(),
view: cx.handle().downgrade(),
}
}
fn constrained(self) -> ConstrainedBox<V>
where
Self: 'static + Sized,
{
ConstrainedBox::new(self.into_any())
}
fn aligned(self) -> Align<V>
where
Self: 'static + Sized,
{
Align::new(self.into_any())
}
fn clipped(self) -> Clipped<V>
where
Self: 'static + Sized,
{
Clipped::new(self.into_any())
}
fn contained(self) -> Container<V>
where
Self: 'static + Sized,
{
Container::new(self.into_any())
}
fn expanded(self) -> Expanded<V>
where
Self: 'static + Sized,
{
Expanded::new(self.into_any())
}
fn flex(self, flex: f32, expanded: bool) -> FlexItem<V>
where
Self: 'static + Sized,
{
FlexItem::new(self.into_any()).flex(flex, expanded)
}
fn flex_float(self) -> FlexItem<V>
where
Self: 'static + Sized,
{
FlexItem::new(self.into_any()).float()
}
fn with_dynamic_tooltip(
self,
tag: TypeTag,
id: usize,
text: impl Into<Cow<'static, str>>,
action: Option<Box<dyn Action>>,
style: TooltipStyle,
cx: &mut ViewContext<V>,
) -> Tooltip<V>
where
Self: 'static + Sized,
{
Tooltip::new_dynamic(tag, id, text, action, style, self.into_any(), cx)
}
fn with_tooltip<Tag: 'static>(
self,
id: usize,
text: impl Into<Cow<'static, str>>,
action: Option<Box<dyn Action>>,
style: TooltipStyle,
cx: &mut ViewContext<V>,
) -> Tooltip<V>
where
Self: 'static + Sized,
{
Tooltip::new::<Tag>(id, text, action, style, self.into_any(), cx)
}
/// Uses the the given element to calculate resizes for the given tag
fn provide_resize_bounds<Tag: 'static>(self) -> BoundsProvider<V, Tag>
where
Self: 'static + Sized,
{
BoundsProvider::<_, Tag>::new(self.into_any())
}
/// Calls the given closure with the new size of the element whenever the
/// handle is dragged. This will be calculated in relation to the bounds
/// provided by the given tag
fn resizable<Tag: 'static>(
self,
side: HandleSide,
size: f32,
on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
) -> Resizable<V>
where
Self: 'static + Sized,
{
Resizable::new::<Tag>(self.into_any(), side, size, on_resize)
}
fn mouse<Tag: 'static>(self, region_id: usize) -> MouseEventHandler<V>
where
Self: Sized,
{
MouseEventHandler::for_child::<Tag>(self.into_any(), region_id)
}
fn component(self) -> StatelessElementAdapter
where
Self: Sized,
{
StatelessElementAdapter::new(self.into_any())
}
fn stateful_component(self) -> StatefulElementAdapter<V>
where
Self: Sized,
{
StatefulElementAdapter::new(self.into_any())
}
fn styleable_component(self) -> StylableAdapter<StatelessElementAdapter>
where
Self: Sized,
{
StatelessElementAdapter::new(self.into_any()).stylable()
}
}
trait AnyElementState<V> {
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Vector2F;
fn paint(
&mut self,
origin: Vector2F,
visible_bounds: RectF,
view: &mut V,
cx: &mut ViewContext<V>,
);
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF>;
fn debug(&self, view: &V, cx: &ViewContext<V>) -> serde_json::Value;
fn size(&self) -> Vector2F;
fn metadata(&self) -> Option<&dyn Any>;
}
enum ElementState<V: 'static, E: Element<V>> {
Empty,
Init {
element: E,
},
PostLayout {
element: E,
constraint: SizeConstraint,
size: Vector2F,
layout: E::LayoutState,
},
PostPaint {
element: E,
constraint: SizeConstraint,
bounds: RectF,
visible_bounds: RectF,
layout: E::LayoutState,
paint: E::PaintState,
},
}
impl<V, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Vector2F {
let result;
*self = match mem::take(self) {
ElementState::Empty => unreachable!(),
ElementState::Init { mut element }
| ElementState::PostLayout { mut element, .. }
| ElementState::PostPaint { mut element, .. } => {
let (size, layout) = element.layout(constraint, view, cx);
debug_assert!(
size.x().is_finite(),
"Element for {:?} had infinite x size after layout",
element.view_name()
);
debug_assert!(
size.y().is_finite(),
"Element for {:?} had infinite y size after layout",
element.view_name()
);
result = size;
ElementState::PostLayout {
element,
constraint,
size,
layout,
}
}
};
result
}
fn paint(
&mut self,
origin: Vector2F,
visible_bounds: RectF,
view: &mut V,
cx: &mut ViewContext<V>,
) {
*self = match mem::take(self) {
ElementState::PostLayout {
mut element,
constraint,
size,
mut layout,
} => {
let bounds = RectF::new(origin, size);
let paint = element.paint(bounds, visible_bounds, &mut layout, view, cx);
ElementState::PostPaint {
element,
constraint,
bounds,
visible_bounds,
layout,
paint,
}
}
ElementState::PostPaint {
mut element,
constraint,
bounds,
mut layout,
..
} => {
let bounds = RectF::new(origin, bounds.size());
let paint = element.paint(bounds, visible_bounds, &mut layout, view, cx);
ElementState::PostPaint {
element,
constraint,
bounds,
visible_bounds,
layout,
paint,
}
}
ElementState::Empty => panic!("invalid element lifecycle state"),
ElementState::Init { .. } => {
panic!("invalid element lifecycle state, paint called before layout")
}
}
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
if let ElementState::PostPaint {
element,
bounds,
visible_bounds,
layout,
paint,
..
} = self
{
element.rect_for_text_range(
range_utf16,
*bounds,
*visible_bounds,
layout,
paint,
view,
cx,
)
} else {
None
}
}
fn size(&self) -> Vector2F {
match self {
ElementState::Empty | ElementState::Init { .. } => {
panic!("invalid element lifecycle state")
}
ElementState::PostLayout { size, .. } => *size,
ElementState::PostPaint { bounds, .. } => bounds.size(),
}
}
fn metadata(&self) -> Option<&dyn Any> {
match self {
ElementState::Empty => unreachable!(),
ElementState::Init { element }
| ElementState::PostLayout { element, .. }
| ElementState::PostPaint { element, .. } => element.metadata(),
}
}
fn debug(&self, view: &V, cx: &ViewContext<V>) -> serde_json::Value {
match self {
ElementState::PostPaint {
element,
constraint,
bounds,
visible_bounds,
layout,
paint,
} => {
let mut value = element.debug(*bounds, layout, paint, view, 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.insert("bounds".into(), bounds.to_json());
new_map.insert("visible_bounds".into(), visible_bounds.to_json());
new_map.append(map);
json::Value::Object(new_map)
} else {
value
}
}
_ => panic!("invalid element lifecycle state"),
}
}
}
impl<V, E: Element<V>> Default for ElementState<V, E> {
fn default() -> Self {
Self::Empty
}
}
pub struct AnyElement<V> {
state: Box<dyn AnyElementState<V>>,
name: Option<Cow<'static, str>>,
}
impl<V> AnyElement<V> {
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn metadata<T: 'static>(&self) -> Option<&T> {
self.state
.metadata()
.and_then(|data| data.downcast_ref::<T>())
}
pub fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Vector2F {
self.state.layout(constraint, view, cx)
}
pub fn paint(
&mut self,
origin: Vector2F,
visible_bounds: RectF,
view: &mut V,
cx: &mut ViewContext<V>,
) {
self.state.paint(origin, visible_bounds, view, cx);
}
pub fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.state.rect_for_text_range(range_utf16, view, cx)
}
pub fn size(&self) -> Vector2F {
self.state.size()
}
pub fn debug(&self, view: &V, cx: &ViewContext<V>) -> json::Value {
let mut value = self.state.debug(view, 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,
{
f(self.state.metadata().and_then(|m| m.downcast_ref()))
}
}
impl<V: 'static> Element<V> for AnyElement<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let size = self.layout(constraint, view, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
self.paint(bounds.origin(), visible_bounds, view, cx);
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
self.debug(view, cx)
}
fn into_any(self) -> AnyElement<V>
where
Self: Sized,
{
self
}
}
impl Entity for AnyElement<()> {
type Event = ();
}
// impl View for AnyElement<()> {}
pub struct RootElement<V> {
element: AnyElement<V>,
view: WeakViewHandle<V>,
}
impl<V> RootElement<V> {
pub fn new(element: AnyElement<V>, view: WeakViewHandle<V>) -> Self {
Self { element, view }
}
}
pub trait AnyRootElement {
fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F>;
fn paint(
&mut self,
origin: Vector2F,
visible_bounds: RectF,
cx: &mut WindowContext,
) -> Result<()>;
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
cx: &WindowContext,
) -> Result<Option<RectF>>;
fn debug(&self, cx: &WindowContext) -> Result<serde_json::Value>;
fn name(&self) -> Option<&str>;
}
impl<V: View> AnyRootElement for RootElement<V> {
fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F> {
let view = self
.view
.upgrade(cx)
.ok_or_else(|| anyhow!("layout called on a root element for a dropped view"))?;
view.update(cx, |view, cx| Ok(self.element.layout(constraint, view, cx)))
}
fn paint(
&mut self,
origin: Vector2F,
visible_bounds: RectF,
cx: &mut WindowContext,
) -> Result<()> {
let view = self
.view
.upgrade(cx)
.ok_or_else(|| anyhow!("paint called on a root element for a dropped view"))?;
view.update(cx, |view, cx| {
self.element.paint(origin, visible_bounds, view, cx);
Ok(())
})
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
cx: &WindowContext,
) -> Result<Option<RectF>> {
let view = self.view.upgrade(cx).ok_or_else(|| {
anyhow!("rect_for_text_range called on a root element for a dropped view")
})?;
let view = view.read(cx);
let view_context = ViewContext::immutable(cx, self.view.id());
Ok(self
.element
.rect_for_text_range(range_utf16, view, &view_context))
}
fn debug(&self, cx: &WindowContext) -> Result<serde_json::Value> {
let view = self
.view
.upgrade(cx)
.ok_or_else(|| anyhow!("debug called on a root element for a dropped view"))?;
let view = view.read(cx);
let view_context = ViewContext::immutable(cx, self.view.id());
Ok(serde_json::json!({
"view_id": self.view.id(),
"view_name": V::ui_name(),
"view": view.debug_json(cx),
"element": self.element.debug(view, &view_context)
}))
}
fn name(&self) -> Option<&str> {
self.element.name()
}
}
pub trait ParentElement<'a, V: 'static>: Extend<AnyElement<V>> + Sized {
fn add_children<E: Element<V>>(&mut self, children: impl IntoIterator<Item = E>) {
self.extend(children.into_iter().map(|child| child.into_any()));
}
fn add_child<D: Element<V>>(&mut self, child: D) {
self.extend(Some(child.into_any()));
}
fn with_children<D: Element<V>>(mut self, children: impl IntoIterator<Item = D>) -> Self {
self.extend(children.into_iter().map(|child| child.into_any()));
self
}
fn with_child<D: Element<V>>(mut self, child: D) -> Self {
self.extend(Some(child.into_any()));
self
}
}
impl<'a, V, T> ParentElement<'a, V> for T
where
V: 'static,
T: Extend<AnyElement<V>>,
{
}
pub fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
if max_size.x().is_infinite() && max_size.y().is_infinite() {
size
} else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() {
vec2f(size.x() * max_size.y() / size.y(), max_size.y())
} else {
vec2f(max_size.x(), size.y() * max_size.x() / size.x())
}
}

View file

@ -1,115 +0,0 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, AnyElement, Element, SizeConstraint, ViewContext,
};
use json::ToJson;
use serde_json::json;
pub struct Align<V> {
child: AnyElement<V>,
alignment: Vector2F,
}
impl<V> Align<V> {
pub fn new(child: AnyElement<V>) -> Self {
Self {
child,
alignment: Vector2F::zero(),
}
}
pub fn top(mut self) -> Self {
self.alignment.set_y(-1.0);
self
}
pub fn bottom(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<V: 'static> Element<V> for Align<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.max;
constraint.min = Vector2F::zero();
let child_size = self.child.layout(constraint, view, 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,
view: &mut V,
cx: &mut ViewContext<V>,
) -> 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,
view,
cx,
);
}
fn rect_for_text_range(
&self,
range_utf16: std::ops::Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
bounds: pathfinder_geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> json::Value {
json!({
"type": "Align",
"bounds": bounds.to_json(),
"alignment": self.alignment.to_json(),
"child": self.child.debug(view, cx),
})
}
}

View file

@ -1,85 +1,54 @@
use std::marker::PhantomData;
use refineable::Refineable as _;
use super::Element;
use crate::{
json::{self, json},
ViewContext,
};
use json::ToJson;
use pathfinder_geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
};
use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
pub struct Canvas<V, F>(F, PhantomData<V>);
impl<V, F> Canvas<V, F>
where
F: FnMut(RectF, RectF, &mut V, &mut ViewContext<V>),
{
pub fn new(f: F) -> Self {
Self(f, PhantomData)
pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
Canvas {
paint_callback: Some(Box::new(callback)),
style: StyleRefinement::default(),
}
}
impl<V: 'static, F> Element<V> for Canvas<V, F>
where
F: 'static + FnMut(RectF, RectF, &mut V, &mut ViewContext<V>),
{
type LayoutState = ();
type PaintState = ();
pub struct Canvas {
paint_callback: Option<Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>>,
style: StyleRefinement,
}
fn layout(
&mut self,
constraint: crate::SizeConstraint,
_: &mut V,
_: &mut crate::ViewContext<V>,
) -> (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), ())
}
impl IntoElement for Canvas {
type Element = Self;
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
self.0(bounds, visible_bounds, view, cx)
}
fn rect_for_text_range(
&self,
_: std::ops::Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> Option<RectF> {
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> json::Value {
json!({"type": "Canvas", "bounds": bounds.to_json()})
fn into_element(self) -> Self::Element {
self
}
}
impl Element for Canvas {
type State = Style;
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (crate::LayoutId, Self::State) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = cx.request_layout(&style, []);
(layout_id, style)
}
fn paint(&mut self, bounds: Bounds<Pixels>, style: &mut Style, cx: &mut WindowContext) {
style.paint(bounds, cx, |cx| {
(self.paint_callback.take().unwrap())(&bounds, cx)
});
}
}
impl Styled for Canvas {
fn style(&mut self) -> &mut crate::StyleRefinement {
&mut self.style
}
}

View file

@ -1,71 +0,0 @@
use std::ops::Range;
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use serde_json::json;
use crate::{json, AnyElement, Element, SizeConstraint, ViewContext};
pub struct Clipped<V> {
child: AnyElement<V>,
}
impl<V> Clipped<V> {
pub fn new(child: AnyElement<V>) -> Self {
Self { child }
}
}
impl<V: 'static> Element<V> for Clipped<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, view, cx), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
cx.scene().push_layer(Some(bounds));
let state = self.child.paint(bounds.origin(), visible_bounds, view, cx);
cx.scene().pop_layer();
state
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> json::Value {
json!({
"type": "Clipped",
"child": self.child.debug(view, cx)
})
}
}

View file

@ -1,342 +0,0 @@
use std::{any::Any, marker::PhantomData};
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use crate::{AnyElement, Element, SizeConstraint, ViewContext};
use super::Empty;
/// The core stateless component trait, simply rendering an element tree
pub trait Component {
fn render<V: 'static>(self, cx: &mut ViewContext<V>) -> AnyElement<V>;
fn element<V: 'static>(self) -> ComponentAdapter<V, Self>
where
Self: Sized,
{
ComponentAdapter::new(self)
}
fn stylable(self) -> StylableAdapter<Self>
where
Self: Sized,
{
StylableAdapter::new(self)
}
fn stateful<V: 'static>(self) -> StatefulAdapter<Self, V>
where
Self: Sized,
{
StatefulAdapter::new(self)
}
}
/// Allows a a component's styles to be rebound in a simple way.
pub trait Stylable: Component {
type Style: Clone;
fn with_style(self, style: Self::Style) -> Self;
}
/// This trait models the typestate pattern for a component's style,
/// enforcing at compile time that a component is only usable after
/// it has been styled while still allowing for late binding of the
/// styling information
pub trait SafeStylable {
type Style: Clone;
type Output: Component;
fn with_style(self, style: Self::Style) -> Self::Output;
}
/// All stylable components can trivially implement SafeStylable
impl<C: Stylable> SafeStylable for C {
type Style = C::Style;
type Output = C;
fn with_style(self, style: Self::Style) -> Self::Output {
self.with_style(style)
}
}
/// Allows converting an unstylable component into a stylable one
/// by using `()` as the style type
pub struct StylableAdapter<C: Component> {
component: C,
}
impl<C: Component> StylableAdapter<C> {
pub fn new(component: C) -> Self {
Self { component }
}
}
impl<C: Component> SafeStylable for StylableAdapter<C> {
type Style = ();
type Output = C;
fn with_style(self, _: Self::Style) -> Self::Output {
self.component
}
}
/// This is a secondary trait for components that can be styled
/// which rely on their view's state. This is useful for components that, for example,
/// want to take click handler callbacks Unfortunately, the generic bound on the
/// Component trait makes it incompatible with the stateless components above.
// So let's just replicate them for now
pub trait StatefulComponent<V: 'static> {
fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
fn element(self) -> ComponentAdapter<V, Self>
where
Self: Sized,
{
ComponentAdapter::new(self)
}
fn styleable(self) -> StatefulStylableAdapter<Self, V>
where
Self: Sized,
{
StatefulStylableAdapter::new(self)
}
fn stateless(self) -> StatelessElementAdapter
where
Self: Sized + 'static,
{
StatelessElementAdapter::new(self.element().into_any())
}
}
/// It is trivial to convert stateless components to stateful components, so lets
/// do so en masse. Note that the reverse is impossible without a helper.
impl<V: 'static, C: Component> StatefulComponent<V> for C {
fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
self.render(cx)
}
}
/// Same as stylable, but generic over a view type
pub trait StatefulStylable<V: 'static>: StatefulComponent<V> {
type Style: Clone;
fn with_style(self, style: Self::Style) -> Self;
}
/// Same as SafeStylable, but generic over a view type
pub trait StatefulSafeStylable<V: 'static> {
type Style: Clone;
type Output: StatefulComponent<V>;
fn with_style(self, style: Self::Style) -> Self::Output;
}
/// Converting from stateless to stateful
impl<V: 'static, C: SafeStylable> StatefulSafeStylable<V> for C {
type Style = C::Style;
type Output = C::Output;
fn with_style(self, style: Self::Style) -> Self::Output {
self.with_style(style)
}
}
// A helper for converting stateless components into stateful ones
pub struct StatefulAdapter<C, V> {
component: C,
phantom: std::marker::PhantomData<V>,
}
impl<C: Component, V: 'static> StatefulAdapter<C, V> {
pub fn new(component: C) -> Self {
Self {
component,
phantom: std::marker::PhantomData,
}
}
}
impl<C: Component, V: 'static> StatefulComponent<V> for StatefulAdapter<C, V> {
fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
self.component.render(cx)
}
}
// A helper for converting stateful but style-less components into stylable ones
// by using `()` as the style type
pub struct StatefulStylableAdapter<C: StatefulComponent<V>, V: 'static> {
component: C,
phantom: std::marker::PhantomData<V>,
}
impl<C: StatefulComponent<V>, V: 'static> StatefulStylableAdapter<C, V> {
pub fn new(component: C) -> Self {
Self {
component,
phantom: std::marker::PhantomData,
}
}
}
impl<C: StatefulComponent<V>, V: 'static> StatefulSafeStylable<V>
for StatefulStylableAdapter<C, V>
{
type Style = ();
type Output = C;
fn with_style(self, _: Self::Style) -> Self::Output {
self.component
}
}
/// A way of erasing the view generic from an element, useful
/// for wrapping up an explicit element tree into stateless
/// components
pub struct StatelessElementAdapter {
element: Box<dyn Any>,
}
impl StatelessElementAdapter {
pub fn new<V: 'static>(element: AnyElement<V>) -> Self {
StatelessElementAdapter {
element: Box::new(element) as Box<dyn Any>,
}
}
}
impl Component for StatelessElementAdapter {
fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
*self
.element
.downcast::<AnyElement<V>>()
.expect("Don't move elements out of their view :(")
}
}
// For converting elements into stateful components
pub struct StatefulElementAdapter<V: 'static> {
element: AnyElement<V>,
_phantom: std::marker::PhantomData<V>,
}
impl<V: 'static> StatefulElementAdapter<V> {
pub fn new(element: AnyElement<V>) -> Self {
Self {
element,
_phantom: std::marker::PhantomData,
}
}
}
impl<V: 'static> StatefulComponent<V> for StatefulElementAdapter<V> {
fn render(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
self.element
}
}
/// A convenient shorthand for creating an empty component.
impl Component for () {
fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
Empty::new().into_any()
}
}
impl Stylable for () {
type Style = ();
fn with_style(self, _: Self::Style) -> Self {
()
}
}
// For converting components back into Elements
pub struct ComponentAdapter<V: 'static, E> {
component: Option<E>,
element: Option<AnyElement<V>>,
phantom: PhantomData<V>,
}
impl<E, V: 'static> ComponentAdapter<V, E> {
pub fn new(e: E) -> Self {
Self {
component: Some(e),
element: None,
phantom: PhantomData,
}
}
}
impl<V: 'static, C: StatefulComponent<V> + 'static> Element<V> for ComponentAdapter<V, C> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
if self.element.is_none() {
let element = self
.component
.take()
.expect("Component can only be rendered once")
.render(view, cx);
self.element = Some(element);
}
let constraint = self.element.as_mut().unwrap().layout(constraint, view, cx);
(constraint, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
self.element
.as_mut()
.expect("Layout should always be called before paint")
.paint(bounds.origin(), visible_bounds, view, cx)
}
fn rect_for_text_range(
&self,
range_utf16: std::ops::Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.element
.as_ref()
.and_then(|el| el.rect_for_text_range(range_utf16, view, cx))
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
serde_json::json!({
"type": "ComponentAdapter",
"component": std::any::type_name::<C>(),
"child": self.element.as_ref().map(|el| el.debug(view, cx)),
})
}
}

View file

@ -1,187 +0,0 @@
use std::ops::Range;
use json::ToJson;
use serde_json::json;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, AnyElement, Element, SizeConstraint, ViewContext,
};
pub struct ConstrainedBox<V> {
child: AnyElement<V>,
constraint: Constraint<V>,
}
pub enum Constraint<V> {
Static(SizeConstraint),
Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint>),
}
impl<V> ToJson for Constraint<V> {
fn to_json(&self) -> serde_json::Value {
match self {
Constraint::Static(constraint) => constraint.to_json(),
Constraint::Dynamic(_) => "dynamic".into(),
}
}
}
impl<V: 'static> ConstrainedBox<V> {
pub fn new(child: impl Element<V>) -> Self {
Self {
child: child.into_any(),
constraint: Constraint::Static(Default::default()),
}
}
pub fn dynamically(
mut self,
constraint: impl 'static + FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint,
) -> Self {
self.constraint = Constraint::Dynamic(Box::new(constraint));
self
}
pub fn with_min_width(mut self, min_width: f32) -> Self {
if let Constraint::Dynamic(_) = self.constraint {
self.constraint = Constraint::Static(Default::default());
}
if let Constraint::Static(constraint) = &mut self.constraint {
constraint.min.set_x(min_width);
} else {
unreachable!()
}
self
}
pub fn with_max_width(mut self, max_width: f32) -> Self {
if let Constraint::Dynamic(_) = self.constraint {
self.constraint = Constraint::Static(Default::default());
}
if let Constraint::Static(constraint) = &mut self.constraint {
constraint.max.set_x(max_width);
} else {
unreachable!()
}
self
}
pub fn with_max_height(mut self, max_height: f32) -> Self {
if let Constraint::Dynamic(_) = self.constraint {
self.constraint = Constraint::Static(Default::default());
}
if let Constraint::Static(constraint) = &mut self.constraint {
constraint.max.set_y(max_height);
} else {
unreachable!()
}
self
}
pub fn with_width(mut self, width: f32) -> Self {
if let Constraint::Dynamic(_) = self.constraint {
self.constraint = Constraint::Static(Default::default());
}
if let Constraint::Static(constraint) = &mut self.constraint {
constraint.min.set_x(width);
constraint.max.set_x(width);
} else {
unreachable!()
}
self
}
pub fn with_height(mut self, height: f32) -> Self {
if let Constraint::Dynamic(_) = self.constraint {
self.constraint = Constraint::Static(Default::default());
}
if let Constraint::Static(constraint) = &mut self.constraint {
constraint.min.set_y(height);
constraint.max.set_y(height);
} else {
unreachable!()
}
self
}
fn constraint(
&mut self,
input_constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> SizeConstraint {
match &mut self.constraint {
Constraint::Static(constraint) => *constraint,
Constraint::Dynamic(compute_constraint) => {
compute_constraint(input_constraint, view, cx)
}
}
}
}
impl<V: 'static> Element<V> for ConstrainedBox<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut parent_constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let constraint = self.constraint(parent_constraint, view, cx);
parent_constraint.min = parent_constraint.min.max(constraint.min);
parent_constraint.max = parent_constraint.max.min(constraint.max);
parent_constraint.max = parent_constraint.max.max(parent_constraint.min);
let size = self.child.layout(parent_constraint, view, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
cx.scene().push_layer(Some(visible_bounds));
self.child.paint(bounds.origin(), visible_bounds, view, cx);
cx.scene().pop_layer();
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> json::Value {
json!({"type": "ConstrainedBox", "assigned_constraint": self.constraint.to_json(), "child": self.child.debug(view, cx)})
}
}

View file

@ -1,684 +0,0 @@
use std::ops::Range;
use crate::{
color::Color,
geometry::{
deserialize_vec2f,
rect::RectF,
vector::{vec2f, Vector2F},
},
json::ToJson,
platform::CursorStyle,
scene::{self, CornerRadii, CursorRegion, Quad},
AnyElement, Element, SizeConstraint, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
pub struct ContainerStyle {
#[serde(default)]
pub margin: Margin,
#[serde(default)]
pub padding: Padding,
#[serde(rename = "background")]
pub background_color: Option<Color>,
#[serde(rename = "overlay")]
pub overlay_color: Option<Color>,
#[serde(default)]
pub border: Border,
#[serde(default)]
#[serde(alias = "corner_radius")]
pub corner_radii: CornerRadii,
#[serde(default)]
pub shadow: Option<Shadow>,
#[serde(default)]
pub cursor: Option<CursorStyle>,
}
impl ContainerStyle {
pub fn fill(color: Color) -> Self {
Self {
background_color: Some(color),
..Default::default()
}
}
pub fn additional_length(&self) -> f32 {
self.padding.left
+ self.padding.right
+ self.border.width * 2.
+ self.margin.left
+ self.margin.right
}
}
pub struct Container<V> {
child: AnyElement<V>,
style: ContainerStyle,
}
impl<V> Container<V> {
pub fn new(child: AnyElement<V>) -> 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_bottom(mut self, margin: f32) -> Self {
self.style.margin.bottom = 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_top(mut self, padding: f32) -> Self {
self.style.padding.top = 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_overlay_color(mut self, color: Color) -> Self {
self.style.overlay_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_radii.top_left = radius;
self.style.corner_radii.top_right = radius;
self.style.corner_radii.bottom_right = radius;
self.style.corner_radii.bottom_left = radius;
self
}
pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: Color) -> Self {
self.style.shadow = Some(Shadow {
offset,
blur,
color,
});
self
}
pub fn with_cursor(mut self, style: CursorStyle) -> Self {
self.style.cursor = Some(style);
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)
}
}
#[derive(Copy, Clone, Debug, Default, JsonSchema)]
pub struct Border {
pub color: Color,
pub width: f32,
pub overlay: bool,
pub top: bool,
pub bottom: bool,
pub left: bool,
pub right: bool,
}
impl Into<scene::Border> for Border {
fn into(self) -> scene::Border {
scene::Border {
color: self.color,
left: if self.left { self.width } else { 0.0 },
right: if self.right { self.width } else { 0.0 },
top: if self.top { self.width } else { 0.0 },
bottom: if self.bottom { self.width } else { 0.0 },
}
}
}
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<'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)
}
}
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
}
}
impl<V: 'static> Element<V> for Container<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (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, view, cx);
(child_size + size_buffer, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> 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_radii: self.style.corner_radii,
sigma: shadow.blur,
color: shadow.color,
});
}
if let Some(hit_bounds) = quad_bounds.intersection(visible_bounds) {
if let Some(style) = self.style.cursor {
cx.scene().push_cursor_region(CursorRegion {
bounds: hit_bounds,
style,
});
}
}
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_radii: self.style.corner_radii.into(),
});
self.child.paint(child_origin, visible_bounds, view, cx);
cx.scene().push_layer(None);
cx.scene().push_quad(Quad {
bounds: quad_bounds,
background: self.style.overlay_color,
border: self.style.border.into(),
corner_radii: self.style.corner_radii.into(),
});
cx.scene().pop_layer();
} else {
cx.scene().push_quad(Quad {
bounds: quad_bounds,
background: self.style.background_color,
border: self.style.border.into(),
corner_radii: self.style.corner_radii.into(),
});
let child_origin = child_origin
+ vec2f(
self.style.border.left_width(),
self.style.border.top_width(),
);
self.child.paint(child_origin, visible_bounds, view, cx);
if self.style.overlay_color.is_some() {
cx.scene().push_layer(None);
cx.scene().push_quad(Quad {
bounds: quad_bounds,
background: self.style.overlay_color,
border: Default::default(),
corner_radii: self.style.corner_radii.into(),
});
cx.scene().pop_layer();
}
}
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
json!({
"type": "Container",
"bounds": bounds.to_json(),
"details": self.style.to_json(),
"child": self.child.debug(view, 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_radii,
"shadow": self.shadow.to_json(),
})
}
}
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
pub struct Margin {
pub top: f32,
pub bottom: f32,
pub left: 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, JsonSchema)]
pub struct Padding {
pub top: f32,
pub left: f32,
pub bottom: f32,
pub right: f32,
}
impl Padding {
pub fn horizontal(padding: f32) -> Self {
Self {
left: padding,
right: padding,
..Default::default()
}
}
pub fn vertical(padding: f32) -> Self {
Self {
top: padding,
bottom: padding,
..Default::default()
}
}
}
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, JsonSchema)]
pub struct Shadow {
#[serde(default, deserialize_with = "deserialize_vec2f")]
#[schemars(with = "Vec::<f32>")]
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()
})
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,89 +0,0 @@
use std::ops::Range;
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
ViewContext,
};
use crate::{Element, SizeConstraint};
#[derive(Default)]
pub struct Empty {
collapsed: bool,
}
impl Empty {
pub fn new() -> Self {
Self::default()
}
pub fn collapsed(mut self) -> Self {
self.collapsed = true;
self
}
}
impl<V: 'static> Element<V> for Empty {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
_: &mut V,
_: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let x = if constraint.max.x().is_finite() && !self.collapsed {
constraint.max.x()
} else {
constraint.min.x()
};
let y = if constraint.max.y().is_finite() && !self.collapsed {
constraint.max.y()
} else {
constraint.min.y()
};
(vec2f(x, y), ())
}
fn paint(
&mut self,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut V,
_: &mut ViewContext<V>,
) -> Self::PaintState {
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> serde_json::Value {
json!({
"type": "Empty",
"bounds": bounds.to_json(),
})
}
}

View file

@ -1,96 +0,0 @@
use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, AnyElement, Element, SizeConstraint, ViewContext,
};
use serde_json::json;
pub struct Expanded<V> {
child: AnyElement<V>,
full_width: bool,
full_height: bool,
}
impl<V: 'static> Expanded<V> {
pub fn new(child: impl Element<V>) -> Self {
Self {
child: child.into_any(),
full_width: true,
full_height: true,
}
}
pub fn full_width(mut self) -> Self {
self.full_width = true;
self.full_height = false;
self
}
pub fn full_height(mut self) -> Self {
self.full_width = false;
self.full_height = true;
self
}
}
impl<V: 'static> Element<V> for Expanded<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
if self.full_width {
constraint.min.set_x(constraint.max.x());
}
if self.full_height {
constraint.min.set_y(constraint.max.y());
}
let size = self.child.layout(constraint, view, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, view, cx);
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> json::Value {
json!({
"type": "Expanded",
"full_width": self.full_width,
"full_height": self.full_height,
"child": self.child.debug(view, cx)
})
}
}

View file

@ -1,512 +0,0 @@
use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
use crate::{
json::{self, ToJson, Value},
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt,
ViewContext,
};
use pathfinder_geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
};
use serde_json::json;
struct ScrollState {
scroll_to: Cell<Option<usize>>,
scroll_position: Cell<f32>,
type_tag: TypeTag,
}
pub struct Flex<V> {
axis: Axis,
children: Vec<AnyElement<V>>,
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
child_alignment: f32,
spacing: f32,
}
impl<V: 'static> Flex<V> {
pub fn new(axis: Axis) -> Self {
Self {
axis,
children: Default::default(),
scroll_state: None,
child_alignment: -1.,
spacing: 0.,
}
}
pub fn row() -> Self {
Self::new(Axis::Horizontal)
}
pub fn column() -> Self {
Self::new(Axis::Vertical)
}
/// Render children centered relative to the cross-axis of the parent flex.
///
/// If this is a flex row, children will be centered vertically. If this is a
/// flex column, children will be centered horizontally.
pub fn align_children_center(mut self) -> Self {
self.child_alignment = 0.;
self
}
pub fn with_spacing(mut self, spacing: f32) -> Self {
self.spacing = spacing;
self
}
pub fn scrollable<Tag>(
mut self,
element_id: usize,
scroll_to: Option<usize>,
cx: &mut ViewContext<V>,
) -> Self
where
Tag: 'static,
{
// Don't assume that this initialization is what scroll_state really is in other panes:
// `element_state` is shared and there could be init races.
let scroll_state = cx.element_state::<Tag, Rc<ScrollState>>(
element_id,
Rc::new(ScrollState {
type_tag: TypeTag::new::<Tag>(),
scroll_to: Default::default(),
scroll_position: Default::default(),
}),
);
// Set scroll_to separately, because the default state is already picked as `None` by other panes
// by the time we start setting it here, hence update all others' state too.
scroll_state.update(cx, |this, _| {
this.scroll_to.set(scroll_to);
});
self.scroll_state = Some((scroll_state, cx.handle().id()));
self
}
pub fn is_empty(&self) -> bool {
self.children.is_empty()
}
fn layout_flex_children(
&mut self,
layout_expanded: bool,
constraint: SizeConstraint,
remaining_space: &mut f32,
remaining_flex: &mut f32,
cross_axis_max: &mut f32,
view: &mut V,
cx: &mut ViewContext<V>,
) {
let cross_axis = self.axis.invert();
for child in self.children.iter_mut() {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if let Some((flex, expanded)) = metadata.flex {
if expanded != layout_expanded {
continue;
}
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, view, cx);
*remaining_space -= child_size.along(self.axis);
*remaining_flex -= flex;
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
}
}
}
}
}
impl<V> Extend<AnyElement<V>> for Flex<V> {
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
self.children.extend(children);
}
}
impl<V: 'static> Element<V> for Flex<V> {
type LayoutState = f32;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let mut total_flex = None;
let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing;
let mut contains_float = false;
let cross_axis = self.axis.invert();
let mut cross_axis_max: f32 = 0.0;
for child in self.children.iter_mut() {
let metadata = child.metadata::<FlexParentData>();
contains_float |= metadata.map_or(false, |metadata| metadata.float);
if let Some(flex) = metadata.and_then(|metadata| metadata.flex.map(|(flex, _)| flex)) {
*total_flex.get_or_insert(0.) += 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, view, cx);
fixed_space += size.along(self.axis);
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
}
}
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
let mut size = if let Some(mut remaining_flex) = total_flex {
if remaining_space.is_infinite() {
panic!("flex contains flexible children but has an infinite constraint along the flex axis");
}
self.layout_flex_children(
false,
constraint,
&mut remaining_space,
&mut remaining_flex,
&mut cross_axis_max,
view,
cx,
);
self.layout_flex_children(
true,
constraint,
&mut remaining_space,
&mut remaining_flex,
&mut cross_axis_max,
view,
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 contains_float {
match self.axis {
Axis::Horizontal => size.set_x(size.x().max(constraint.max.x())),
Axis::Vertical => size.set_y(size.y().max(constraint.max.y())),
}
}
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()));
}
if size.x() > constraint.max.x() {
size.set_x(constraint.max.x());
}
if size.y() > constraint.max.y() {
size.set_y(constraint.max.y());
}
if let Some(scroll_state) = self.scroll_state.as_ref() {
scroll_state.0.update(cx, |scroll_state, _| {
if let Some(scroll_to) = scroll_state.scroll_to.take() {
let visible_start = scroll_state.scroll_position.get();
let visible_end = visible_start + size.along(self.axis);
if let Some(child) = self.children.get(scroll_to) {
let child_start: f32 = self.children[..scroll_to]
.iter()
.map(|c| c.size().along(self.axis))
.sum();
let child_end = child_start + child.size().along(self.axis);
if child_start < visible_start {
scroll_state.scroll_position.set(child_start);
} else if child_end > visible_end {
scroll_state
.scroll_position
.set(child_end - size.along(self.axis));
}
}
}
scroll_state.scroll_position.set(
scroll_state
.scroll_position
.get()
.min(-remaining_space)
.max(0.),
);
});
}
(size, remaining_space)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
remaining_space: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
let mut remaining_space = *remaining_space;
let overflowing = remaining_space < 0.;
if overflowing {
cx.scene().push_layer(Some(visible_bounds));
}
if let Some((scroll_state, id)) = &self.scroll_state {
let scroll_state = scroll_state.read(cx).clone();
cx.scene().push_mouse_region(
crate::MouseRegion::from_handlers(
scroll_state.type_tag,
*id,
0,
bounds,
Default::default(),
)
.on_scroll({
let axis = self.axis;
move |e, _: &mut V, cx| {
if remaining_space < 0. {
let scroll_delta = e.delta.raw();
let mut delta = match axis {
Axis::Horizontal => {
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
scroll_delta.x()
} else {
scroll_delta.y()
}
}
Axis::Vertical => scroll_delta.y(),
};
if !e.delta.precise() {
delta *= 20.;
}
scroll_state
.scroll_position
.set(scroll_state.scroll_position.get() - delta);
cx.notify();
} else {
cx.propagate_event();
}
}
})
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
)
}
let mut child_origin = bounds.origin();
if let Some(scroll_state) = self.scroll_state.as_ref() {
let scroll_position = scroll_state.0.read(cx).scroll_position.get();
match self.axis {
Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
}
}
for child in self.children.iter_mut() {
if remaining_space > 0. {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if metadata.float {
match self.axis {
Axis::Horizontal => child_origin += vec2f(remaining_space, 0.0),
Axis::Vertical => child_origin += vec2f(0.0, remaining_space),
}
remaining_space = 0.;
}
}
}
// We use the child_alignment f32 to determine a point along the cross axis of the
// overall flex element and each child. We then align these points. So 0 would center
// each child relative to the overall height/width of the flex. -1 puts children at
// the start. 1 puts children at the end.
let aligned_child_origin = {
let cross_axis = self.axis.invert();
let my_center = bounds.size().along(cross_axis) / 2.;
let my_target = my_center + my_center * self.child_alignment;
let child_center = child.size().along(cross_axis) / 2.;
let child_target = child_center + child_center * self.child_alignment;
let mut aligned_child_origin = child_origin;
match self.axis {
Axis::Horizontal => aligned_child_origin
.set_y(aligned_child_origin.y() - (child_target - my_target)),
Axis::Vertical => aligned_child_origin
.set_x(aligned_child_origin.x() - (child_target - my_target)),
}
aligned_child_origin
};
child.paint(aligned_child_origin, visible_bounds, view, cx);
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing),
}
}
if overflowing {
cx.scene().pop_layer();
}
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.children
.iter()
.find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> json::Value {
json!({
"type": "Flex",
"bounds": bounds.to_json(),
"axis": self.axis.to_json(),
"children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
})
}
}
struct FlexParentData {
flex: Option<(f32, bool)>,
float: bool,
}
pub struct FlexItem<V> {
metadata: FlexParentData,
child: AnyElement<V>,
}
impl<V: 'static> FlexItem<V> {
pub fn new(child: impl Element<V>) -> Self {
FlexItem {
metadata: FlexParentData {
flex: None,
float: false,
},
child: child.into_any(),
}
}
pub fn flex(mut self, flex: f32, expanded: bool) -> Self {
self.metadata.flex = Some((flex, expanded));
self
}
pub fn float(mut self) -> Self {
self.metadata.float = true;
self
}
}
impl<V: 'static> Element<V> for FlexItem<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, view, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, view, cx)
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn metadata(&self) -> Option<&dyn Any> {
Some(&self.metadata)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Value {
json!({
"type": "Flexible",
"flex": self.metadata.flex,
"child": self.child.debug(view, cx)
})
}
}

View file

@ -1,85 +0,0 @@
use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::json,
AnyElement, Element, SizeConstraint, ViewContext,
};
pub struct Hook<V> {
child: AnyElement<V>,
after_layout: Option<Box<dyn FnMut(Vector2F, &mut ViewContext<V>)>>,
}
impl<V: 'static> Hook<V> {
pub fn new(child: impl Element<V>) -> Self {
Self {
child: child.into_any(),
after_layout: None,
}
}
pub fn on_after_layout(
mut self,
f: impl 'static + FnMut(Vector2F, &mut ViewContext<V>),
) -> Self {
self.after_layout = Some(Box::new(f));
self
}
}
impl<V: 'static> Element<V> for Hook<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, view, 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,
view: &mut V,
cx: &mut ViewContext<V>,
) {
self.child.paint(bounds.origin(), visible_bounds, view, cx);
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
json!({
"type": "Hooks",
"child": self.child.debug(view, cx),
})
}
}

View file

@ -1,137 +0,0 @@
use super::{constrain_size_preserving_aspect_ratio, Border};
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
scene, Element, ImageData, SizeConstraint, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use std::{ops::Range, sync::Arc};
enum ImageSource {
Path(&'static str),
Data(Arc<ImageData>),
}
pub struct Image {
source: ImageSource,
style: ImageStyle,
}
#[derive(Copy, Clone, Default, Deserialize, JsonSchema)]
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>,
#[serde(default)]
pub grayscale: bool,
}
impl Image {
pub fn new(asset_path: &'static str) -> Self {
Self {
source: ImageSource::Path(asset_path),
style: Default::default(),
}
}
pub fn from_data(data: Arc<ImageData>) -> Self {
Self {
source: ImageSource::Data(data),
style: Default::default(),
}
}
pub fn with_style(mut self, style: ImageStyle) -> Self {
self.style = style;
self
}
}
impl<V: 'static> Element<V> for Image {
type LayoutState = Option<Arc<ImageData>>;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
_: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let data = match &self.source {
ImageSource::Path(path) => match cx.asset_cache.png(path) {
Ok(data) => data,
Err(error) => {
log::error!("could not load image: {}", error);
return (Vector2F::zero(), None);
}
},
ImageSource::Data(data) => data.clone(),
};
let desired_size = vec2f(
self.style.width.unwrap_or_else(|| constraint.max.x()),
self.style.height.unwrap_or_else(|| constraint.max.y()),
);
let size = constrain_size_preserving_aspect_ratio(
constraint.constrain(desired_size),
data.size().to_f32(),
);
(size, Some(data))
}
fn paint(
&mut self,
bounds: RectF,
_: RectF,
layout: &mut Self::LayoutState,
_: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
if let Some(data) = layout {
cx.scene().push_image(scene::Image {
bounds,
border: self.style.border.into(),
corner_radii: self.style.corner_radius.into(),
grayscale: self.style.grayscale,
data: data.clone(),
});
}
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> serde_json::Value {
json!({
"type": "Image",
"bounds": bounds.to_json(),
})
}
}

View file

@ -0,0 +1,186 @@
use std::sync::Arc;
use crate::{
point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement,
InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size,
StyleRefinement, Styled, WindowContext,
};
use futures::FutureExt;
use media::core_video::CVImageBuffer;
use util::ResultExt;
#[derive(Clone, Debug)]
pub enum ImageSource {
/// Image content will be loaded from provided URI at render time.
Uri(SharedString),
Data(Arc<ImageData>),
Surface(CVImageBuffer),
}
impl From<SharedString> for ImageSource {
fn from(value: SharedString) -> Self {
Self::Uri(value)
}
}
impl From<&'static str> for ImageSource {
fn from(uri: &'static str) -> Self {
Self::Uri(uri.into())
}
}
impl From<String> for ImageSource {
fn from(uri: String) -> Self {
Self::Uri(uri.into())
}
}
impl From<Arc<ImageData>> for ImageSource {
fn from(value: Arc<ImageData>) -> Self {
Self::Data(value)
}
}
impl From<CVImageBuffer> for ImageSource {
fn from(value: CVImageBuffer) -> Self {
Self::Surface(value)
}
}
pub struct Img {
interactivity: Interactivity,
source: ImageSource,
grayscale: bool,
}
pub fn img(source: impl Into<ImageSource>) -> Img {
Img {
interactivity: Interactivity::default(),
source: source.into(),
grayscale: false,
}
}
impl Img {
pub fn grayscale(mut self, grayscale: bool) -> Self {
self.grayscale = grayscale;
self
}
}
impl Element for Img {
type State = InteractiveElementState;
fn request_layout(
&mut self,
element_state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
self.interactivity
.layout(element_state, cx, |style, cx| cx.request_layout(&style, []))
}
fn paint(
&mut self,
bounds: Bounds<Pixels>,
element_state: &mut Self::State,
cx: &mut WindowContext,
) {
let source = self.source.clone();
self.interactivity.paint(
bounds,
bounds.size,
element_state,
cx,
|style, _scroll_offset, cx| {
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
cx.with_z_index(1, |cx| {
match source {
ImageSource::Uri(uri) => {
let image_future = cx.image_cache.get(uri.clone());
if let Some(data) = image_future
.clone()
.now_or_never()
.and_then(|result| result.ok())
{
let new_bounds = preserve_aspect_ratio(bounds, data.size());
cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
.log_err();
} else {
cx.spawn(|mut cx| async move {
if image_future.await.ok().is_some() {
cx.on_next_frame(|cx| cx.notify());
}
})
.detach();
}
}
ImageSource::Data(data) => {
let new_bounds = preserve_aspect_ratio(bounds, data.size());
cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
.log_err();
}
ImageSource::Surface(surface) => {
let size = size(surface.width().into(), surface.height().into());
let new_bounds = preserve_aspect_ratio(bounds, size);
// TODO: Add support for corner_radii and grayscale.
cx.paint_surface(new_bounds, surface);
}
};
});
},
)
}
}
impl IntoElement for Img {
type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
self.interactivity.element_id.clone()
}
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for Img {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl InteractiveElement for Img {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
fn preserve_aspect_ratio(bounds: Bounds<Pixels>, image_size: Size<DevicePixels>) -> Bounds<Pixels> {
let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
let image_ratio = image_size.width / image_size.height;
let bounds_ratio = bounds.size.width / bounds.size.height;
let new_size = if bounds_ratio > image_ratio {
size(
image_size.width * (bounds.size.height / image_size.height),
bounds.size.height,
)
} else {
size(
bounds.size.width,
image_size.height * (bounds.size.width / image_size.width),
)
};
Bounds {
origin: point(
bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
),
size: new_size,
}
}

View file

@ -1,100 +0,0 @@
use crate::{
elements::*,
fonts::TextStyle,
geometry::{rect::RectF, vector::Vector2F},
Action, AnyElement, SizeConstraint,
};
use serde_json::json;
use super::ContainerStyle;
pub struct KeystrokeLabel {
action: Box<dyn Action>,
container_style: ContainerStyle,
text_style: TextStyle,
view_id: usize,
}
impl KeystrokeLabel {
pub fn new(
view_id: usize,
action: Box<dyn Action>,
container_style: ContainerStyle,
text_style: TextStyle,
) -> Self {
Self {
view_id,
action,
container_style,
text_style,
}
}
}
impl<V: 'static> Element<V> for KeystrokeLabel {
type LayoutState = AnyElement<V>;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, AnyElement<V>) {
let mut element = if let Some(keystrokes) =
cx.keystrokes_for_action(self.view_id, self.action.as_ref())
{
Flex::row()
.with_children(keystrokes.iter().map(|keystroke| {
Label::new(keystroke.to_string(), self.text_style.clone())
.contained()
.with_style(self.container_style)
}))
.into_any()
} else {
Empty::new().collapsed().into_any()
};
let size = element.layout(constraint, view, cx);
(size, element)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
element: &mut AnyElement<V>,
view: &mut V,
cx: &mut ViewContext<V>,
) {
element.paint(bounds.origin(), visible_bounds, view, cx);
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> Option<RectF> {
None
}
fn debug(
&self,
_: RectF,
element: &AnyElement<V>,
_: &(),
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
json!({
"type": "KeystrokeLabel",
"action": self.action.name(),
"child": element.debug(view, cx)
})
}
}

View file

@ -1,280 +0,0 @@
use std::{borrow::Cow, ops::Range};
use crate::{
fonts::TextStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{ToJson, Value},
text_layout::{Line, RunStyle},
Element, SizeConstraint, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
use smallvec::{smallvec, SmallVec};
pub struct Label {
text: Cow<'static, str>,
style: LabelStyle,
highlight_indices: Vec<usize>,
}
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
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 LabelStyle {
pub fn with_font_size(mut self, font_size: f32) -> Self {
self.text.font_size = font_size;
self
}
}
impl Label {
pub fn new<I: Into<Cow<'static, str>>>(text: I, style: impl Into<LabelStyle>) -> Self {
Self {
text: text.into(),
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<V: 'static> Element<V> for Label {
type LayoutState = Line;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
_: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let runs = self.compute_runs();
let line = cx.text_layout_cache().layout_str(
&self.text,
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_size),
);
(size, line)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
line: &mut Self::LayoutState,
_: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> 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::AppContext) {
let default_style = TextStyle::new(
"Menlo",
12.,
Default::default(),
Default::default(),
Default::default(),
Color::black(),
cx.font_cache(),
)
.unwrap();
let highlight_style = TextStyle::new(
"Menlo",
12.,
*FontProperties::new().weight(Weight::BOLD),
Default::default(),
Default::default(),
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),
]
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
mod canvas;
mod div;
mod img;
mod list;
mod overlay;
mod svg;
mod text;
mod uniform_list;
pub use canvas::*;
pub use div::*;
pub use img::*;
pub use list::*;
pub use overlay::*;
pub use svg::*;
pub use text::*;
pub use uniform_list::*;

View file

@ -1,323 +0,0 @@
use super::Padding;
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
platform::CursorStyle,
platform::MouseButton,
scene::{
CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
},
AnyElement, Element, EventContext, MouseRegion, MouseState, SizeConstraint, TypeTag,
ViewContext,
};
use serde_json::json;
use std::ops::Range;
pub struct MouseEventHandler<V: 'static> {
child: AnyElement<V>,
region_id: usize,
cursor_style: Option<CursorStyle>,
handlers: HandlerSet,
hoverable: bool,
notify_on_hover: bool,
notify_on_click: bool,
above: bool,
padding: Padding,
tag: TypeTag,
}
/// Element which provides a render_child callback with a MouseState and paints a mouse
/// region under (or above) it for easy mouse event handling.
impl<V: 'static> MouseEventHandler<V> {
pub fn for_child<Tag: 'static>(child: impl Element<V>, region_id: usize) -> Self {
Self {
child: child.into_any(),
region_id,
cursor_style: None,
handlers: Default::default(),
notify_on_hover: false,
notify_on_click: false,
hoverable: false,
above: false,
padding: Default::default(),
tag: TypeTag::new::<Tag>(),
}
}
pub fn new<Tag: 'static, E>(
region_id: usize,
cx: &mut ViewContext<V>,
render_child: impl FnOnce(&mut MouseState, &mut ViewContext<V>) -> E,
) -> Self
where
E: Element<V>,
{
let mut mouse_state = cx.mouse_state_dynamic(TypeTag::new::<Tag>(), region_id);
let child = render_child(&mut mouse_state, cx).into_any();
let notify_on_hover = mouse_state.accessed_hovered();
let notify_on_click = mouse_state.accessed_clicked();
Self {
child,
region_id,
cursor_style: None,
handlers: Default::default(),
notify_on_hover,
notify_on_click,
hoverable: true,
above: false,
padding: Default::default(),
tag: TypeTag::new::<Tag>(),
}
}
pub fn new_dynamic(
tag: TypeTag,
region_id: usize,
cx: &mut ViewContext<V>,
render_child: impl FnOnce(&mut MouseState, &mut ViewContext<V>) -> AnyElement<V>,
) -> Self {
let mut mouse_state = cx.mouse_state_dynamic(tag, region_id);
let child = render_child(&mut mouse_state, cx);
let notify_on_hover = mouse_state.accessed_hovered();
let notify_on_click = mouse_state.accessed_clicked();
Self {
child,
region_id,
cursor_style: None,
handlers: Default::default(),
notify_on_hover,
notify_on_click,
hoverable: true,
above: false,
padding: Default::default(),
tag,
}
}
/// Modifies the MouseEventHandler to render the MouseRegion above the child element. Useful
/// for drag and drop handling and similar events which should be captured before the child
/// gets the opportunity
pub fn above<Tag: 'static, D>(
region_id: usize,
cx: &mut ViewContext<V>,
render_child: impl FnOnce(&mut MouseState, &mut ViewContext<V>) -> D,
) -> Self
where
D: Element<V>,
{
let mut handler = Self::new::<Tag, _>(region_id, cx, render_child);
handler.above = true;
handler
}
pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
self.cursor_style = Some(cursor);
self
}
pub fn capture_all(mut self) -> Self {
self.handlers = HandlerSet::capture_all();
self
}
pub fn on_move(
mut self,
handler: impl Fn(MouseMove, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_move(handler);
self
}
pub fn on_move_out(
mut self,
handler: impl Fn(MouseMoveOut, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_move_out(handler);
self
}
pub fn on_down(
mut self,
button: MouseButton,
handler: impl Fn(MouseDown, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_down(button, handler);
self
}
pub fn on_up(
mut self,
button: MouseButton,
handler: impl Fn(MouseUp, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_up(button, handler);
self
}
pub fn on_click(
mut self,
button: MouseButton,
handler: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_click(button, handler);
self
}
pub fn on_click_out(
mut self,
button: MouseButton,
handler: impl Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_click_out(button, handler);
self
}
pub fn on_down_out(
mut self,
button: MouseButton,
handler: impl Fn(MouseDownOut, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_down_out(button, handler);
self
}
pub fn on_up_out(
mut self,
button: MouseButton,
handler: impl Fn(MouseUpOut, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_up_out(button, handler);
self
}
pub fn on_drag(
mut self,
button: MouseButton,
handler: impl Fn(MouseDrag, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_drag(button, handler);
self
}
pub fn on_hover(
mut self,
handler: impl Fn(MouseHover, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_hover(handler);
self
}
pub fn on_scroll(
mut self,
handler: impl Fn(MouseScrollWheel, &mut V, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers = self.handlers.on_scroll(handler);
self
}
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
self.hoverable = is_hoverable;
self
}
pub fn with_padding(mut self, padding: Padding) -> Self {
self.padding = padding;
self
}
fn hit_bounds(&self, bounds: RectF) -> RectF {
RectF::from_points(
bounds.origin() - vec2f(self.padding.left, self.padding.top),
bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
)
.round_out()
}
fn paint_regions(&self, bounds: RectF, visible_bounds: RectF, cx: &mut ViewContext<V>) {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
let hit_bounds = self.hit_bounds(visible_bounds);
if let Some(style) = self.cursor_style {
cx.scene().push_cursor_region(CursorRegion {
bounds: hit_bounds,
style,
});
}
let view_id = cx.view_id();
cx.scene().push_mouse_region(
MouseRegion::from_handlers(
self.tag,
view_id,
self.region_id,
hit_bounds,
self.handlers.clone(),
)
.with_hoverable(self.hoverable)
.with_notify_on_hover(self.notify_on_hover)
.with_notify_on_click(self.notify_on_click),
);
}
}
impl<V: 'static> Element<V> for MouseEventHandler<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, view, cx), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
if self.above {
self.child.paint(bounds.origin(), visible_bounds, view, cx);
cx.paint_layer(None, |cx| {
self.paint_regions(bounds, visible_bounds, cx);
});
} else {
self.paint_regions(bounds, visible_bounds, cx);
self.child.paint(bounds.origin(), visible_bounds, view, cx);
}
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
json!({
"type": "MouseEventHandler",
"child": self.child.debug(view, cx),
})
}
}

View file

@ -1,33 +1,187 @@
use std::ops::Range;
use smallvec::SmallVec;
use taffy::style::{Display, Position};
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::ToJson,
AnyElement, Axis, Element, MouseRegion, SizeConstraint, ViewContext,
point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels,
Point, Size, Style, WindowContext,
};
use serde_json::json;
pub struct Overlay<V> {
child: AnyElement<V>,
anchor_position: Option<Vector2F>,
anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
position_mode: OverlayPositionMode,
hoverable: bool,
z_index: Option<usize>,
pub struct OverlayState {
child_layout_ids: SmallVec<[LayoutId; 4]>,
}
#[derive(Copy, Clone)]
pub struct Overlay {
children: SmallVec<[AnyElement; 2]>,
anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
// todo!();
anchor_position: Option<Point<Pixels>>,
// position_mode: OverlayPositionMode,
}
/// overlay gives you a floating element that will avoid overflowing the window bounds.
/// Its children should have no margin to avoid measurement issues.
pub fn overlay() -> Overlay {
Overlay {
children: SmallVec::new(),
anchor_corner: AnchorCorner::TopLeft,
fit_mode: OverlayFitMode::SwitchAnchor,
anchor_position: None,
}
}
impl Overlay {
/// Sets which corner of the overlay should be anchored to the current position.
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
self.anchor_corner = anchor;
self
}
/// Sets the position in window co-ordinates
/// (otherwise the location the overlay is rendered is used)
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
self.anchor_position = Some(anchor);
self
}
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
pub fn snap_to_window(mut self) -> Self {
self.fit_mode = OverlayFitMode::SnapToWindow;
self
}
}
impl ParentElement for Overlay {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
impl Element for Overlay {
type State = OverlayState;
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (crate::LayoutId, Self::State) {
let child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(cx))
.collect::<SmallVec<_>>();
let overlay_style = Style {
position: Position::Absolute,
display: Display::Flex,
..Style::default()
};
let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
(layout_id, OverlayState { child_layout_ids })
}
fn paint(
&mut self,
bounds: crate::Bounds<crate::Pixels>,
element_state: &mut Self::State,
cx: &mut WindowContext,
) {
if element_state.child_layout_ids.is_empty() {
return;
}
let mut child_min = point(Pixels::MAX, Pixels::MAX);
let mut child_max = Point::default();
for child_layout_id in &element_state.child_layout_ids {
let child_bounds = cx.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.lower_right());
}
let size: Size<Pixels> = (child_max - child_min).into();
let origin = self.anchor_position.unwrap_or(bounds.origin);
let mut desired = self.anchor_corner.get_bounds(origin, size);
let limits = Bounds {
origin: Point::default(),
size: cx.viewport_size(),
};
if self.fit_mode == OverlayFitMode::SwitchAnchor {
let mut anchor_corner = self.anchor_corner;
if desired.left() < limits.left() || desired.right() > limits.right() {
let switched = anchor_corner
.switch_axis(Axis::Horizontal)
.get_bounds(origin, size);
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
desired = switched
}
}
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
let switched = anchor_corner
.switch_axis(Axis::Vertical)
.get_bounds(origin, size);
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
desired = switched;
}
}
}
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
// its horizontal bounds overflow, aligning to the left if it is wider than the limits.
if desired.right() > limits.right() {
desired.origin.x -= desired.right() - limits.right();
}
if desired.left() < limits.left() {
desired.origin.x = limits.origin.x;
}
// Snap the vertical edges of the overlay to the vertical edges of the window if
// its vertical bounds overflow, aligning to the top if it is taller than the limits.
if desired.bottom() > limits.bottom() {
desired.origin.y -= desired.bottom() - limits.bottom();
}
if desired.top() < limits.top() {
desired.origin.y = limits.origin.y;
}
let mut offset = cx.element_offset() + desired.origin - bounds.origin;
offset = point(offset.x.round(), offset.y.round());
cx.with_absolute_element_offset(offset, |cx| {
cx.break_content_mask(|cx| {
for child in &mut self.children {
child.paint(cx);
}
})
})
}
}
impl IntoElement for Overlay {
type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn into_element(self) -> Self::Element {
self
}
}
enum Axis {
Horizontal,
Vertical,
}
#[derive(Copy, Clone, PartialEq)]
pub enum OverlayFitMode {
SnapToWindow,
SwitchAnchor,
None,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum OverlayPositionMode {
Window,
Local,
}
#[derive(Clone, Copy, PartialEq, Eq)]
@ -39,18 +193,32 @@ pub enum AnchorCorner {
}
impl AnchorCorner {
fn get_bounds(&self, anchor_position: Vector2F, size: Vector2F) -> RectF {
fn get_bounds(&self, origin: Point<Pixels>, size: Size<Pixels>) -> Bounds<Pixels> {
let origin = match self {
Self::TopLeft => origin,
Self::TopRight => Point {
x: origin.x - size.width,
y: origin.y,
},
Self::BottomLeft => Point {
x: origin.x,
y: origin.y - size.height,
},
Self::BottomRight => Point {
x: origin.x - size.width,
y: origin.y - size.height,
},
};
Bounds { origin, size }
}
pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
match self {
Self::TopLeft => RectF::from_points(anchor_position, anchor_position + size),
Self::TopRight => RectF::from_points(
anchor_position - Vector2F::new(size.x(), 0.),
anchor_position + Vector2F::new(0., size.y()),
),
Self::BottomLeft => RectF::from_points(
anchor_position - Vector2F::new(0., size.y()),
anchor_position + Vector2F::new(size.x(), 0.),
),
Self::BottomRight => RectF::from_points(anchor_position - size, anchor_position),
Self::TopLeft => bounds.origin,
Self::TopRight => bounds.upper_right(),
Self::BottomLeft => bounds.lower_left(),
Self::BottomRight => bounds.lower_right(),
}
}
@ -71,190 +239,3 @@ impl AnchorCorner {
}
}
}
impl<V: 'static> Overlay<V> {
pub fn new(child: impl Element<V>) -> Self {
Self {
child: child.into_any(),
anchor_position: None,
anchor_corner: AnchorCorner::TopLeft,
fit_mode: OverlayFitMode::None,
position_mode: OverlayPositionMode::Window,
hoverable: false,
z_index: None,
}
}
pub fn with_anchor_position(mut self, position: Vector2F) -> Self {
self.anchor_position = Some(position);
self
}
pub fn with_anchor_corner(mut self, anchor_corner: AnchorCorner) -> Self {
self.anchor_corner = anchor_corner;
self
}
pub fn with_fit_mode(mut self, fit_mode: OverlayFitMode) -> Self {
self.fit_mode = fit_mode;
self
}
pub fn with_position_mode(mut self, position_mode: OverlayPositionMode) -> Self {
self.position_mode = position_mode;
self
}
pub fn with_hoverable(mut self, hoverable: bool) -> Self {
self.hoverable = hoverable;
self
}
pub fn with_z_index(mut self, z_index: usize) -> Self {
self.z_index = Some(z_index);
self
}
}
impl<V: 'static> Element<V> for Overlay<V> {
type LayoutState = Vector2F;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let constraint = if self.anchor_position.is_some() {
SizeConstraint::new(Vector2F::zero(), cx.window_size())
} else {
constraint
};
let size = self.child.layout(constraint, view, cx);
(Vector2F::zero(), size)
}
fn paint(
&mut self,
bounds: RectF,
_: RectF,
size: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) {
let (anchor_position, mut bounds) = match self.position_mode {
OverlayPositionMode::Window => {
let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
let bounds = self.anchor_corner.get_bounds(anchor_position, *size);
(anchor_position, bounds)
}
OverlayPositionMode::Local => {
let anchor_position = self.anchor_position.unwrap_or_default();
let bounds = self
.anchor_corner
.get_bounds(bounds.origin() + anchor_position, *size);
(anchor_position, bounds)
}
};
match self.fit_mode {
OverlayFitMode::SnapToWindow => {
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
// its horizontal bounds overflow
if bounds.max_x() > cx.window_size().x() {
let mut lower_right = bounds.lower_right();
lower_right.set_x(cx.window_size().x());
bounds = RectF::from_points(lower_right - *size, lower_right);
} else if bounds.min_x() < 0. {
let mut upper_left = bounds.origin();
upper_left.set_x(0.);
bounds = RectF::from_points(upper_left, upper_left + *size);
}
// Snap the vertical edges of the overlay to the vertical edges of the window if
// its vertical bounds overflow.
if bounds.max_y() > cx.window_size().y() {
let mut lower_right = bounds.lower_right();
lower_right.set_y(cx.window_size().y());
bounds = RectF::from_points(lower_right - *size, lower_right);
} else if bounds.min_y() < 0. {
let mut upper_left = bounds.origin();
upper_left.set_y(0.);
bounds = RectF::from_points(upper_left, upper_left + *size);
}
}
OverlayFitMode::SwitchAnchor => {
let mut anchor_corner = self.anchor_corner;
if bounds.max_x() > cx.window_size().x() {
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
}
if bounds.max_y() > cx.window_size().y() {
anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
}
if bounds.min_x() < 0. {
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal)
}
if bounds.min_y() < 0. {
anchor_corner = anchor_corner.switch_axis(Axis::Vertical)
}
// Update bounds if needed
if anchor_corner != self.anchor_corner {
bounds = anchor_corner.get_bounds(anchor_position, *size)
}
}
OverlayFitMode::None => {}
}
cx.scene().push_stacking_context(None, self.z_index);
if self.hoverable {
enum OverlayHoverCapture {}
// Block hovers in lower stacking contexts
let view_id = cx.view_id();
cx.scene()
.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
view_id, view_id, bounds,
));
}
self.child.paint(
bounds.origin(),
RectF::new(Vector2F::zero(), cx.window_size()),
view,
cx,
);
cx.scene().pop_stacking_context();
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
json!({
"type": "Overlay",
"abs_position": self.anchor_position.to_json(),
"child": self.child.debug(view, cx),
})
}
}

View file

@ -1,290 +0,0 @@
use std::{cell::RefCell, rc::Rc};
use collections::HashMap;
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
use crate::{
geometry::rect::RectF,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Axis, Element, MouseRegion, SizeConstraint, TypeTag, View, ViewContext,
};
#[derive(Copy, Clone, Debug)]
pub enum HandleSide {
Top,
Bottom,
Left,
Right,
}
impl HandleSide {
fn axis(&self) -> Axis {
match self {
HandleSide::Left | HandleSide::Right => Axis::Horizontal,
HandleSide::Top | HandleSide::Bottom => Axis::Vertical,
}
}
fn relevant_component(&self, vector: Vector2F) -> f32 {
match self.axis() {
Axis::Horizontal => vector.x(),
Axis::Vertical => vector.y(),
}
}
fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
match self {
HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
HandleSide::Bottom => {
let mut origin = bounds.lower_left();
origin.set_y(origin.y() - handle_size);
RectF::new(origin, vec2f(bounds.width(), handle_size))
}
HandleSide::Right => {
let mut origin = bounds.upper_right();
origin.set_x(origin.x() - handle_size);
RectF::new(origin, vec2f(handle_size, bounds.height()))
}
}
}
}
fn get_bounds(tag: TypeTag, cx: &AppContext) -> Option<&(RectF, RectF)>
where
{
cx.optional_global::<ProviderMap>()
.and_then(|map| map.0.get(&tag))
}
pub struct Resizable<V: 'static> {
child: AnyElement<V>,
tag: TypeTag,
handle_side: HandleSide,
handle_size: f32,
on_resize: Rc<RefCell<dyn FnMut(&mut V, Option<f32>, &mut ViewContext<V>)>>,
}
const DEFAULT_HANDLE_SIZE: f32 = 4.0;
impl<V: 'static> Resizable<V> {
pub fn new<Tag: 'static>(
child: AnyElement<V>,
handle_side: HandleSide,
size: f32,
on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
) -> Self {
let child = match handle_side.axis() {
Axis::Horizontal => child.constrained().with_max_width(size),
Axis::Vertical => child.constrained().with_max_height(size),
}
.into_any();
Self {
child,
handle_side,
tag: TypeTag::new::<Tag>(),
handle_size: DEFAULT_HANDLE_SIZE,
on_resize: Rc::new(RefCell::new(on_resize)),
}
}
pub fn with_handle_size(mut self, handle_size: f32) -> Self {
self.handle_size = handle_size;
self
}
}
impl<V: 'static> Element<V> for Resizable<V> {
type LayoutState = SizeConstraint;
type PaintState = ();
fn layout(
&mut self,
constraint: crate::SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, view, cx), constraint)
}
fn paint(
&mut self,
bounds: pathfinder_geometry::rect::RectF,
visible_bounds: pathfinder_geometry::rect::RectF,
constraint: &mut SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
cx.scene().push_stacking_context(None, None);
let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
enum ResizeHandle {}
let view_id = cx.view_id();
cx.scene().push_mouse_region(
MouseRegion::new::<ResizeHandle>(view_id, self.handle_side as usize, handle_region)
.on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
.on_click(MouseButton::Left, {
let on_resize = self.on_resize.clone();
move |click, v, cx| {
if click.click_count == 2 {
on_resize.borrow_mut()(v, None, cx);
}
}
})
.on_drag(MouseButton::Left, {
let bounds = bounds.clone();
let side = self.handle_side;
let prev_size = side.relevant_component(bounds.size());
let min_size = side.relevant_component(constraint.min);
let max_size = side.relevant_component(constraint.max);
let on_resize = self.on_resize.clone();
let tag = self.tag;
move |event, view: &mut V, cx| {
if event.end {
return;
}
let Some((bounds, _)) = get_bounds(tag, cx) else {
return;
};
let new_size_raw = match side {
// Handle on top side of element => Element is on bottom
HandleSide::Top => {
bounds.height() + bounds.origin_y() - event.position.y()
}
// Handle on right side of element => Element is on left
HandleSide::Right => event.position.x() - bounds.lower_left().x(),
// Handle on left side of element => Element is on the right
HandleSide::Left => {
bounds.width() + bounds.origin_x() - event.position.x()
}
// Handle on bottom side of element => Element is on the top
HandleSide::Bottom => event.position.y() - bounds.lower_left().y(),
};
let new_size = min_size.max(new_size_raw).min(max_size).round();
if new_size != prev_size {
on_resize.borrow_mut()(view, Some(new_size), cx);
}
}
}),
);
cx.scene().push_cursor_region(crate::CursorRegion {
bounds: handle_region,
style: match self.handle_side.axis() {
Axis::Horizontal => CursorStyle::ResizeLeftRight,
Axis::Vertical => CursorStyle::ResizeUpDown,
},
});
cx.scene().pop_stacking_context();
self.child.paint(bounds.origin(), visible_bounds, view, cx);
}
fn rect_for_text_range(
&self,
range_utf16: std::ops::Range<usize>,
_bounds: pathfinder_geometry::rect::RectF,
_visible_bounds: pathfinder_geometry::rect::RectF,
_layout: &Self::LayoutState,
_paint: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<pathfinder_geometry::rect::RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_bounds: pathfinder_geometry::rect::RectF,
_layout: &Self::LayoutState,
_paint: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
json!({
"child": self.child.debug(view, cx),
})
}
}
#[derive(Debug, Default)]
struct ProviderMap(HashMap<TypeTag, (RectF, RectF)>);
pub struct BoundsProvider<V: 'static, P> {
child: AnyElement<V>,
phantom: std::marker::PhantomData<P>,
}
impl<V: 'static, P: 'static> BoundsProvider<V, P> {
pub fn new(child: AnyElement<V>) -> Self {
Self {
child,
phantom: std::marker::PhantomData,
}
}
}
impl<V: View, P: 'static> Element<V> for BoundsProvider<V, P> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: crate::SizeConstraint,
view: &mut V,
cx: &mut crate::ViewContext<V>,
) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) {
(self.child.layout(constraint, view, cx), ())
}
fn paint(
&mut self,
bounds: pathfinder_geometry::rect::RectF,
visible_bounds: pathfinder_geometry::rect::RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut crate::ViewContext<V>,
) -> Self::PaintState {
cx.update_default_global::<ProviderMap, _, _>(|map, _| {
map.0.insert(TypeTag::new::<P>(), (bounds, visible_bounds));
});
self.child.paint(bounds.origin(), visible_bounds, view, cx)
}
fn rect_for_text_range(
&self,
range_utf16: std::ops::Range<usize>,
_: pathfinder_geometry::rect::RectF,
_: pathfinder_geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &crate::ViewContext<V>,
) -> Option<pathfinder_geometry::rect::RectF> {
self.child.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: pathfinder_geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &crate::ViewContext<V>,
) -> serde_json::Value {
serde_json::json!({
"type": "Provider",
"providing": format!("{:?}", TypeTag::new::<P>()),
"child": self.child.debug(view, cx),
})
}
}

View file

@ -1,104 +0,0 @@
use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::{self, json, ToJson},
AnyElement, Element, SizeConstraint, ViewContext,
};
/// Element which renders it's children in a stack on top of each other.
/// The first child determines the size of the others.
pub struct Stack<V> {
children: Vec<AnyElement<V>>,
}
impl<V> Default for Stack<V> {
fn default() -> Self {
Self {
children: Vec::new(),
}
}
}
impl<V> Stack<V> {
pub fn new() -> Self {
Self::default()
}
}
impl<V: 'static> Element<V> for Stack<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.min;
let mut children = self.children.iter_mut();
if let Some(bottom_child) = children.next() {
size = bottom_child.layout(constraint, view, cx);
constraint = SizeConstraint::strict(size);
}
for child in children {
child.layout(constraint, view, cx);
}
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
for child in &mut self.children {
cx.scene().push_layer(None);
child.paint(bounds.origin(), visible_bounds, view, cx);
cx.scene().pop_layer();
}
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.children
.iter()
.rev()
.find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> json::Value {
json!({
"type": "Stack",
"bounds": bounds.to_json(),
"children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
})
}
}
impl<V> Extend<AnyElement<V>> for Stack<V> {
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
self.children.extend(children)
}
}

View file

@ -1,141 +1,78 @@
use super::constrain_size_preserving_aspect_ratio;
use crate::json::ToJson;
use crate::{
color::Color,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
scene, Element, SizeConstraint, ViewContext,
Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
};
use schemars::JsonSchema;
use serde_derive::Deserialize;
use serde_json::json;
use std::{borrow::Cow, ops::Range};
use util::ResultExt;
pub struct Svg {
path: Cow<'static, str>,
color: Color,
interactivity: Interactivity,
path: Option<SharedString>,
}
pub fn svg() -> Svg {
Svg {
interactivity: Interactivity::default(),
path: None,
}
}
impl Svg {
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
Self {
path: path.into(),
color: Color::black(),
}
}
pub fn for_style<V: 'static>(style: SvgStyle) -> impl Element<V> {
Self::new(style.asset)
.with_color(style.color)
.constrained()
.with_width(style.dimensions.width)
.with_height(style.dimensions.height)
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = Some(path.into());
self
}
}
impl<V: 'static> Element<V> for Svg {
type LayoutState = Option<usvg::Tree>;
type PaintState = ();
impl Element for Svg {
type State = InteractiveElementState;
fn layout(
fn request_layout(
&mut self,
constraint: SizeConstraint,
_: &mut V,
cx: &mut ViewContext<V>,
) -> (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)
}
}
element_state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})
}
fn paint(
&mut self,
bounds: RectF,
_visible_bounds: RectF,
svg: &mut Self::LayoutState,
_: &mut V,
cx: &mut ViewContext<V>,
) {
if let Some(svg) = svg.clone() {
cx.scene().push_icon(scene::Icon {
bounds,
svg,
path: self.path.clone(),
color: self.color,
});
}
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> serde_json::Value {
json!({
"type": "Svg",
"bounds": bounds.to_json(),
"path": self.path,
"color": self.color.to_json(),
})
bounds: Bounds<Pixels>,
element_state: &mut Self::State,
cx: &mut WindowContext,
) where
Self: Sized,
{
self.interactivity
.paint(bounds, bounds.size, element_state, cx, |style, _, cx| {
if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
cx.paint_svg(bounds, path.clone(), color).log_err();
}
})
}
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct SvgStyle {
pub color: Color,
pub asset: String,
pub dimensions: Dimensions,
}
impl IntoElement for Svg {
type Element = Self;
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Dimensions {
pub width: f32,
pub height: f32,
}
fn element_id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
impl Dimensions {
pub fn to_vec(&self) -> Vector2F {
vec2f(self.width, self.height)
fn into_element(self) -> Self::Element {
self
}
}
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),
)
impl Styled for Svg {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl InteractiveElement for Svg {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}

View file

@ -1,438 +1,423 @@
use crate::{
color::Color,
fonts::{HighlightStyle, TextStyle},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{ToJson, Value},
text_layout::{Line, RunStyle, ShapedBoundary},
Element, FontCache, SizeConstraint, TextLayoutCache, ViewContext, WindowContext,
Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId,
MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle,
WhiteSpace, WindowContext, WrappedLine,
};
use log::warn;
use serde_json::json;
use std::{borrow::Cow, ops::Range, sync::Arc};
use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec;
use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc};
use util::ResultExt;
pub struct Text {
text: Cow<'static, str>,
style: TextStyle,
soft_wrap: bool,
highlights: Option<Box<[(Range<usize>, HighlightStyle)]>>,
custom_runs: Option<(
Box<[Range<usize>]>,
Box<dyn FnMut(usize, RectF, &mut WindowContext)>,
)>,
}
impl Element for &'static str {
type State = TextState;
pub struct LayoutState {
shaped_lines: Vec<Line>,
wrap_boundaries: Vec<Vec<ShapedBoundary>>,
line_height: f32,
}
impl Text {
pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
Self {
text: text.into(),
style,
soft_wrap: true,
highlights: None,
custom_runs: None,
}
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let mut state = TextState::default();
let layout_id = state.layout(SharedString::from(*self), None, cx);
(layout_id, state)
}
pub fn with_default_color(mut self, color: Color) -> Self {
self.style.color = color;
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
state.paint(bounds, self, cx)
}
}
impl IntoElement for &'static str {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
None
}
fn into_element(self) -> Self::Element {
self
}
}
impl Element for SharedString {
type State = TextState;
fn request_layout(
&mut self,
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let mut state = TextState::default();
let layout_id = state.layout(self.clone(), None, cx);
(layout_id, state)
}
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
let text_str: &str = self.as_ref();
state.paint(bounds, text_str, cx)
}
}
impl IntoElement for SharedString {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
None
}
fn into_element(self) -> Self::Element {
self
}
}
/// Renders text with runs of different styles.
///
/// Callers are responsible for setting the correct style for each run.
/// For text with a uniform style, you can usually avoid calling this constructor
/// and just pass text directly.
pub struct StyledText {
text: SharedString,
runs: Option<Vec<TextRun>>,
}
impl StyledText {
pub fn new(text: impl Into<SharedString>) -> Self {
StyledText {
text: text.into(),
runs: None,
}
}
pub fn with_highlights(
mut self,
runs: impl Into<Box<[(Range<usize>, HighlightStyle)]>>,
default_style: &TextStyle,
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
) -> Self {
self.highlights = Some(runs.into());
self
}
pub fn with_custom_runs(
mut self,
runs: impl Into<Box<[Range<usize>]>>,
callback: impl 'static + FnMut(usize, RectF, &mut WindowContext),
) -> Self {
self.custom_runs = Some((runs.into(), Box::new(callback)));
self
}
pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
self.soft_wrap = soft_wrap;
let mut runs = Vec::new();
let mut ix = 0;
for (range, highlight) in highlights {
if ix < range.start {
runs.push(default_style.clone().to_run(range.start - ix));
}
runs.push(
default_style
.clone()
.highlight(highlight)
.to_run(range.len()),
);
ix = range.end;
}
if ix < self.text.len() {
runs.push(default_style.to_run(self.text.len() - ix));
}
self.runs = Some(runs);
self
}
}
impl<V: 'static> Element<V> for Text {
type LayoutState = LayoutState;
type PaintState = ();
impl Element for StyledText {
type State = TextState;
fn layout(
fn request_layout(
&mut self,
constraint: SizeConstraint,
_: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
// Convert the string and highlight ranges into an iterator of highlighted chunks.
let mut offset = 0;
let mut highlight_ranges = self
.highlights
.as_ref()
.map_or(Default::default(), AsRef::as_ref)
.iter()
.peekable();
let chunks = std::iter::from_fn(|| {
let result;
if let Some((range, highlight_style)) = highlight_ranges.peek() {
if offset < range.start {
result = Some((&self.text[offset..range.start], None));
offset = range.start;
} else if range.end <= self.text.len() {
result = Some((&self.text[range.clone()], Some(*highlight_style)));
highlight_ranges.next();
offset = range.end;
} else {
warn!(
"Highlight out of text range. Text len: {}, Highlight range: {}..{}",
self.text.len(),
range.start,
range.end
);
result = None;
}
} else if offset < self.text.len() {
result = Some((&self.text[offset..], None));
offset = self.text.len();
} else {
result = None;
}
result
});
// Perform shaping on these highlighted chunks
let shaped_lines = layout_highlighted_chunks(
chunks,
&self.style,
cx.text_layout_cache(),
&cx.font_cache,
usize::MAX,
self.text.matches('\n').count() + 1,
);
// If line wrapping is enabled, wrap each of the shaped lines.
let font_id = self.style.font_id;
let mut line_count = 0;
let mut max_line_width = 0_f32;
let mut wrap_boundaries = Vec::new();
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
for (line, shaped_line) in self.text.split('\n').zip(&shaped_lines) {
if self.soft_wrap {
let boundaries = wrapper
.wrap_shaped_line(line, shaped_line, constraint.max.x())
.collect::<Vec<_>>();
line_count += boundaries.len() + 1;
wrap_boundaries.push(boundaries);
} else {
line_count += 1;
}
max_line_width = max_line_width.max(shaped_line.width());
}
let line_height = cx.font_cache.line_height(self.style.font_size);
let size = vec2f(
max_line_width
.ceil()
.max(constraint.min.x())
.min(constraint.max.x()),
(line_height * line_count as f32).ceil(),
);
(
size,
LayoutState {
shaped_lines,
wrap_boundaries,
line_height,
},
)
_: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let mut state = TextState::default();
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
(layout_id, state)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
layout: &mut Self::LayoutState,
_: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
let mut origin = bounds.origin();
let empty = Vec::new();
let mut callback = |_, _, _: &mut WindowContext| {};
let mouse_runs;
let custom_run_callback;
if let Some((runs, build_region)) = &mut self.custom_runs {
mouse_runs = runs.iter();
custom_run_callback = build_region.as_mut();
} else {
mouse_runs = [].iter();
custom_run_callback = &mut callback;
}
let mut custom_runs = mouse_runs.enumerate().peekable();
let mut offset = 0;
for (ix, line) in layout.shaped_lines.iter().enumerate() {
let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
let boundaries = RectF::new(
origin,
vec2f(
bounds.width(),
(wrap_boundaries.len() + 1) as f32 * layout.line_height,
),
);
if boundaries.intersects(visible_bounds) {
if self.soft_wrap {
line.paint_wrapped(
origin,
visible_bounds,
layout.line_height,
wrap_boundaries,
cx,
);
} else {
line.paint(origin, visible_bounds, layout.line_height, cx);
}
}
// Paint any custom runs that intersect this line.
let end_offset = offset + line.len();
if let Some((custom_run_ix, custom_run_range)) = custom_runs.peek().cloned() {
if custom_run_range.start < end_offset {
let mut current_custom_run = None;
if custom_run_range.start <= offset {
current_custom_run = Some((custom_run_ix, custom_run_range.end, origin));
}
let mut glyph_origin = origin;
let mut prev_position = 0.;
let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable();
for (run_ix, glyph_ix, glyph) in
line.runs().iter().enumerate().flat_map(|(run_ix, run)| {
run.glyphs()
.iter()
.enumerate()
.map(move |(ix, glyph)| (run_ix, ix, glyph))
})
{
glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
prev_position = glyph.position.x();
// If we've reached a soft wrap position, move down one line. If there
// is a custom run in-progress, paint it.
if wrap_boundaries
.peek()
.map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
{
if let Some((run_ix, _, run_origin)) = &mut current_custom_run {
let bounds = RectF::from_points(
*run_origin,
glyph_origin + vec2f(0., layout.line_height),
);
custom_run_callback(*run_ix, bounds, cx);
*run_origin =
vec2f(origin.x(), glyph_origin.y() + layout.line_height);
}
wrap_boundaries.next();
glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height);
}
// If we've reached the end of the current custom run, paint it.
if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
if offset + glyph.index == run_end_offset {
current_custom_run.take();
let bounds = RectF::from_points(
run_origin,
glyph_origin + vec2f(0., layout.line_height),
);
custom_run_callback(run_ix, bounds, cx);
custom_runs.next();
}
if let Some((_, run_range)) = custom_runs.peek() {
if run_range.start >= end_offset {
break;
}
if run_range.start == offset + glyph.index {
current_custom_run =
Some((run_ix, run_range.end, glyph_origin));
}
}
}
// If we've reached the start of a new custom run, start tracking it.
if let Some((run_ix, run_range)) = custom_runs.peek() {
if offset + glyph.index == run_range.start {
current_custom_run = Some((*run_ix, run_range.end, glyph_origin));
}
}
}
// If a custom run extends beyond the end of the line, paint it.
if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.);
let bounds = RectF::from_points(
run_origin,
line_end + vec2f(0., layout.line_height),
);
custom_run_callback(run_ix, bounds, cx);
if end_offset == run_end_offset {
custom_runs.next();
}
}
}
}
offset = end_offset + 1;
origin.set_y(boundaries.max_y());
}
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
state.paint(bounds, &self.text, cx)
}
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> Option<RectF> {
impl IntoElement for StyledText {
type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &V,
_: &ViewContext<V>,
) -> Value {
json!({
"type": "Text",
"bounds": bounds.to_json(),
"text": &self.text,
"style": self.style.to_json(),
})
fn into_element(self) -> Self::Element {
self
}
}
/// Perform text layout on a series of highlighted chunks of text.
pub fn layout_highlighted_chunks<'a>(
chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
text_style: &TextStyle,
text_layout_cache: &TextLayoutCache,
font_cache: &Arc<FontCache>,
max_line_len: usize,
max_line_count: usize,
) -> Vec<Line> {
let mut layouts = Vec::with_capacity(max_line_count);
let mut line = String::new();
let mut styles = Vec::new();
let mut row = 0;
let mut line_exceeded_max_len = false;
for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
if ix > 0 {
layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
line.clear();
styles.clear();
row += 1;
line_exceeded_max_len = false;
if row == max_line_count {
return layouts;
}
}
#[derive(Default, Clone)]
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
if !line_chunk.is_empty() && !line_exceeded_max_len {
let text_style = if let Some(style) = highlight_style {
text_style
.clone()
.highlight(style, font_cache)
.map(Cow::Owned)
.unwrap_or_else(|_| Cow::Borrowed(text_style))
struct TextStateInner {
lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels,
wrap_width: Option<Pixels>,
size: Option<Size<Pixels>>,
}
impl TextState {
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
self.0.lock()
}
fn layout(
&mut self,
text: SharedString,
runs: Option<Vec<TextRun>>,
cx: &mut WindowContext,
) -> LayoutId {
let text_style = cx.text_style();
let font_size = text_style.font_size.to_pixels(cx.rem_size());
let line_height = text_style
.line_height
.to_pixels(font_size.into(), cx.rem_size());
let runs = if let Some(runs) = runs {
runs
} else {
vec![text_style.to_run(text.len())]
};
let layout_id = cx.request_measured_layout(Default::default(), {
let element_state = self.clone();
move |known_dimensions, available_space, cx| {
let wrap_width = if text_style.white_space == WhiteSpace::Normal {
known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => Some(x),
_ => None,
})
} else {
Cow::Borrowed(text_style)
None
};
if line.len() + line_chunk.len() > max_line_len {
let mut chunk_len = max_line_len - line.len();
while !line_chunk.is_char_boundary(chunk_len) {
chunk_len -= 1;
if let Some(text_state) = element_state.0.lock().as_ref() {
if text_state.size.is_some()
&& (wrap_width.is_none() || wrap_width == text_state.wrap_width)
{
return text_state.size.unwrap();
}
line_chunk = &line_chunk[..chunk_len];
line_exceeded_max_len = true;
}
line.push_str(line_chunk);
styles.push((
line_chunk.len(),
RunStyle {
font_id: text_style.font_id,
color: text_style.color,
underline: text_style.underline,
},
));
let Some(lines) = cx
.text_system()
.shape_text(
&text, font_size, &runs, wrap_width, // Wrap if we know the width.
)
.log_err()
else {
element_state.lock().replace(TextStateInner {
lines: Default::default(),
line_height,
wrap_width,
size: Some(Size::default()),
});
return Size::default();
};
let mut size: Size<Pixels> = Size::default();
for line in &lines {
let line_size = line.size(line_height);
size.height += line_size.height;
size.width = size.width.max(line_size.width).ceil();
}
element_state.lock().replace(TextStateInner {
lines,
line_height,
wrap_width,
size: Some(size),
});
size
}
});
layout_id
}
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
let element_state = self.lock();
let element_state = element_state
.as_ref()
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
.unwrap();
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
for line in &element_state.lines {
line.paint(line_origin, line_height, cx).log_err();
line_origin.y += line.size(line_height).height;
}
}
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
if !bounds.contains(&position) {
return None;
}
let element_state = self.lock();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_bottom = line_origin.y + line.size(line_height).height;
if position.y > line_bottom {
line_origin.y = line_bottom;
line_start_ix += line.len() + 1;
} else {
let position_within_line = position - line_origin;
let index_within_line =
line.index_for_position(position_within_line, line_height)?;
return Some(line_start_ix + index_within_line);
}
}
}
layouts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{elements::Empty, fonts, AnyElement, AppContext, Entity, View, ViewContext};
#[crate::test(self)]
fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) {
cx.add_window(Default::default(), |cx| {
let mut view = TestView;
fonts::with_font_cache(cx.font_cache().clone(), || {
let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
let (_, state) = text.layout(
SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
&mut view,
cx,
);
assert_eq!(state.shaped_lines.len(), 2);
assert_eq!(state.wrap_boundaries.len(), 2);
});
view
});
}
struct TestView;
impl Entity for TestView {
type Event = ();
}
impl View for TestView {
fn ui_name() -> &'static str {
"TestView"
}
fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
Empty::new().into_any()
}
None
}
}
pub struct InteractiveText {
element_id: ElementId,
text: StyledText,
click_listener:
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
clickable_ranges: Vec<Range<usize>>,
}
struct InteractiveTextClickEvent {
mouse_down_index: usize,
mouse_up_index: usize,
}
pub struct InteractiveTextState {
text_state: TextState,
mouse_down_index: Rc<Cell<Option<usize>>>,
}
impl InteractiveText {
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
Self {
element_id: id.into(),
text,
click_listener: None,
clickable_ranges: Vec::new(),
}
}
pub fn on_click(
mut self,
ranges: Vec<Range<usize>>,
listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
) -> Self {
self.click_listener = Some(Box::new(move |ranges, event, cx| {
for (range_ix, range) in ranges.iter().enumerate() {
if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
{
listener(range_ix, cx);
}
}
}));
self.clickable_ranges = ranges;
self
}
}
impl Element for InteractiveText {
type State = InteractiveTextState;
fn request_layout(
&mut self,
state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
if let Some(InteractiveTextState {
mouse_down_index, ..
}) = state
{
let (layout_id, text_state) = self.text.request_layout(None, cx);
let element_state = InteractiveTextState {
text_state,
mouse_down_index,
};
(layout_id, element_state)
} else {
let (layout_id, text_state) = self.text.request_layout(None, cx);
let element_state = InteractiveTextState {
text_state,
mouse_down_index: Rc::default(),
};
(layout_id, element_state)
}
}
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
if let Some(click_listener) = self.click_listener.take() {
let mouse_position = cx.mouse_position();
if let Some(ix) = state.text_state.index_for_position(bounds, mouse_position) {
if self
.clickable_ranges
.iter()
.any(|range| range.contains(&ix))
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
{
cx.set_cursor_style(crate::CursorStyle::PointingHand)
}
}
let text_state = state.text_state.clone();
let mouse_down = state.mouse_down_index.clone();
if let Some(mouse_down_index) = mouse_down.get() {
let clickable_ranges = mem::take(&mut self.clickable_ranges);
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
if let Some(mouse_up_index) =
text_state.index_for_position(bounds, event.position)
{
click_listener(
&clickable_ranges,
InteractiveTextClickEvent {
mouse_down_index,
mouse_up_index,
},
cx,
)
}
mouse_down.take();
cx.notify();
}
});
} else {
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
if let Some(mouse_down_index) =
text_state.index_for_position(bounds, event.position)
{
mouse_down.set(Some(mouse_down_index));
cx.notify();
}
}
});
}
}
self.text.paint(bounds, &mut state.text_state, cx)
}
}
impl IntoElement for InteractiveText {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
Some(self.element_id.clone())
}
fn into_element(self) -> Self::Element {
self
}
}

View file

@ -1,244 +0,0 @@
use super::{
AnyElement, ContainerStyle, Element, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
OverlayFitMode, ParentElement, Text,
};
use crate::{
fonts::TextStyle,
geometry::{rect::RectF, vector::Vector2F},
json::json,
Action, Axis, ElementStateHandle, SizeConstraint, Task, TypeTag, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use std::{
borrow::Cow,
cell::{Cell, RefCell},
ops::Range,
rc::Rc,
time::Duration,
};
use util::ResultExt;
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
pub struct Tooltip<V> {
child: AnyElement<V>,
tooltip: Option<AnyElement<V>>,
_state: ElementStateHandle<Rc<TooltipState>>,
}
#[derive(Default)]
struct TooltipState {
visible: Cell<bool>,
position: Cell<Vector2F>,
debounce: RefCell<Option<Task<()>>>,
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct TooltipStyle {
#[serde(flatten)]
pub container: ContainerStyle,
pub text: TextStyle,
keystroke: KeystrokeStyle,
pub max_text_width: Option<f32>,
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct KeystrokeStyle {
#[serde(flatten)]
container: ContainerStyle,
#[serde(flatten)]
text: TextStyle,
}
impl<V: 'static> Tooltip<V> {
pub fn new<Tag: 'static>(
id: usize,
text: impl Into<Cow<'static, str>>,
action: Option<Box<dyn Action>>,
style: TooltipStyle,
child: AnyElement<V>,
cx: &mut ViewContext<V>,
) -> Self {
Self::new_dynamic(TypeTag::new::<Tag>(), id, text, action, style, child, cx)
}
pub fn new_dynamic(
mut tag: TypeTag,
id: usize,
text: impl Into<Cow<'static, str>>,
action: Option<Box<dyn Action>>,
style: TooltipStyle,
child: AnyElement<V>,
cx: &mut ViewContext<V>,
) -> Self {
tag = tag.compose(TypeTag::new::<Self>());
let focused_view_id = cx.focused_view_id();
let state_handle = cx.default_element_state_dynamic::<Rc<TooltipState>>(tag, id);
let state = state_handle.read(cx).clone();
let text = text.into();
let tooltip = if state.visible.get() {
let mut collapsed_tooltip = Self::render_tooltip(
focused_view_id,
text.clone(),
style.clone(),
action.as_ref().map(|a| a.boxed_clone()),
true,
);
Some(
Overlay::new(
Self::render_tooltip(focused_view_id, text, style, action, false)
.constrained()
.dynamically(move |constraint, view, cx| {
SizeConstraint::strict_along(
Axis::Vertical,
collapsed_tooltip.layout(constraint, view, cx).0.y(),
)
}),
)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_position(state.position.get())
.into_any(),
)
} else {
None
};
let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child)
.on_hover(move |e, _, cx| {
let position = e.position;
if e.started {
if !state.visible.get() {
state.position.set(position);
let mut debounce = state.debounce.borrow_mut();
if debounce.is_none() {
*debounce = Some(cx.spawn({
let state = state.clone();
|view, mut cx| async move {
cx.background().timer(DEBOUNCE_TIMEOUT).await;
state.visible.set(true);
view.update(&mut cx, |_, cx| cx.notify()).log_err();
}
}));
}
}
} else {
state.visible.set(false);
state.debounce.take();
cx.notify();
}
})
.into_any();
Self {
child,
tooltip,
_state: state_handle,
}
}
pub fn render_tooltip(
focused_view_id: Option<usize>,
text: impl Into<Cow<'static, str>>,
style: TooltipStyle,
action: Option<Box<dyn Action>>,
measure: bool,
) -> impl Element<V> {
Flex::row()
.with_child({
let text = if let Some(max_text_width) = style.max_text_width {
Text::new(text, style.text)
.constrained()
.with_max_width(max_text_width)
} else {
Text::new(text, style.text).constrained()
};
if measure {
text.flex(1., false).into_any()
} else {
text.flex(1., false).aligned().into_any()
}
})
.with_children(action.and_then(|action| {
let keystroke_label = KeystrokeLabel::new(
focused_view_id?,
action,
style.keystroke.container,
style.keystroke.text,
);
if measure {
Some(keystroke_label.into_any())
} else {
Some(keystroke_label.aligned().into_any())
}
}))
.contained()
.with_style(style.container)
}
}
impl<V: 'static> Element<V> for Tooltip<V> {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, view, cx);
if let Some(tooltip) = self.tooltip.as_mut() {
tooltip.layout(
SizeConstraint::new(Vector2F::zero(), cx.window_size()),
view,
cx,
);
}
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) {
self.child.paint(bounds.origin(), visible_bounds, view, cx);
if let Some(tooltip) = self.tooltip.as_mut() {
tooltip.paint(bounds.origin(), visible_bounds, view, cx);
}
}
fn rect_for_text_range(
&self,
range: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
self.child.rect_for_text_range(range, view, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
json!({
"child": self.child.debug(view, cx),
"tooltip": self.tooltip.as_ref().map(|t| t.debug(view, cx)),
})
}
}

View file

@ -1,354 +1,316 @@
use super::{Element, SizeConstraint};
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{self, json},
platform::ScrollWheelEvent,
AnyElement, MouseRegion, ViewContext,
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
};
use json::ToJson;
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
use taffy::style::Overflow;
#[derive(Clone, Default)]
pub struct UniformListState(Rc<RefCell<StateInner>>);
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
/// uniform_list will only render the visible subset of items.
#[track_caller]
pub fn uniform_list<I, R, V>(
view: View<V>,
id: I,
item_count: usize,
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
) -> UniformList
where
I: Into<ElementId>,
R: IntoElement,
V: Render,
{
let id = id.into();
let mut base_style = StyleRefinement::default();
base_style.overflow.y = Some(Overflow::Scroll);
#[derive(Debug)]
pub enum ScrollTarget {
Show(usize),
Center(usize),
let render_range = move |range, cx: &mut WindowContext| {
view.update(cx, |this, cx| {
f(this, range, cx)
.into_iter()
.map(|component| component.into_any_element())
.collect()
})
};
UniformList {
id: id.clone(),
item_count,
item_to_measure_index: 0,
render_items: Box::new(render_range),
interactivity: Interactivity {
element_id: Some(id),
base_style: Box::new(base_style),
#[cfg(debug_assertions)]
location: Some(*core::panic::Location::caller()),
..Default::default()
},
scroll_handle: None,
}
}
impl UniformListState {
pub fn scroll_to(&self, scroll_to: ScrollTarget) {
self.0.borrow_mut().scroll_to = Some(scroll_to);
pub struct UniformList {
id: ElementId,
item_count: usize,
item_to_measure_index: usize,
render_items:
Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
interactivity: Interactivity,
scroll_handle: Option<UniformListScrollHandle>,
}
#[derive(Clone, Default)]
pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
#[derive(Clone, Debug)]
struct ScrollHandleState {
item_height: Pixels,
list_height: Pixels,
scroll_offset: Rc<RefCell<Point<Pixels>>>,
}
impl UniformListScrollHandle {
pub fn new() -> Self {
Self(Rc::new(RefCell::new(None)))
}
pub fn scroll_top(&self) -> f32 {
self.0.borrow().scroll_top
pub fn scroll_to_item(&self, ix: usize) {
if let Some(state) = &*self.0.borrow() {
let mut scroll_offset = state.scroll_offset.borrow_mut();
let item_top = state.item_height * ix;
let item_bottom = item_top + state.item_height;
let scroll_top = -scroll_offset.y;
if item_top < scroll_top {
scroll_offset.y = -item_top;
} else if item_bottom > scroll_top + state.list_height {
scroll_offset.y = -(item_bottom - state.list_height);
}
}
}
pub fn scroll_top(&self) -> Pixels {
if let Some(state) = &*self.0.borrow() {
-state.scroll_offset.borrow().y
} else {
Pixels::ZERO
}
}
}
impl Styled for UniformList {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
#[derive(Default)]
struct StateInner {
scroll_top: f32,
scroll_to: Option<ScrollTarget>,
pub struct UniformListState {
interactive: InteractiveElementState,
item_size: Size<Pixels>,
}
pub struct UniformListLayoutState<V> {
scroll_max: f32,
item_height: f32,
items: Vec<AnyElement<V>>,
}
impl Element for UniformList {
type State = UniformListState;
pub struct UniformList<V> {
state: UniformListState,
item_count: usize,
#[allow(clippy::type_complexity)]
append_items: Box<dyn Fn(&mut V, Range<usize>, &mut Vec<AnyElement<V>>, &mut ViewContext<V>)>,
padding_top: f32,
padding_bottom: f32,
get_width_from_item: Option<usize>,
view_id: usize,
}
impl<V: 'static> UniformList<V> {
pub fn new<F>(
state: UniformListState,
item_count: usize,
cx: &mut ViewContext<V>,
append_items: F,
) -> Self
where
F: 'static + Fn(&mut V, Range<usize>, &mut Vec<AnyElement<V>>, &mut ViewContext<V>),
{
Self {
state,
item_count,
append_items: Box::new(append_items),
padding_top: 0.,
padding_bottom: 0.,
get_width_from_item: None,
view_id: cx.handle().id(),
}
}
pub fn with_width_from_item(mut self, item_ix: Option<usize>) -> Self {
self.get_width_from_item = item_ix;
self
}
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(
state: UniformListState,
_: Vector2F,
mut delta: Vector2F,
precise: bool,
scroll_max: f32,
cx: &mut ViewContext<V>,
) -> bool {
if !precise {
delta *= 20.;
}
let mut state = state.0.borrow_mut();
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.borrow_mut();
if let Some(scroll_to) = state.scroll_to.take() {
let item_ix;
let center;
match scroll_to {
ScrollTarget::Show(ix) => {
item_ix = ix;
center = false;
}
ScrollTarget::Center(ix) => {
item_ix = ix;
center = true;
}
}
let item_top = self.padding_top + item_ix as f32 * item_height;
let item_bottom = item_top + item_height;
if center {
let item_center = item_top + item_height / 2.;
state.scroll_top = (item_center - list_height / 2.).max(0.);
} else {
let scroll_bottom = state.scroll_top + list_height;
if item_top < state.scroll_top {
state.scroll_top = item_top;
} else if item_bottom > scroll_bottom {
state.scroll_top = item_bottom - list_height;
}
}
}
if state.scroll_top > scroll_max {
state.scroll_top = scroll_max;
}
}
fn scroll_top(&self) -> f32 {
self.state.0.borrow().scroll_top
}
}
impl<V: 'static> Element<V> for UniformList<V> {
type LayoutState = UniformListLayoutState<V>;
type PaintState = ();
fn layout(
fn request_layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> (Vector2F, Self::LayoutState) {
if constraint.max.y().is_infinite() {
unimplemented!(
"UniformList does not support being rendered with an unconstrained height"
);
}
state: Option<Self::State>,
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let max_items = self.item_count;
let item_size = state
.as_ref()
.map(|s| s.item_size)
.unwrap_or_else(|| self.measure_item(None, cx));
let no_items = (
constraint.min,
UniformListLayoutState {
item_height: 0.,
scroll_max: 0.,
items: Default::default(),
},
);
let (layout_id, interactive) =
self.interactivity
.layout(state.map(|s| s.interactive), cx, |style, cx| {
cx.request_measured_layout(
style,
move |known_dimensions, available_space, _cx| {
let desired_height = item_size.height * max_items;
let width =
known_dimensions
.width
.unwrap_or(match available_space.width {
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_size.width
}
});
if self.item_count == 0 {
return no_items;
}
let height = match available_space.height {
AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
desired_height
}
};
size(width, height)
},
)
});
let mut items = Vec::new();
let mut size = constraint.max;
let mut item_size;
let sample_item_ix;
let sample_item;
if let Some(sample_ix) = self.get_width_from_item {
(self.append_items)(view, sample_ix..sample_ix + 1, &mut items, cx);
sample_item_ix = sample_ix;
if let Some(mut item) = items.pop() {
item_size = item.layout(constraint, view, cx);
size.set_x(item_size.x());
sample_item = item;
} else {
return no_items;
}
} else {
(self.append_items)(view, 0..1, &mut items, cx);
sample_item_ix = 0;
if let Some(mut item) = items.pop() {
item_size = item.layout(
SizeConstraint::new(
vec2f(constraint.max.x(), 0.0),
vec2f(constraint.max.x(), f32::INFINITY),
),
view,
cx,
);
item_size.set_x(size.x());
sample_item = item
} else {
return no_items;
}
}
let item_constraint = SizeConstraint {
min: item_size,
max: vec2f(constraint.max.x(), item_size.y()),
let element_state = UniformListState {
interactive,
item_size,
};
let 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;
let scroll_max = (scroll_height - size.y()).max(0.);
self.autoscroll(scroll_max, size.y(), item_height);
let start = cmp::min(
((self.scroll_top() - self.padding_top) / item_height.max(1.)) as usize,
self.item_count,
);
let end = cmp::min(
self.item_count,
start + (size.y() / item_height.max(1.)).ceil() as usize + 1,
);
if (start..end).contains(&sample_item_ix) {
if sample_item_ix > start {
(self.append_items)(view, start..sample_item_ix, &mut items, cx);
}
items.push(sample_item);
if sample_item_ix < end {
(self.append_items)(view, sample_item_ix + 1..end, &mut items, cx);
}
} else {
(self.append_items)(view, start..end, &mut items, cx);
}
for item in &mut items {
let item_size = item.layout(item_constraint, view, cx);
if item_size.x() > size.x() {
size.set_x(item_size.x());
}
}
(
size,
UniformListLayoutState {
item_height,
scroll_max,
items,
},
)
(layout_id, element_state)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
layout: &mut Self::LayoutState,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
bounds: Bounds<crate::Pixels>,
element_state: &mut Self::State,
cx: &mut WindowContext,
) {
let style =
self.interactivity
.compute_style(Some(bounds), &mut element_state.interactive, cx);
let border = style.border_widths.to_pixels(cx.rem_size());
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
cx.scene().push_layer(Some(visible_bounds));
cx.scene().push_mouse_region(
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
let scroll_max = layout.scroll_max;
let state = self.state.clone();
move |event, _, cx| {
let ScrollWheelEvent {
position, delta, ..
} = event.platform_event;
if !Self::scroll(
state.clone(),
position,
*delta.raw(),
delta.precise(),
scroll_max,
cx,
) {
cx.propagate_event();
}
}
}),
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top + padding.top),
bounds.lower_right()
- point(border.right + padding.right, border.bottom + padding.bottom),
);
let mut item_origin = bounds.origin()
- vec2f(
0.,
(self.state.scroll_top() - self.padding_top) % layout.item_height,
);
let item_size = element_state.item_size;
let content_size = Size {
width: padded_bounds.size.width,
height: item_size.height * self.item_count + padding.top + padding.bottom,
};
for item in &mut layout.items {
item.paint(item_origin, visible_bounds, view, cx);
item_origin += vec2f(0.0, layout.item_height);
}
let shared_scroll_offset = element_state
.interactive
.scroll_offset
.get_or_insert_with(|| {
if let Some(scroll_handle) = self.scroll_handle.as_ref() {
if let Some(scroll_handle) = scroll_handle.0.borrow().as_ref() {
return scroll_handle.scroll_offset.clone();
}
}
cx.scene().pop_layer();
}
Rc::default()
})
.clone();
fn rect_for_text_range(
&self,
range: Range<usize>,
_: RectF,
_: RectF,
layout: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
layout
.items
.iter()
.find_map(|child| child.rect_for_text_range(range.clone(), view, cx))
}
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
fn debug(
&self,
bounds: RectF,
layout: &Self::LayoutState,
_: &Self::PaintState,
view: &V,
cx: &ViewContext<V>,
) -> 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(view, cx)).collect::<Vec<json::Value>>()
self.interactivity.paint(
bounds,
content_size,
&mut element_state.interactive,
cx,
|style, mut scroll_offset, cx| {
let border = style.border_widths.to_pixels(cx.rem_size());
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
})
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top),
bounds.lower_right() - point(border.right + padding.right, border.bottom),
);
if self.item_count > 0 {
let content_height =
item_height * self.item_count + padding.top + padding.bottom;
let min_scroll_offset = padded_bounds.size.height - content_height;
let is_scrolled = scroll_offset.y != px(0.);
if is_scrolled && scroll_offset.y < min_scroll_offset {
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
scroll_offset.y = min_scroll_offset;
}
if let Some(scroll_handle) = self.scroll_handle.clone() {
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
item_height,
list_height: padded_bounds.size.height,
scroll_offset: shared_scroll_offset,
});
}
let first_visible_element_ix =
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
/ item_height)
.ceil() as usize;
let visible_range = first_visible_element_ix
..cmp::min(last_visible_element_ix, self.item_count);
let mut items = (self.render_items)(visible_range.clone(), cx);
cx.with_z_index(1, |cx| {
let content_mask = ContentMask { bounds };
cx.with_content_mask(Some(content_mask), |cx| {
for (item, ix) in items.iter_mut().zip(visible_range) {
let item_origin = padded_bounds.origin
+ point(
px(0.),
item_height * ix + scroll_offset.y + padding.top,
);
let available_space = size(
AvailableSpace::Definite(padded_bounds.size.width),
AvailableSpace::Definite(item_height),
);
item.draw(item_origin, available_space, cx);
}
});
});
}
},
)
}
}
impl IntoElement for UniformList {
type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
Some(self.id.clone())
}
fn into_element(self) -> Self::Element {
self
}
}
impl UniformList {
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
self.item_to_measure_index = item_index.unwrap_or(0);
self
}
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
if self.item_count == 0 {
return Size::default();
}
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
let mut item_to_measure = items.pop().unwrap();
let available_space = size(
list_width.map_or(AvailableSpace::MinContent, |width| {
AvailableSpace::Definite(width)
}),
AvailableSpace::MinContent,
);
item_to_measure.measure(available_space, cx)
}
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
self.scroll_handle = Some(handle);
self
}
}
impl InteractiveElement for UniformList {
fn interactivity(&mut self) -> &mut crate::Interactivity {
&mut self.interactivity
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,330 +0,0 @@
use crate::{
fonts::{Features, 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 schemars::JsonSchema;
use std::{
collections::HashMap,
ops::{Deref, DerefMut},
sync::Arc,
};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
pub struct FamilyId(usize);
struct Family {
name: Arc<str>,
font_features: Features,
font_ids: Vec<FontId>,
}
pub struct FontCache(RwLock<FontCacheState>);
pub struct FontCacheState {
font_system: Arc<dyn platform::FontSystem>,
families: Vec<Family>,
default_family: Option<FamilyId>,
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 {
font_system: fonts,
families: Default::default(),
default_family: None,
font_selections: Default::default(),
metrics: Default::default(),
wrapper_pool: Default::default(),
}))
}
pub fn family_name(&self, family_id: 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], features: &Features) -> 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 && f.font_features == *features)
{
return Ok(FamilyId(ix));
}
let mut state = RwLockUpgradableReadGuard::upgrade(state);
if let Ok(font_ids) = state.font_system.load_family(name, features) {
if font_ids.is_empty() {
continue;
}
let family_id = FamilyId(state.families.len());
for font_id in &font_ids {
if state.font_system.glyph_for_char(*font_id, 'm').is_none() {
return Err(anyhow!("font must contain a glyph for the 'm' character"));
}
}
state.families.push(Family {
name: Arc::from(*name),
font_features: features.clone(),
font_ids,
});
return Ok(family_id);
}
}
Err(anyhow!(
"could not find a non-empty font family matching one of the given names: {}",
names
.iter()
.map(|name| format!("`{name}`"))
.collect::<Vec<_>>()
.join(", ")
))
}
/// Returns an arbitrary font family that is available on the system.
pub fn known_existing_family(&self) -> FamilyId {
if let Some(family_id) = self.0.read().default_family {
return family_id;
}
let default_family = self
.load_family(
&["Courier", "Helvetica", "Arial", "Verdana"],
&Default::default(),
)
.unwrap_or_else(|_| {
let all_family_names = self.0.read().font_system.all_families();
let all_family_names: Vec<_> = all_family_names
.iter()
.map(|string| string.as_str())
.collect();
self.load_family(&all_family_names, &Default::default())
.expect("could not load any default font family")
});
self.0.write().default_family = Some(default_family);
default_family
}
pub fn default_font(&self, family_id: 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
.font_system
.select_font(&family.font_ids, properties)
.unwrap_or(family.font_ids[0]);
inner
.font_selections
.entry(family_id)
.or_default()
.insert(*properties, 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.font_system.font_metrics(font_id);
let metric = f(&metrics);
let mut state = RwLockUpgradableReadGuard::upgrade(state);
state.metrics.insert(font_id, metrics);
metric
}
}
pub fn bounding_box(&self, font_id: FontId, font_size: 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.font_system.glyph_for_char(font_id, 'm').unwrap();
bounds = state
.font_system
.typographic_bounds(font_id, glyph_id)
.unwrap();
}
bounds.width() * self.em_scale(font_id, font_size)
}
pub fn em_advance(&self, font_id: FontId, font_size: f32) -> f32 {
let glyph_id;
let advance;
{
let state = self.0.read();
glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
advance = state.font_system.advance(font_id, glyph_id).unwrap();
}
advance.x() * self.em_scale(font_id, font_size)
}
pub fn line_height(&self, font_size: f32) -> f32 {
(font_size * 1.618).round()
}
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_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.font_system.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"],
&Features {
calt: Some(false),
..Default::default()
},
)
.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);
let arial_with_calt = fonts
.load_family(
&["Arial"],
&Features {
calt: Some(true),
..Default::default()
},
)
.unwrap();
assert_ne!(arial_with_calt, arial);
}
}

View file

@ -1,636 +0,0 @@
use crate::{
color::Color,
font_cache::FamilyId,
json::{json, ToJson},
text_layout::RunStyle,
FontCache,
};
use anyhow::{anyhow, Result};
pub use font_kit::{
metrics::Metrics,
properties::{Properties, Stretch, Style, Weight},
};
use ordered_float::OrderedFloat;
use refineable::Refineable;
use schemars::JsonSchema;
use serde::{de, Deserialize, Serialize};
use serde_json::Value;
use std::{cell::RefCell, sync::Arc};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
pub struct FontId(pub usize);
pub type GlyphId = u32;
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Features {
pub calt: Option<bool>,
pub case: Option<bool>,
pub cpsp: Option<bool>,
pub frac: Option<bool>,
pub liga: Option<bool>,
pub onum: Option<bool>,
pub ordn: Option<bool>,
pub pnum: Option<bool>,
pub ss01: Option<bool>,
pub ss02: Option<bool>,
pub ss03: Option<bool>,
pub ss04: Option<bool>,
pub ss05: Option<bool>,
pub ss06: Option<bool>,
pub ss07: Option<bool>,
pub ss08: Option<bool>,
pub ss09: Option<bool>,
pub ss10: Option<bool>,
pub ss11: Option<bool>,
pub ss12: Option<bool>,
pub ss13: Option<bool>,
pub ss14: Option<bool>,
pub ss15: Option<bool>,
pub ss16: Option<bool>,
pub ss17: Option<bool>,
pub ss18: Option<bool>,
pub ss19: Option<bool>,
pub ss20: Option<bool>,
pub subs: Option<bool>,
pub sups: Option<bool>,
pub swsh: Option<bool>,
pub titl: Option<bool>,
pub tnum: Option<bool>,
pub zero: Option<bool>,
}
#[derive(Clone, Debug, JsonSchema)]
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,
#[schemars(with = "PropertiesDef")]
pub font_properties: Properties,
pub underline: Underline,
pub soft_wrap: bool,
}
impl TextStyle {
pub fn for_color(color: Color) -> Self {
Self {
color,
..Default::default()
}
}
}
impl TextStyle {
pub fn refine(
&mut self,
refinement: &TextStyleRefinement,
font_cache: &FontCache,
) -> Result<()> {
if let Some(font_size) = refinement.font_size {
self.font_size = font_size;
}
if let Some(color) = refinement.color {
self.color = color;
}
if let Some(underline) = refinement.underline {
self.underline = underline;
}
let mut update_font_id = false;
if let Some(font_family) = refinement.font_family.clone() {
self.font_family_id = font_cache.load_family(&[&font_family], &Default::default())?;
self.font_family_name = font_family;
update_font_id = true;
}
if let Some(font_weight) = refinement.font_weight {
self.font_properties.weight = font_weight;
update_font_id = true;
}
if let Some(font_style) = refinement.font_style {
self.font_properties.style = font_style;
update_font_id = true;
}
if update_font_id {
self.font_id = font_cache.select_font(self.font_family_id, &self.font_properties)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default)]
pub struct TextStyleRefinement {
pub color: Option<Color>,
pub font_family: Option<Arc<str>>,
pub font_size: Option<f32>,
pub font_weight: Option<Weight>,
pub font_style: Option<Style>,
pub underline: Option<Underline>,
}
impl Refineable for TextStyleRefinement {
type Refinement = Self;
fn refine(&mut self, refinement: &Self::Refinement) {
if refinement.color.is_some() {
self.color = refinement.color;
}
if refinement.font_family.is_some() {
self.font_family = refinement.font_family.clone();
}
if refinement.font_size.is_some() {
self.font_size = refinement.font_size;
}
if refinement.font_weight.is_some() {
self.font_weight = refinement.font_weight;
}
if refinement.font_style.is_some() {
self.font_style = refinement.font_style;
}
if refinement.underline.is_some() {
self.underline = refinement.underline;
}
}
fn refined(mut self, refinement: Self::Refinement) -> Self {
self.refine(&refinement);
self
}
}
#[derive(JsonSchema)]
#[serde(remote = "Properties")]
pub struct PropertiesDef {
/// The font style, as defined in CSS.
pub style: StyleDef,
/// The font weight, as defined in CSS.
pub weight: f32,
/// The font stretchiness, as defined in CSS.
pub stretch: f32,
}
#[derive(JsonSchema)]
#[schemars(remote = "Style")]
pub enum StyleDef {
/// A face that is neither italic not obliqued.
Normal,
/// A form that is generally cursive in nature.
Italic,
/// A typically-sloped version of the regular face.
Oblique,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, JsonSchema)]
pub struct HighlightStyle {
pub color: Option<Color>,
#[schemars(with = "Option::<f32>")]
pub weight: Option<Weight>,
pub italic: Option<bool>,
pub underline: Option<Underline>,
pub fade_out: Option<f32>,
}
impl Eq for HighlightStyle {}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, JsonSchema)]
pub struct Underline {
pub color: Option<Color>,
#[schemars(with = "f32")]
pub thickness: OrderedFloat<f32>,
pub squiggly: 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,
#[serde(default)]
features: Features,
weight: Option<WeightJson>,
size: f32,
#[serde(default)]
italic: bool,
#[serde(default)]
underline: UnderlineStyleJson,
}
#[derive(Deserialize)]
struct HighlightStyleJson {
color: Option<Color>,
weight: Option<WeightJson>,
italic: Option<bool>,
underline: Option<UnderlineStyleJson>,
fade_out: Option<f32>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum UnderlineStyleJson {
Underlined(bool),
UnderlinedWithProperties {
#[serde(default)]
color: Option<Color>,
#[serde(default)]
thickness: Option<f32>,
#[serde(default)]
squiggly: bool,
},
}
impl TextStyle {
pub fn new(
font_family_name: impl Into<Arc<str>>,
font_size: f32,
font_properties: Properties,
font_features: Features,
underline: Underline,
color: Color,
font_cache: &FontCache,
) -> Result<Self> {
let font_family_name = font_family_name.into();
let font_family_id = font_cache.load_family(&[&font_family_name], &font_features)?;
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,
soft_wrap: false,
})
}
pub fn default(font_cache: &FontCache) -> Self {
let font_family_id = font_cache.known_existing_family();
let font_id = font_cache
.select_font(font_family_id, &Default::default())
.expect("did not have any font in system-provided family");
let font_family_name = font_cache
.family_name(font_family_id)
.expect("we loaded this family from the font cache, so this should work");
Self {
color: Color::default(),
font_family_name,
font_family_id,
font_id,
font_size: 14.,
font_properties: Default::default(),
underline: Default::default(),
soft_wrap: true,
}
}
pub fn with_font_size(mut self, font_size: f32) -> Self {
self.font_size = font_size;
self
}
pub fn highlight(mut self, style: HighlightStyle, font_cache: &FontCache) -> Result<Self> {
let mut font_properties = self.font_properties;
if let Some(weight) = style.weight {
font_properties.weight(weight);
}
if let Some(italic) = style.italic {
if italic {
font_properties.style(Style::Italic);
} else {
font_properties.style(Style::Normal);
}
}
if self.font_properties != font_properties {
self.font_id = font_cache.select_font(self.font_family_id, &font_properties)?;
}
if let Some(color) = style.color {
self.color = Color::blend(color, self.color);
}
if let Some(factor) = style.fade_out {
self.color.fade_out(factor);
}
if let Some(underline) = style.underline {
self.underline = underline;
}
Ok(self)
}
pub fn to_run(&self) -> RunStyle {
RunStyle {
font_id: self.font_id,
color: self.color,
underline: self.underline,
}
}
fn from_json(json: TextStyleJson) -> 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.features,
underline_from_json(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_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 em_advance(&self, font_cache: &FontCache) -> f32 {
font_cache.em_advance(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::from(&other)
}
}
impl From<&TextStyle> for HighlightStyle {
fn from(other: &TextStyle) -> Self {
Self {
color: Some(other.color),
weight: Some(other.font_properties.weight),
italic: Some(other.font_properties.style == Style::Italic),
underline: Some(other.underline),
fade_out: None,
}
}
}
impl Default for UnderlineStyleJson {
fn default() -> Self {
Self::Underlined(false)
}
}
impl Default for TextStyle {
fn default() -> Self {
FONT_CACHE.with(|font_cache| {
let font_cache = font_cache.borrow();
let font_cache = font_cache
.as_ref()
.expect("TextStyle::default can only be called within a call to with_font_cache");
Self::default(font_cache)
})
}
}
impl HighlightStyle {
fn from_json(json: HighlightStyleJson) -> Self {
Self {
color: json.color,
weight: json.weight.map(weight_from_json),
italic: json.italic,
underline: json.underline.map(underline_from_json),
fade_out: json.fade_out,
}
}
pub fn highlight(&mut self, other: HighlightStyle) {
match (self.color, other.color) {
(Some(self_color), Some(other_color)) => {
self.color = Some(Color::blend(other_color, self_color));
}
(None, Some(other_color)) => {
self.color = Some(other_color);
}
_ => {}
}
if other.weight.is_some() {
self.weight = other.weight;
}
if other.italic.is_some() {
self.italic = other.italic;
}
if other.underline.is_some() {
self.underline = other.underline;
}
match (other.fade_out, self.fade_out) {
(Some(source_fade), None) => self.fade_out = Some(source_fade),
(Some(source_fade), Some(dest_fade)) => {
let source_alpha = 1. - source_fade;
let dest_alpha = 1. - dest_fade;
let blended_alpha = source_alpha + (dest_alpha * source_fade);
let blended_fade = 1. - blended_alpha;
self.fade_out = Some(blended_fade);
}
_ => {}
}
}
}
impl From<Color> for HighlightStyle {
fn from(color: Color) -> Self {
Self {
color: Some(color),
..Default::default()
}
}
}
impl<'de> Deserialize<'de> for TextStyle {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Self::from_json(TextStyleJson::deserialize(deserializer)?).map_err(de::Error::custom)
}
}
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)?,
..Default::default()
})
}
}
}
fn underline_from_json(json: UnderlineStyleJson) -> Underline {
match json {
UnderlineStyleJson::Underlined(false) => Underline::default(),
UnderlineStyleJson::Underlined(true) => Underline {
color: None,
thickness: 1.0.into(),
squiggly: false,
},
UnderlineStyleJson::UnderlinedWithProperties {
color,
thickness,
squiggly,
} => Underline {
color,
thickness: thickness.unwrap_or(1.).into(),
squiggly,
},
}
}
fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
let weight = weight.map(weight_from_json).unwrap_or_default();
let style = if italic { Style::Italic } else { Style::Normal };
*Properties::new().weight(weight).style(style)
}
fn weight_from_json(weight: WeightJson) -> Weight {
match weight {
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,
}
}
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
})
}

File diff suppressed because it is too large Load diff

View file

@ -1,40 +1,215 @@
#[macro_use]
mod action;
mod app;
mod image_cache;
pub use app::*;
mod arena;
mod assets;
mod color;
mod element;
mod elements;
mod executor;
mod geometry;
mod image_cache;
mod input;
mod interactive;
mod key_dispatch;
mod keymap;
mod platform;
pub mod prelude;
mod scene;
mod shared_string;
mod style;
mod styled;
mod subscription;
mod svg_renderer;
mod taffy;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
pub use assets::*;
pub mod elements;
pub mod font_cache;
mod image_data;
pub use crate::image_data::ImageData;
pub use taffy;
pub mod views;
pub use font_cache::FontCache;
mod clipboard;
pub use clipboard::ClipboardItem;
pub mod fonts;
pub mod geometry;
pub mod scene;
pub use scene::{Border, CursorRegion, MouseRegion, MouseRegionId, Quad, Scene, SceneBuilder};
pub mod text_layout;
pub use text_layout::TextLayoutCache;
mod text_system;
mod util;
pub use elements::{AnyElement, Element};
pub mod executor;
pub use executor::Task;
pub mod color;
pub mod json;
pub mod keymap_matcher;
pub mod platform;
pub use gpui_macros::{test, Element};
pub use usvg;
pub use window::{
Axis, Layout, LayoutEngine, LayoutId, RectFExt, SizeConstraint, Vector2FExt, WindowContext,
};
mod view;
mod window;
pub use anyhow;
mod private {
/// A mechanism for restricting implementations of a trait to only those in GPUI.
/// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/
pub trait Sealed {}
}
pub use action::*;
pub use anyhow::Result;
pub use app::*;
pub(crate) use arena::*;
pub use assets::*;
pub use color::*;
pub use ctor::ctor;
pub use element::*;
pub use elements::*;
pub use executor::*;
pub use geometry::*;
pub use gpui2_macros::*;
pub use image_cache::*;
pub use input::*;
pub use interactive::*;
pub use key_dispatch::*;
pub use keymap::*;
pub use linkme;
pub use platform::*;
use private::Sealed;
pub use refineable::*;
pub use scene::*;
pub use serde;
pub use serde_derive;
pub use serde_json;
pub use shared_string::*;
pub use smallvec;
pub use smol::Timer;
pub use style::*;
pub use styled::*;
pub use subscription::*;
pub use svg_renderer::*;
pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
pub use text_system::*;
pub use util::arc_cow::ArcCow;
pub use view::*;
pub use window::*;
actions!(zed, [NoAction]);
use std::{
any::{Any, TypeId},
borrow::BorrowMut,
};
use taffy::TaffyLayoutEngine;
pub trait Context {
type Result<T>;
fn new_model<T: 'static>(
&mut self,
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
) -> Self::Result<Model<T>>;
fn update_model<T, R>(
&mut self,
handle: &Model<T>,
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> Self::Result<R>
where
T: 'static;
fn read_model<T, R>(
&self,
handle: &Model<T>,
read: impl FnOnce(&T, &AppContext) -> R,
) -> Self::Result<R>
where
T: 'static;
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T;
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static;
}
pub trait VisualContext: Context {
fn new_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
V: 'static + Render;
fn update_view<V: 'static, R>(
&mut self,
view: &View<V>,
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
) -> Self::Result<R>;
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
V: 'static + Render;
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: FocusableView;
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
V: ManagedView;
}
pub trait Entity<T>: Sealed {
type Weak: 'static;
fn entity_id(&self) -> EntityId;
fn downgrade(&self) -> Self::Weak;
fn upgrade_from(weak: &Self::Weak) -> Option<Self>
where
Self: Sized;
}
pub trait EventEmitter<E: Any>: 'static {}
pub enum GlobalKey {
Numeric(usize),
View(EntityId),
Type(TypeId),
}
pub trait BorrowAppContext {
fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
where
F: FnOnce(&mut Self) -> R;
fn set_global<T: 'static>(&mut self, global: T);
}
impl<C> BorrowAppContext for C
where
C: BorrowMut<AppContext>,
{
fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
if let Some(style) = style {
self.borrow_mut().push_text_style(style);
let result = f(self);
self.borrow_mut().pop_text_style();
result
} else {
f(self)
}
}
fn set_global<G: 'static>(&mut self, global: G) {
self.borrow_mut().set_global(global)
}
}
pub trait Flatten<T> {
fn flatten(self) -> Result<T>;
}
impl<T> Flatten<T> for Result<Result<T>> {
fn flatten(self) -> Result<T> {
self?
}
}
impl<T> Flatten<T> for Result<T> {
fn flatten(self) -> Result<T> {
self
}
}

View file

@ -1,18 +1,19 @@
use std::sync::Arc;
use crate::ImageData;
use crate::{ImageData, ImageId, SharedString};
use collections::HashMap;
use futures::{
future::{BoxFuture, Shared},
AsyncReadExt, FutureExt,
AsyncReadExt, FutureExt, TryFutureExt,
};
use image::ImageError;
use parking_lot::Mutex;
use std::sync::Arc;
use thiserror::Error;
use util::{
arc_cow::ArcCow,
http::{self, HttpClient},
};
use util::http::{self, HttpClient};
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct RenderImageParams {
pub(crate) image_id: ImageId,
}
#[derive(Debug, Error, Clone)]
pub enum Error {
@ -43,7 +44,7 @@ impl From<ImageError> for Error {
pub struct ImageCache {
client: Arc<dyn HttpClient>,
images: Arc<Mutex<HashMap<ArcCow<'static, str>, FetchImageFuture>>>,
images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>,
}
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
@ -58,12 +59,12 @@ impl ImageCache {
pub fn get(
&self,
uri: impl Into<ArcCow<'static, str>>,
uri: impl Into<SharedString>,
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
let uri = uri.into();
let mut images = self.images.lock();
match images.get(uri.as_ref()) {
match images.get(&uri) {
Some(future) => future.clone(),
None => {
let client = self.client.clone();
@ -84,9 +85,17 @@ impl ImageCache {
let format = image::guess_format(&body)?;
let image =
image::load_from_memory_with_format(&body, format)?.into_bgra8();
Ok(ImageData::new(image))
Ok(Arc::new(ImageData::new(image)))
}
}
.map_err({
let uri = uri.clone();
move |error| {
log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
error
}
})
.boxed()
.shared();

View file

@ -1,43 +0,0 @@
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()
}
}

116
crates/gpui/src/input.rs Normal file
View file

@ -0,0 +1,116 @@
use crate::{
AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext,
};
use std::ops::Range;
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
///
/// Once your view `V` implements this trait, you can use it to construct an [ElementInputHandler<V>].
/// This input handler can then be assigned during paint by calling [WindowContext::handle_input].
pub trait InputHandler: 'static + Sized {
fn text_for_range(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>)
-> Option<String>;
fn selected_text_range(&mut self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
fn unmark_text(&mut self, cx: &mut ViewContext<Self>);
fn replace_text_in_range(
&mut self,
range: Option<Range<usize>>,
text: &str,
cx: &mut ViewContext<Self>,
);
fn replace_and_mark_text_in_range(
&mut self,
range: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
cx: &mut ViewContext<Self>,
);
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
element_bounds: Bounds<Pixels>,
cx: &mut ViewContext<Self>,
) -> Option<Bounds<Pixels>>;
}
/// The canonical implementation of `PlatformInputHandler`. Call `WindowContext::handle_input`
/// with an instance during your element's paint.
pub struct ElementInputHandler<V> {
view: View<V>,
element_bounds: Bounds<Pixels>,
cx: AsyncWindowContext,
}
impl<V: 'static> ElementInputHandler<V> {
/// Used in [Element::paint] with the element's bounds and a view context for its
/// containing view.
pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self {
ElementInputHandler {
view,
element_bounds,
cx: cx.to_async(),
}
}
}
impl<V: InputHandler> PlatformInputHandler for ElementInputHandler<V> {
fn selected_text_range(&mut self) -> Option<Range<usize>> {
self.view
.update(&mut self.cx, |view, cx| view.selected_text_range(cx))
.ok()
.flatten()
}
fn marked_text_range(&mut self) -> Option<Range<usize>> {
self.view
.update(&mut self.cx, |view, cx| view.marked_text_range(cx))
.ok()
.flatten()
}
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
self.view
.update(&mut self.cx, |view, cx| {
view.text_for_range(range_utf16, cx)
})
.ok()
.flatten()
}
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
self.view
.update(&mut self.cx, |view, cx| {
view.replace_text_in_range(replacement_range, text, cx)
})
.ok();
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
) {
self.view
.update(&mut self.cx, |view, cx| {
view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx)
})
.ok();
}
fn unmark_text(&mut self) {
self.view
.update(&mut self.cx, |view, cx| view.unmark_text(cx))
.ok();
}
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
self.view
.update(&mut self.cx, |view, cx| {
view.bounds_for_range(range_utf16, self.element_bounds, cx)
})
.ok()
.flatten()
}
}

View file

@ -0,0 +1,356 @@
use crate::{
div, point, Element, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext,
};
use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeyDownEvent {
pub keystroke: Keystroke,
pub is_held: bool,
}
#[derive(Clone, Debug)]
pub struct KeyUpEvent {
pub keystroke: Keystroke,
}
#[derive(Clone, Debug, Default)]
pub struct ModifiersChangedEvent {
pub modifiers: Modifiers,
}
impl Deref for ModifiersChangedEvent {
type Target = Modifiers;
fn deref(&self) -> &Self::Target {
&self.modifiers
}
}
/// The phase of a touch motion event.
/// Based on the winit enum of the same name.
#[derive(Clone, Copy, Debug)]
pub enum TouchPhase {
Started,
Moved,
Ended,
}
#[derive(Clone, Debug, Default)]
pub struct MouseDownEvent {
pub button: MouseButton,
pub position: Point<Pixels>,
pub modifiers: Modifiers,
pub click_count: usize,
}
#[derive(Clone, Debug, Default)]
pub struct MouseUpEvent {
pub button: MouseButton,
pub position: Point<Pixels>,
pub modifiers: Modifiers,
pub click_count: usize,
}
#[derive(Clone, Debug, Default)]
pub struct ClickEvent {
pub down: MouseDownEvent,
pub up: MouseUpEvent,
}
pub struct Drag<S, R, V, E>
where
R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static,
E: IntoElement,
{
pub state: S,
pub render_drag_handle: R,
view_element_types: PhantomData<(V, E)>,
}
impl<S, R, V, E> Drag<S, R, V, E>
where
R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static,
E: Element,
{
pub fn new(state: S, render_drag_handle: R) -> Self {
Drag {
state,
render_drag_handle,
view_element_types: Default::default(),
}
}
}
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum MouseButton {
Left,
Right,
Middle,
Navigate(NavigationDirection),
}
impl MouseButton {
pub fn all() -> Vec<Self> {
vec![
MouseButton::Left,
MouseButton::Right,
MouseButton::Middle,
MouseButton::Navigate(NavigationDirection::Back),
MouseButton::Navigate(NavigationDirection::Forward),
]
}
}
impl Default for MouseButton {
fn default() -> Self {
Self::Left
}
}
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum NavigationDirection {
Back,
Forward,
}
impl Default for NavigationDirection {
fn default() -> Self {
Self::Back
}
}
#[derive(Clone, Debug, Default)]
pub struct MouseMoveEvent {
pub position: Point<Pixels>,
pub pressed_button: Option<MouseButton>,
pub modifiers: Modifiers,
}
impl MouseMoveEvent {
pub fn dragging(&self) -> bool {
self.pressed_button == Some(MouseButton::Left)
}
}
#[derive(Clone, Debug)]
pub struct ScrollWheelEvent {
pub position: Point<Pixels>,
pub delta: ScrollDelta,
pub modifiers: Modifiers,
pub touch_phase: TouchPhase,
}
impl Deref for ScrollWheelEvent {
type Target = Modifiers;
fn deref(&self) -> &Self::Target {
&self.modifiers
}
}
#[derive(Clone, Copy, Debug)]
pub enum ScrollDelta {
Pixels(Point<Pixels>),
Lines(Point<f32>),
}
impl Default for ScrollDelta {
fn default() -> Self {
Self::Lines(Default::default())
}
}
impl ScrollDelta {
pub fn precise(&self) -> bool {
match self {
ScrollDelta::Pixels(_) => true,
ScrollDelta::Lines(_) => false,
}
}
pub fn pixel_delta(&self, line_height: Pixels) -> Point<Pixels> {
match self {
ScrollDelta::Pixels(delta) => *delta,
ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct MouseExitEvent {
pub position: Point<Pixels>,
pub pressed_button: Option<MouseButton>,
pub modifiers: Modifiers,
}
impl Deref for MouseExitEvent {
type Target = Modifiers;
fn deref(&self) -> &Self::Target {
&self.modifiers
}
}
#[derive(Debug, Clone, Default)]
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
impl ExternalPaths {
pub fn paths(&self) -> &[PathBuf] {
&self.0
}
}
impl Render for ExternalPaths {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
div() // Intentionally left empty because the platform will render icons for the dragged files
}
}
#[derive(Debug, Clone)]
pub enum FileDropEvent {
Entered {
position: Point<Pixels>,
files: ExternalPaths,
},
Pending {
position: Point<Pixels>,
},
Submit {
position: Point<Pixels>,
},
Exited,
}
#[derive(Clone, Debug)]
pub enum InputEvent {
KeyDown(KeyDownEvent),
KeyUp(KeyUpEvent),
ModifiersChanged(ModifiersChangedEvent),
MouseDown(MouseDownEvent),
MouseUp(MouseUpEvent),
MouseMove(MouseMoveEvent),
MouseExited(MouseExitEvent),
ScrollWheel(ScrollWheelEvent),
FileDrop(FileDropEvent),
}
impl InputEvent {
pub fn position(&self) -> Option<Point<Pixels>> {
match self {
InputEvent::KeyDown { .. } => None,
InputEvent::KeyUp { .. } => None,
InputEvent::ModifiersChanged { .. } => None,
InputEvent::MouseDown(event) => Some(event.position),
InputEvent::MouseUp(event) => Some(event.position),
InputEvent::MouseMove(event) => Some(event.position),
InputEvent::MouseExited(event) => Some(event.position),
InputEvent::ScrollWheel(event) => Some(event.position),
InputEvent::FileDrop(FileDropEvent::Exited) => None,
InputEvent::FileDrop(
FileDropEvent::Entered { position, .. }
| FileDropEvent::Pending { position, .. }
| FileDropEvent::Submit { position, .. },
) => Some(*position),
}
}
pub fn mouse_event(&self) -> Option<&dyn Any> {
match self {
InputEvent::KeyDown { .. } => None,
InputEvent::KeyUp { .. } => None,
InputEvent::ModifiersChanged { .. } => None,
InputEvent::MouseDown(event) => Some(event),
InputEvent::MouseUp(event) => Some(event),
InputEvent::MouseMove(event) => Some(event),
InputEvent::MouseExited(event) => Some(event),
InputEvent::ScrollWheel(event) => Some(event),
InputEvent::FileDrop(event) => Some(event),
}
}
pub fn keyboard_event(&self) -> Option<&dyn Any> {
match self {
InputEvent::KeyDown(event) => Some(event),
InputEvent::KeyUp(event) => Some(event),
InputEvent::ModifiersChanged(event) => Some(event),
InputEvent::MouseDown(_) => None,
InputEvent::MouseUp(_) => None,
InputEvent::MouseMove(_) => None,
InputEvent::MouseExited(_) => None,
InputEvent::ScrollWheel(_) => None,
InputEvent::FileDrop(_) => None,
}
}
}
#[cfg(test)]
mod test {
use crate::{
self as gpui, div, Element, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
Keystroke, ParentElement, Render, TestAppContext, VisualContext,
};
struct TestView {
saw_key_down: bool,
saw_action: bool,
focus_handle: FocusHandle,
}
actions!(test, [TestAction]);
impl Render for TestView {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl Element {
div().id("testview").child(
div()
.key_context("parent")
.on_key_down(cx.listener(|this, _, _| this.saw_key_down = true))
.on_action(
cx.listener(|this: &mut TestView, _: &TestAction, _| {
this.saw_action = true
}),
)
.child(
div()
.key_context("nested")
.track_focus(&self.focus_handle)
.into_element(),
),
)
}
}
#[gpui::test]
fn test_on_events(cx: &mut TestAppContext) {
let window = cx.update(|cx| {
cx.open_window(Default::default(), |cx| {
cx.new_view(|cx| TestView {
saw_key_down: false,
saw_action: false,
focus_handle: cx.focus_handle(),
})
})
});
cx.update(|cx| {
cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]);
});
window
.update(cx, |test_view, cx| cx.focus(&test_view.focus_handle))
.unwrap();
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false);
window
.update(cx, |test_view, _| {
assert!(test_view.saw_key_down || test_view.saw_action);
assert!(test_view.saw_key_down);
assert!(test_view.saw_action);
})
.unwrap();
}
}

View file

@ -1,15 +0,0 @@
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)
}
}
}

View file

@ -0,0 +1,287 @@
use crate::{
arena::ArenaRef, Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext,
KeyMatch, Keymap, Keystroke, KeystrokeMatcher, WindowContext,
};
use collections::HashMap;
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
rc::Rc,
sync::Arc,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct DispatchNodeId(usize);
pub(crate) struct DispatchTree {
node_stack: Vec<DispatchNodeId>,
pub(crate) context_stack: Vec<KeyContext>,
nodes: Vec<DispatchNode>,
focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Arc<Mutex<Keymap>>,
action_registry: Rc<ActionRegistry>,
}
#[derive(Default)]
pub(crate) struct DispatchNode {
pub key_listeners: Vec<KeyListener>,
pub action_listeners: Vec<DispatchActionListener>,
pub context: Option<KeyContext>,
focus_id: Option<FocusId>,
parent: Option<DispatchNodeId>,
}
type KeyListener = ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
#[derive(Clone)]
pub(crate) struct DispatchActionListener {
pub(crate) action_type: TypeId,
pub(crate) listener: ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
}
impl DispatchTree {
pub fn new(keymap: Arc<Mutex<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
Self {
node_stack: Vec::new(),
context_stack: Vec::new(),
nodes: Vec::new(),
focusable_node_ids: HashMap::default(),
keystroke_matchers: HashMap::default(),
keymap,
action_registry,
}
}
pub fn clear(&mut self) {
self.node_stack.clear();
self.nodes.clear();
self.context_stack.clear();
self.focusable_node_ids.clear();
self.keystroke_matchers.clear();
}
pub fn push_node(&mut self, context: Option<KeyContext>) {
let parent = self.node_stack.last().copied();
let node_id = DispatchNodeId(self.nodes.len());
self.nodes.push(DispatchNode {
parent,
..Default::default()
});
self.node_stack.push(node_id);
if let Some(context) = context {
self.active_node().context = Some(context.clone());
self.context_stack.push(context);
}
}
pub fn pop_node(&mut self) {
let node_id = self.node_stack.pop().unwrap();
if self.nodes[node_id.0].context.is_some() {
self.context_stack.pop();
}
}
pub fn clear_pending_keystrokes(&mut self) {
self.keystroke_matchers.clear();
}
/// Preserve keystroke matchers from previous frames to support multi-stroke
/// bindings across multiple frames.
pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
let dispatch_path = self.dispatch_path(node_id);
self.context_stack.clear();
for node_id in dispatch_path {
let node = self.node(node_id);
if let Some(context) = node.context.clone() {
self.context_stack.push(context);
}
if let Some((context_stack, matcher)) = old_tree
.keystroke_matchers
.remove_entry(self.context_stack.as_slice())
{
self.keystroke_matchers.insert(context_stack, matcher);
}
}
}
}
pub fn on_key_event(&mut self, listener: KeyListener) {
self.active_node().key_listeners.push(listener);
}
pub fn on_action(
&mut self,
action_type: TypeId,
listener: ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
) {
self.active_node()
.action_listeners
.push(DispatchActionListener {
action_type,
listener,
});
}
pub fn make_focusable(&mut self, focus_id: FocusId) {
let node_id = self.active_node_id();
self.active_node().focus_id = Some(focus_id);
self.focusable_node_ids.insert(focus_id, node_id);
}
pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
if parent == child {
return true;
}
if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) {
let mut current_node_id = self.focusable_node_ids.get(&child).copied();
while let Some(node_id) = current_node_id {
if node_id == *parent_node_id {
return true;
}
current_node_id = self.nodes[node_id.0].parent;
}
}
false
}
pub fn available_actions(&self, target: DispatchNodeId) -> Vec<Box<dyn Action>> {
let mut actions = Vec::<Box<dyn Action>>::new();
for node_id in self.dispatch_path(target) {
let node = &self.nodes[node_id.0];
for DispatchActionListener { action_type, .. } in &node.action_listeners {
if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id())
{
// Intentionally silence these errors without logging.
// If an action cannot be built by default, it's not available.
let action = self.action_registry.build_action_type(action_type).ok();
if let Some(action) = action {
actions.insert(ix, action);
}
}
}
}
actions
}
pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool {
for node_id in self.dispatch_path(target) {
let node = &self.nodes[node_id.0];
if node
.action_listeners
.iter()
.any(|listener| listener.action_type == action.as_any().type_id())
{
return true;
}
}
false
}
pub fn bindings_for_action(
&self,
action: &dyn Action,
context_stack: &Vec<KeyContext>,
) -> Vec<KeyBinding> {
self.keymap
.lock()
.bindings_for_action(action.type_id())
.filter(|candidate| {
if !candidate.action.partial_eq(action) {
return false;
}
for i in 1..context_stack.len() {
if candidate.matches_context(&context_stack[0..=i]) {
return true;
}
}
false
})
.cloned()
.collect()
}
pub fn dispatch_key(
&mut self,
keystroke: &Keystroke,
context: &[KeyContext],
) -> Vec<Box<dyn Action>> {
if !self.keystroke_matchers.contains_key(context) {
let keystroke_contexts = context.iter().cloned().collect();
self.keystroke_matchers.insert(
keystroke_contexts,
KeystrokeMatcher::new(self.keymap.clone()),
);
}
let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap();
if let KeyMatch::Some(actions) = keystroke_matcher.match_keystroke(keystroke, context) {
// Clear all pending keystrokes when an action has been found.
for keystroke_matcher in self.keystroke_matchers.values_mut() {
keystroke_matcher.clear_pending();
}
actions
} else {
vec![]
}
}
pub fn has_pending_keystrokes(&self) -> bool {
self.keystroke_matchers
.iter()
.any(|(_, matcher)| matcher.has_pending_keystrokes())
}
pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> {
let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new();
let mut current_node_id = Some(target);
while let Some(node_id) = current_node_id {
dispatch_path.push(node_id);
current_node_id = self.nodes[node_id.0].parent;
}
dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
dispatch_path
}
pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> {
let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new();
let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied();
while let Some(node_id) = current_node_id {
let node = self.node(node_id);
if let Some(focus_id) = node.focus_id {
focus_path.push(focus_id);
}
current_node_id = node.parent;
}
focus_path.reverse(); // Reverse the path so it goes from the root to the focused node.
focus_path
}
pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode {
&self.nodes[node_id.0]
}
fn active_node(&mut self) -> &mut DispatchNode {
let active_node_id = self.active_node_id();
&mut self.nodes[active_node_id.0]
}
pub fn focusable_node_id(&self, target: FocusId) -> Option<DispatchNodeId> {
self.focusable_node_ids.get(&target).copied()
}
pub fn root_node_id(&self) -> DispatchNodeId {
debug_assert!(!self.nodes.is_empty());
DispatchNodeId(0)
}
fn active_node_id(&self) -> DispatchNodeId {
*self.node_stack.last().unwrap()
}
}

View file

@ -0,0 +1,90 @@
use crate::{Action, KeyBindingContextPredicate, KeyContext, KeyMatch, Keystroke};
use anyhow::Result;
use smallvec::SmallVec;
pub struct KeyBinding {
pub(crate) action: Box<dyn Action>,
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
pub(crate) context_predicate: Option<KeyBindingContextPredicate>,
}
impl Clone for KeyBinding {
fn clone(&self) -> Self {
KeyBinding {
action: self.action.boxed_clone(),
keystrokes: self.keystrokes.clone(),
context_predicate: self.context_predicate.clone(),
}
}
}
impl KeyBinding {
pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
Self::load(keystrokes, Box::new(action), context_predicate).unwrap()
}
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
let context = if let Some(context) = context {
Some(KeyBindingContextPredicate::parse(context)?)
} else {
None
};
let keystrokes = keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<_>>()?;
Ok(Self {
keystrokes,
action,
context_predicate: context,
})
}
pub fn matches_context(&self, contexts: &[KeyContext]) -> bool {
self.context_predicate
.as_ref()
.map(|predicate| predicate.eval(contexts))
.unwrap_or(true)
}
pub fn match_keystrokes(
&self,
pending_keystrokes: &[Keystroke],
contexts: &[KeyContext],
) -> KeyMatch {
if self.keystrokes.as_ref().starts_with(pending_keystrokes)
&& self.matches_context(contexts)
{
// If the binding is completed, push it onto the matches list
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
KeyMatch::Some(vec![self.action.boxed_clone()])
} else {
KeyMatch::Pending
}
} else {
KeyMatch::None
}
}
pub fn keystrokes_for_action(
&self,
action: &dyn Action,
contexts: &[KeyContext],
) -> Option<SmallVec<[Keystroke; 2]>> {
if self.action.partial_eq(action) && self.matches_context(contexts) {
Some(self.keystrokes.clone())
} else {
None
}
}
pub fn keystrokes(&self) -> &[Keystroke] {
self.keystrokes.as_slice()
}
pub fn action(&self) -> &dyn Action {
self.action.as_ref()
}
}

View file

@ -0,0 +1,452 @@
use crate::SharedString;
use anyhow::{anyhow, Result};
use smallvec::SmallVec;
use std::fmt;
#[derive(Clone, Default, Eq, PartialEq, Hash)]
pub struct KeyContext(SmallVec<[ContextEntry; 1]>);
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
struct ContextEntry {
key: SharedString,
value: Option<SharedString>,
}
impl<'a> TryFrom<&'a str> for KeyContext {
type Error = anyhow::Error;
fn try_from(value: &'a str) -> Result<Self> {
Self::parse(value)
}
}
impl KeyContext {
pub fn parse(source: &str) -> Result<Self> {
let mut context = Self::default();
let source = skip_whitespace(source);
Self::parse_expr(source, &mut context)?;
Ok(context)
}
fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
if source.is_empty() {
return Ok(());
}
let key = source
.chars()
.take_while(|c| is_identifier_char(*c))
.collect::<String>();
source = skip_whitespace(&source[key.len()..]);
if let Some(suffix) = source.strip_prefix('=') {
source = skip_whitespace(suffix);
let value = source
.chars()
.take_while(|c| is_identifier_char(*c))
.collect::<String>();
source = skip_whitespace(&source[value.len()..]);
context.set(key, value);
} else {
context.add(key);
}
Self::parse_expr(source, context)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn clear(&mut self) {
self.0.clear();
}
pub fn extend(&mut self, other: &Self) {
for entry in &other.0 {
if !self.contains(&entry.key) {
self.0.push(entry.clone());
}
}
}
pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
let key = identifier.into();
if !self.contains(&key) {
self.0.push(ContextEntry { key, value: None })
}
}
pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
let key = key.into();
if !self.contains(&key) {
self.0.push(ContextEntry {
key,
value: Some(value.into()),
})
}
}
pub fn contains(&self, key: &str) -> bool {
self.0.iter().any(|entry| entry.key.as_ref() == key)
}
pub fn get(&self, key: &str) -> Option<&SharedString> {
self.0
.iter()
.find(|entry| entry.key.as_ref() == key)?
.value
.as_ref()
}
}
impl fmt::Debug for KeyContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut entries = self.0.iter().peekable();
while let Some(entry) = entries.next() {
if let Some(ref value) = entry.value {
write!(f, "{}={}", entry.key, value)?;
} else {
write!(f, "{}", entry.key)?;
}
if entries.peek().is_some() {
write!(f, " ")?;
}
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum KeyBindingContextPredicate {
Identifier(SharedString),
Equal(SharedString, SharedString),
NotEqual(SharedString, SharedString),
Child(
Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>,
),
Not(Box<KeyBindingContextPredicate>),
And(
Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>,
),
Or(
Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>,
),
}
impl KeyBindingContextPredicate {
pub fn parse(source: &str) -> Result<Self> {
let source = skip_whitespace(source);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if let Some(next) = rest.chars().next() {
Err(anyhow!("unexpected character {next:?}"))
} else {
Ok(predicate)
}
}
pub fn eval(&self, contexts: &[KeyContext]) -> bool {
let Some(context) = contexts.last() else {
return false;
};
match self {
Self::Identifier(name) => context.contains(name),
Self::Equal(left, right) => context
.get(left)
.map(|value| value == right)
.unwrap_or(false),
Self::NotEqual(left, right) => context
.get(left)
.map(|value| value != right)
.unwrap_or(true),
Self::Not(pred) => !pred.eval(contexts),
Self::Child(parent, child) => {
parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
}
Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
}
}
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
type Op = fn(
KeyBindingContextPredicate,
KeyBindingContextPredicate,
) -> Result<KeyBindingContextPredicate>;
let (mut predicate, rest) = Self::parse_primary(source)?;
source = rest;
'parse: loop {
for (operator, precedence, constructor) in [
(">", PRECEDENCE_CHILD, Self::new_child as Op),
("&&", PRECEDENCE_AND, Self::new_and as Op),
("||", PRECEDENCE_OR, Self::new_or as Op),
("==", PRECEDENCE_EQ, Self::new_eq as Op),
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
] {
if source.starts_with(operator) && precedence >= min_precedence {
source = skip_whitespace(&source[operator.len()..]);
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
predicate = constructor(predicate, right)?;
source = rest;
continue 'parse;
}
}
break;
}
Ok((predicate, source))
}
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
let next = source
.chars()
.next()
.ok_or_else(|| anyhow!("unexpected eof"))?;
match next {
'(' => {
source = skip_whitespace(&source[1..]);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if rest.starts_with(')') {
source = skip_whitespace(&rest[1..]);
Ok((predicate, source))
} else {
Err(anyhow!("expected a ')'"))
}
}
'!' => {
let source = skip_whitespace(&source[1..]);
let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?;
Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source))
}
_ if is_identifier_char(next) => {
let len = source
.find(|c: char| !is_identifier_char(c))
.unwrap_or(source.len());
let (identifier, rest) = source.split_at(len);
source = skip_whitespace(rest);
Ok((
KeyBindingContextPredicate::Identifier(identifier.to_string().into()),
source,
))
}
_ => Err(anyhow!("unexpected character {next:?}")),
}
}
fn new_or(self, other: Self) -> Result<Self> {
Ok(Self::Or(Box::new(self), Box::new(other)))
}
fn new_and(self, other: Self) -> Result<Self> {
Ok(Self::And(Box::new(self), Box::new(other)))
}
fn new_child(self, other: Self) -> Result<Self> {
Ok(Self::Child(Box::new(self), Box::new(other)))
}
fn new_eq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::Equal(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
fn new_neq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::NotEqual(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
}
const PRECEDENCE_CHILD: u32 = 1;
const PRECEDENCE_OR: u32 = 2;
const PRECEDENCE_AND: u32 = 3;
const PRECEDENCE_EQ: u32 = 4;
const PRECEDENCE_NOT: u32 = 5;
fn is_identifier_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-'
}
fn skip_whitespace(source: &str) -> &str {
let len = source
.find(|c: char| !c.is_whitespace())
.unwrap_or(source.len());
&source[len..]
}
#[cfg(test)]
mod tests {
use super::*;
use crate as gpui;
use KeyBindingContextPredicate::*;
#[test]
fn test_actions_definition() {
{
actions!(test, [A, B, C, D, E, F, G]);
}
{
actions!(
test,
[
A,
B,
C,
D,
E,
F,
G, // Don't wrap, test the trailing comma
]
);
}
}
#[test]
fn test_parse_context() {
let mut expected = KeyContext::default();
expected.add("baz");
expected.set("foo", "bar");
assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected);
assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected);
assert_eq!(
KeyContext::parse(" baz foo = bar baz").unwrap(),
expected
);
assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected);
}
#[test]
fn test_parse_identifiers() {
// Identifiers
assert_eq!(
KeyBindingContextPredicate::parse("abc12").unwrap(),
Identifier("abc12".into())
);
assert_eq!(
KeyBindingContextPredicate::parse("_1a").unwrap(),
Identifier("_1a".into())
);
}
#[test]
fn test_parse_negations() {
assert_eq!(
KeyBindingContextPredicate::parse("!abc").unwrap(),
Not(Box::new(Identifier("abc".into())))
);
assert_eq!(
KeyBindingContextPredicate::parse(" ! ! abc").unwrap(),
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
);
}
#[test]
fn test_parse_equality_operators() {
assert_eq!(
KeyBindingContextPredicate::parse("a == b").unwrap(),
Equal("a".into(), "b".into())
);
assert_eq!(
KeyBindingContextPredicate::parse("c!=d").unwrap(),
NotEqual("c".into(), "d".into())
);
assert_eq!(
KeyBindingContextPredicate::parse("c == !d")
.unwrap_err()
.to_string(),
"operands must be identifiers"
);
}
#[test]
fn test_parse_boolean_operators() {
assert_eq!(
KeyBindingContextPredicate::parse("a || b").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)
);
assert_eq!(
KeyBindingContextPredicate::parse("a || !b && c").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(And(
Box::new(Not(Box::new(Identifier("b".into())))),
Box::new(Identifier("c".into()))
))
)
);
assert_eq!(
KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(),
Or(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(And(
Box::new(Identifier("c".into())),
Box::new(Identifier("d".into()))
))
)
);
assert_eq!(
KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(),
Or(
Box::new(And(
Box::new(Equal("a".into(), "b".into())),
Box::new(Identifier("c".into()))
)),
Box::new(And(
Box::new(Equal("d".into(), "e".into())),
Box::new(Identifier("f".into()))
))
)
);
assert_eq!(
KeyBindingContextPredicate::parse("a && b && c && d").unwrap(),
And(
Box::new(And(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(Identifier("c".into())),
)),
Box::new(Identifier("d".into()))
),
);
}
#[test]
fn test_parse_parenthesized_expressions() {
assert_eq!(
KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
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!(
KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into())),
)
);
}
}

View file

@ -0,0 +1,400 @@
use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction};
use collections::HashSet;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
collections::HashMap,
};
#[derive(Copy, Clone, Eq, PartialEq, Default)]
pub struct KeymapVersion(usize);
#[derive(Default)]
pub struct Keymap {
bindings: Vec<KeyBinding>,
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
disabled_keystrokes:
HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeyBindingContextPredicate>>>,
version: KeymapVersion,
}
impl Keymap {
pub fn new(bindings: Vec<KeyBinding>) -> Self {
let mut this = Self::default();
this.add_bindings(bindings);
this
}
pub fn version(&self) -> KeymapVersion {
self.version
}
pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator<Item = &'_ KeyBinding> {
self.binding_indices_by_action_id
.get(&action_id)
.map(SmallVec::as_slice)
.unwrap_or(&[])
.iter()
.map(|ix| &self.bindings[*ix])
.filter(|binding| !self.binding_disabled(binding))
}
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
let no_action_id = &(NoAction {}).type_id();
let mut new_bindings = Vec::new();
let mut has_new_disabled_keystrokes = false;
for binding in bindings {
if binding.action.type_id() == *no_action_id {
has_new_disabled_keystrokes |= self
.disabled_keystrokes
.entry(binding.keystrokes)
.or_default()
.insert(binding.context_predicate);
} else {
new_bindings.push(binding);
}
}
if has_new_disabled_keystrokes {
self.binding_indices_by_action_id.retain(|_, indices| {
indices.retain(|ix| {
let binding = &self.bindings[*ix];
match self.disabled_keystrokes.get(&binding.keystrokes) {
Some(disabled_predicates) => {
!disabled_predicates.contains(&binding.context_predicate)
}
None => true,
}
});
!indices.is_empty()
});
}
for new_binding in new_bindings {
if !self.binding_disabled(&new_binding) {
self.binding_indices_by_action_id
.entry(new_binding.action().as_any().type_id())
.or_default()
.push(self.bindings.len());
self.bindings.push(new_binding);
}
}
self.version.0 += 1;
}
pub fn clear(&mut self) {
self.bindings.clear();
self.binding_indices_by_action_id.clear();
self.disabled_keystrokes.clear();
self.version.0 += 1;
}
pub fn bindings(&self) -> Vec<&KeyBinding> {
self.bindings
.iter()
.filter(|binding| !self.binding_disabled(binding))
.collect()
}
fn binding_disabled(&self, binding: &KeyBinding) -> bool {
match self.disabled_keystrokes.get(&binding.keystrokes) {
Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate),
None => false,
}
}
}
// #[cfg(test)]
// mod tests {
// use crate::actions;
// use super::*;
// actions!(
// keymap_test,
// [Present1, Present2, Present3, Duplicate, Missing]
// );
// #[test]
// fn regular_keymap() {
// let present_1 = Binding::new("ctrl-q", Present1 {}, None);
// let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
// let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
// let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
// let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
// let missing = Binding::new("ctrl-r", Missing {}, None);
// let all_bindings = [
// &present_1,
// &present_2,
// &present_3,
// &keystroke_duplicate_to_1,
// &full_duplicate_to_2,
// &missing,
// ];
// let mut keymap = Keymap::default();
// assert_absent(&keymap, &all_bindings);
// assert!(keymap.bindings().is_empty());
// keymap.add_bindings([present_1.clone(), present_2.clone(), present_3.clone()]);
// assert_absent(&keymap, &[&keystroke_duplicate_to_1, &missing]);
// assert_present(
// &keymap,
// &[(&present_1, "q"), (&present_2, "w"), (&present_3, "e")],
// );
// keymap.add_bindings([
// keystroke_duplicate_to_1.clone(),
// full_duplicate_to_2.clone(),
// ]);
// assert_absent(&keymap, &[&missing]);
// assert!(
// !keymap.binding_disabled(&keystroke_duplicate_to_1),
// "Duplicate binding 1 was added and should not be disabled"
// );
// assert!(
// !keymap.binding_disabled(&full_duplicate_to_2),
// "Duplicate binding 2 was added and should not be disabled"
// );
// assert_eq!(
// keymap
// .bindings_for_action(keystroke_duplicate_to_1.action().id())
// .map(|binding| &binding.keystrokes)
// .flatten()
// .collect::<Vec<_>>(),
// vec![&Keystroke {
// ctrl: true,
// alt: false,
// shift: false,
// cmd: false,
// function: false,
// key: "q".to_string(),
// ime_key: None,
// }],
// "{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap"
// );
// assert_eq!(
// keymap
// .bindings_for_action(full_duplicate_to_2.action().id())
// .map(|binding| &binding.keystrokes)
// .flatten()
// .collect::<Vec<_>>(),
// vec![
// &Keystroke {
// ctrl: true,
// alt: false,
// shift: false,
// cmd: false,
// function: false,
// key: "w".to_string(),
// ime_key: None,
// },
// &Keystroke {
// ctrl: true,
// alt: false,
// shift: false,
// cmd: false,
// function: false,
// key: "w".to_string(),
// ime_key: None,
// }
// ],
// "{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap"
// );
// let updated_bindings = keymap.bindings();
// let expected_updated_bindings = vec![
// &present_1,
// &present_2,
// &present_3,
// &keystroke_duplicate_to_1,
// &full_duplicate_to_2,
// ];
// assert_eq!(
// updated_bindings.len(),
// expected_updated_bindings.len(),
// "Unexpected updated keymap bindings {updated_bindings:?}"
// );
// for (i, expected) in expected_updated_bindings.iter().enumerate() {
// let keymap_binding = &updated_bindings[i];
// assert_eq!(
// keymap_binding.context_predicate, expected.context_predicate,
// "Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
// );
// assert_eq!(
// keymap_binding.keystrokes, expected.keystrokes,
// "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
// );
// }
// keymap.clear();
// assert_absent(&keymap, &all_bindings);
// assert!(keymap.bindings().is_empty());
// }
// #[test]
// fn keymap_with_ignored() {
// let present_1 = Binding::new("ctrl-q", Present1 {}, None);
// let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
// let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
// let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
// let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
// let ignored_1 = Binding::new("ctrl-q", NoAction {}, None);
// let ignored_2 = Binding::new("ctrl-w", NoAction {}, Some("pane"));
// let ignored_3_with_other_context =
// Binding::new("ctrl-e", NoAction {}, Some("other_context"));
// let mut keymap = Keymap::default();
// keymap.add_bindings([
// ignored_1.clone(),
// ignored_2.clone(),
// ignored_3_with_other_context.clone(),
// ]);
// assert_absent(&keymap, &[&present_3]);
// assert_disabled(
// &keymap,
// &[
// &present_1,
// &present_2,
// &ignored_1,
// &ignored_2,
// &ignored_3_with_other_context,
// ],
// );
// assert!(keymap.bindings().is_empty());
// keymap.clear();
// keymap.add_bindings([
// present_1.clone(),
// present_2.clone(),
// present_3.clone(),
// ignored_1.clone(),
// ignored_2.clone(),
// ignored_3_with_other_context.clone(),
// ]);
// assert_present(&keymap, &[(&present_3, "e")]);
// assert_disabled(
// &keymap,
// &[
// &present_1,
// &present_2,
// &ignored_1,
// &ignored_2,
// &ignored_3_with_other_context,
// ],
// );
// keymap.clear();
// keymap.add_bindings([
// present_1.clone(),
// present_2.clone(),
// present_3.clone(),
// ignored_1.clone(),
// ]);
// assert_present(&keymap, &[(&present_2, "w"), (&present_3, "e")]);
// assert_disabled(&keymap, &[&present_1, &ignored_1]);
// assert_absent(&keymap, &[&ignored_2, &ignored_3_with_other_context]);
// keymap.clear();
// keymap.add_bindings([
// present_1.clone(),
// present_2.clone(),
// present_3.clone(),
// keystroke_duplicate_to_1.clone(),
// full_duplicate_to_2.clone(),
// ignored_1.clone(),
// ignored_2.clone(),
// ignored_3_with_other_context.clone(),
// ]);
// assert_present(&keymap, &[(&present_3, "e")]);
// assert_disabled(
// &keymap,
// &[
// &present_1,
// &present_2,
// &keystroke_duplicate_to_1,
// &full_duplicate_to_2,
// &ignored_1,
// &ignored_2,
// &ignored_3_with_other_context,
// ],
// );
// keymap.clear();
// }
// #[track_caller]
// fn assert_present(keymap: &Keymap, expected_bindings: &[(&Binding, &str)]) {
// let keymap_bindings = keymap.bindings();
// assert_eq!(
// expected_bindings.len(),
// keymap_bindings.len(),
// "Unexpected keymap bindings {keymap_bindings:?}"
// );
// for (i, (expected, expected_key)) in expected_bindings.iter().enumerate() {
// assert!(
// !keymap.binding_disabled(expected),
// "{expected:?} should not be disabled as it was added into keymap for element {i}"
// );
// assert_eq!(
// keymap
// .bindings_for_action(expected.action().id())
// .map(|binding| &binding.keystrokes)
// .flatten()
// .collect::<Vec<_>>(),
// vec![&Keystroke {
// ctrl: true,
// alt: false,
// shift: false,
// cmd: false,
// function: false,
// key: expected_key.to_string(),
// ime_key: None,
// }],
// "{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}"
// );
// let keymap_binding = &keymap_bindings[i];
// assert_eq!(
// keymap_binding.context_predicate, expected.context_predicate,
// "Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
// );
// assert_eq!(
// keymap_binding.keystrokes, expected.keystrokes,
// "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
// );
// }
// }
// #[track_caller]
// fn assert_absent(keymap: &Keymap, bindings: &[&Binding]) {
// for binding in bindings.iter() {
// assert!(
// !keymap.binding_disabled(binding),
// "{binding:?} should not be disabled in the keymap where was not added"
// );
// assert_eq!(
// keymap.bindings_for_action(binding.action().id()).count(),
// 0,
// "{binding:?} should have no actions in the keymap where was not added"
// );
// }
// }
// #[track_caller]
// fn assert_disabled(keymap: &Keymap, bindings: &[&Binding]) {
// for binding in bindings.iter() {
// assert!(
// keymap.binding_disabled(binding),
// "{binding:?} should be disabled in the keymap"
// );
// assert_eq!(
// keymap.bindings_for_action(binding.action().id()).count(),
// 0,
// "{binding:?} should have no actions in the keymap where it was disabled"
// );
// }
// }
// }

View file

@ -0,0 +1,474 @@
use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::sync::Arc;
pub struct KeystrokeMatcher {
pending_keystrokes: Vec<Keystroke>,
keymap: Arc<Mutex<Keymap>>,
keymap_version: KeymapVersion,
}
impl KeystrokeMatcher {
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
let keymap_version = keymap.lock().version();
Self {
pending_keystrokes: Vec::new(),
keymap_version,
keymap,
}
}
pub fn clear_pending(&mut self) {
self.pending_keystrokes.clear();
}
pub fn has_pending_keystrokes(&self) -> bool {
!self.pending_keystrokes.is_empty()
}
/// Pushes a keystroke onto the matcher.
/// The result of the new keystroke is returned:
/// KeyMatch::None =>
/// No match is valid for this key given any pending keystrokes.
/// KeyMatch::Pending =>
/// There exist bindings which are still waiting for more keys.
/// KeyMatch::Complete(matches) =>
/// One or more bindings have received the necessary key presses.
/// Bindings added later will take precedence over earlier bindings.
pub fn match_keystroke(
&mut self,
keystroke: &Keystroke,
context_stack: &[KeyContext],
) -> KeyMatch {
let keymap = self.keymap.lock();
// Clear pending keystrokes if the keymap has changed since the last matched keystroke.
if keymap.version() != self.keymap_version {
self.keymap_version = keymap.version();
self.pending_keystrokes.clear();
}
let mut pending_key = None;
let mut found_actions = Vec::new();
for binding in keymap.bindings().iter().rev() {
for candidate in keystroke.match_candidates() {
self.pending_keystrokes.push(candidate.clone());
match binding.match_keystrokes(&self.pending_keystrokes, context_stack) {
KeyMatch::Some(mut actions) => {
found_actions.append(&mut actions);
}
KeyMatch::Pending => {
pending_key.get_or_insert(candidate);
}
KeyMatch::None => {}
}
self.pending_keystrokes.pop();
}
}
if !found_actions.is_empty() {
self.pending_keystrokes.clear();
return KeyMatch::Some(found_actions);
}
if let Some(pending_key) = pending_key {
self.pending_keystrokes.push(pending_key);
}
if self.pending_keystrokes.is_empty() {
KeyMatch::None
} else {
KeyMatch::Pending
}
}
pub fn keystrokes_for_action(
&self,
action: &dyn Action,
contexts: &[KeyContext],
) -> Option<SmallVec<[Keystroke; 2]>> {
self.keymap
.lock()
.bindings()
.iter()
.rev()
.find_map(|binding| binding.keystrokes_for_action(action, contexts))
}
}
#[derive(Debug)]
pub enum KeyMatch {
None,
Pending,
Some(Vec<Box<dyn Action>>),
}
impl KeyMatch {
pub fn is_some(&self) -> bool {
matches!(self, KeyMatch::Some(_))
}
}
// #[cfg(test)]
// mod tests {
// use anyhow::Result;
// use serde::Deserialize;
// use crate::{actions, impl_actions, keymap_matcher::ActionContext};
// use super::*;
// #[test]
// fn test_keymap_and_view_ordering() -> Result<()> {
// actions!(test, [EditorAction, ProjectPanelAction]);
// let mut editor = ActionContext::default();
// editor.add_identifier("Editor");
// let mut project_panel = ActionContext::default();
// project_panel.add_identifier("ProjectPanel");
// // Editor 'deeper' in than project panel
// let dispatch_path = vec![(2, editor), (1, project_panel)];
// // But editor actions 'higher' up in keymap
// let keymap = Keymap::new(vec![
// Binding::new("left", EditorAction, Some("Editor")),
// Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
// ]);
// let mut matcher = KeymapMatcher::new(keymap);
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
// KeyMatch::Matches(vec![
// (2, Box::new(EditorAction)),
// (1, Box::new(ProjectPanelAction)),
// ]),
// );
// Ok(())
// }
// #[test]
// fn test_push_keystroke() -> Result<()> {
// actions!(test, [B, AB, C, D, DA, E, EF]);
// let mut context1 = ActionContext::default();
// context1.add_identifier("1");
// let mut context2 = ActionContext::default();
// context2.add_identifier("2");
// let dispatch_path = vec![(2, context2), (1, context1)];
// let keymap = Keymap::new(vec![
// Binding::new("a b", AB, Some("1")),
// Binding::new("b", B, Some("2")),
// Binding::new("c", C, Some("2")),
// Binding::new("d", D, Some("1")),
// Binding::new("d", D, Some("2")),
// Binding::new("d a", DA, Some("2")),
// ]);
// let mut matcher = KeymapMatcher::new(keymap);
// // Binding with pending prefix always takes precedence
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
// KeyMatch::Pending,
// );
// // B alone doesn't match because a was pending, so AB is returned instead
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
// KeyMatch::Matches(vec![(1, Box::new(AB))]),
// );
// assert!(!matcher.has_pending_keystrokes());
// // Without an a prefix, B is dispatched like expected
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
// KeyMatch::Matches(vec![(2, Box::new(B))]),
// );
// assert!(!matcher.has_pending_keystrokes());
// // If a is prefixed, C will not be dispatched because there
// // was a pending binding for it
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
// KeyMatch::Pending,
// );
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
// KeyMatch::None,
// );
// assert!(!matcher.has_pending_keystrokes());
// // If a single keystroke matches multiple bindings in the tree
// // all of them are returned so that we can fallback if the action
// // handler decides to propagate the action
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
// KeyMatch::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
// );
// // If none of the d action handlers consume the binding, a pending
// // binding may then be used
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
// KeyMatch::Matches(vec![(2, Box::new(DA))]),
// );
// assert!(!matcher.has_pending_keystrokes());
// Ok(())
// }
// #[test]
// fn test_keystroke_parsing() -> Result<()> {
// assert_eq!(
// Keystroke::parse("ctrl-p")?,
// Keystroke {
// key: "p".into(),
// ctrl: true,
// alt: false,
// shift: false,
// cmd: false,
// function: false,
// ime_key: None,
// }
// );
// assert_eq!(
// Keystroke::parse("alt-shift-down")?,
// Keystroke {
// key: "down".into(),
// ctrl: false,
// alt: true,
// shift: true,
// cmd: false,
// function: false,
// ime_key: None,
// }
// );
// assert_eq!(
// Keystroke::parse("shift-cmd--")?,
// Keystroke {
// key: "-".into(),
// ctrl: false,
// alt: false,
// shift: true,
// cmd: true,
// function: false,
// ime_key: None,
// }
// );
// Ok(())
// }
// #[test]
// fn test_context_predicate_parsing() -> Result<()> {
// use KeymapContextPredicate::*;
// assert_eq!(
// KeymapContextPredicate::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!(
// KeymapContextPredicate::parse("!a")?,
// Not(Box::new(Identifier("a".into())),)
// );
// Ok(())
// }
// #[test]
// fn test_context_predicate_eval() {
// let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
// let mut context = ActionContext::default();
// context.add_identifier("a");
// assert!(!predicate.eval(&[context]));
// let mut context = ActionContext::default();
// context.add_identifier("a");
// context.add_identifier("b");
// assert!(predicate.eval(&[context]));
// let mut context = ActionContext::default();
// context.add_identifier("a");
// context.add_key("c", "x");
// assert!(!predicate.eval(&[context]));
// let mut context = ActionContext::default();
// context.add_identifier("a");
// context.add_key("c", "d");
// assert!(predicate.eval(&[context]));
// let predicate = KeymapContextPredicate::parse("!a").unwrap();
// assert!(predicate.eval(&[ActionContext::default()]));
// }
// #[test]
// fn test_context_child_predicate_eval() {
// let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();
// let contexts = [
// context_set(&["e", "f"]),
// context_set(&["c", "d"]), // match this context
// context_set(&["a", "b"]),
// ];
// assert!(!predicate.eval(&contexts[0..]));
// assert!(predicate.eval(&contexts[1..]));
// assert!(!predicate.eval(&contexts[2..]));
// let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();
// let contexts = [
// context_set(&["f"]),
// context_set(&["e"]), // only match this context
// context_set(&["c"]),
// context_set(&["a", "b"]),
// context_set(&["e"]),
// context_set(&["c", "d"]),
// context_set(&["a", "b"]),
// ];
// assert!(!predicate.eval(&contexts[0..]));
// assert!(predicate.eval(&contexts[1..]));
// assert!(!predicate.eval(&contexts[2..]));
// assert!(!predicate.eval(&contexts[3..]));
// assert!(!predicate.eval(&contexts[4..]));
// assert!(!predicate.eval(&contexts[5..]));
// assert!(!predicate.eval(&contexts[6..]));
// fn context_set(names: &[&str]) -> ActionContext {
// let mut keymap = ActionContext::new();
// names
// .iter()
// .for_each(|name| keymap.add_identifier(name.to_string()));
// keymap
// }
// }
// #[test]
// fn test_matcher() -> Result<()> {
// #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
// pub struct A(pub String);
// impl_actions!(test, [A]);
// actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
// #[derive(Clone, Debug, Eq, PartialEq)]
// struct ActionArg {
// a: &'static str,
// }
// let keymap = Keymap::new(vec![
// Binding::new("a", A("x".to_string()), Some("a")),
// Binding::new("b", B, Some("a")),
// Binding::new("a b", Ab, Some("a || b")),
// Binding::new("$", Dollar, Some("a")),
// Binding::new("\"", Quote, Some("a")),
// Binding::new("alt-s", Ess, Some("a")),
// Binding::new("ctrl-`", Backtick, Some("a")),
// ]);
// let mut context_a = ActionContext::default();
// context_a.add_identifier("a");
// let mut context_b = ActionContext::default();
// context_b.add_identifier("b");
// let mut matcher = KeymapMatcher::new(keymap);
// // Basic match
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
// );
// matcher.clear_pending();
// // Multi-keystroke match
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
// KeyMatch::Pending
// );
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
// KeyMatch::Matches(vec![(1, Box::new(Ab))])
// );
// matcher.clear_pending();
// // Failed matches don't interfere with matching subsequent keys
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
// KeyMatch::None
// );
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
// );
// matcher.clear_pending();
// // Pending keystrokes are cleared when the context changes
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
// KeyMatch::Pending
// );
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
// KeyMatch::None
// );
// matcher.clear_pending();
// let mut context_c = ActionContext::default();
// context_c.add_identifier("c");
// // Pending keystrokes are maintained per-view
// assert_eq!(
// matcher.match_keystroke(
// Keystroke::parse("a")?,
// vec![(1, context_b.clone()), (2, context_c.clone())]
// ),
// KeyMatch::Pending
// );
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
// KeyMatch::Matches(vec![(1, Box::new(Ab))])
// );
// // handle Czech $ (option + 4 key)
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]),
// KeyMatch::Matches(vec![(1, Box::new(Dollar))])
// );
// // handle Brazillian quote (quote key then space key)
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]),
// KeyMatch::Matches(vec![(1, Box::new(Quote))])
// );
// // handle ctrl+` on a brazillian keyboard
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]),
// KeyMatch::Matches(vec![(1, Box::new(Backtick))])
// );
// // handle alt-s on a US keyboard
// assert_eq!(
// matcher.match_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]),
// KeyMatch::Matches(vec![(1, Box::new(Ess))])
// );
// Ok(())
// }
// }

View file

@ -0,0 +1,9 @@
mod binding;
mod context;
mod keymap;
mod matcher;
pub use binding::*;
pub use context::*;
pub use keymap::*;
pub use matcher::*;

View file

@ -1,587 +0,0 @@
mod binding;
mod keymap;
mod keymap_context;
mod keystroke;
use std::{any::TypeId, fmt::Debug};
use collections::HashMap;
use smallvec::SmallVec;
use crate::{Action, NoAction};
pub use binding::{Binding, BindingMatchResult};
pub use keymap::Keymap;
pub use keymap_context::{KeymapContext, KeymapContextPredicate};
pub use keystroke::Keystroke;
pub struct KeymapMatcher {
pub contexts: Vec<KeymapContext>,
pending_views: HashMap<usize, KeymapContext>,
pending_keystrokes: Vec<Keystroke>,
keymap: Keymap,
}
impl KeymapMatcher {
pub fn new(keymap: Keymap) -> Self {
Self {
contexts: Vec::new(),
pending_views: Default::default(),
pending_keystrokes: Vec::new(),
keymap,
}
}
pub fn set_keymap(&mut self, keymap: Keymap) {
self.clear_pending();
self.keymap = keymap;
}
pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
self.clear_pending();
self.keymap.add_bindings(bindings);
}
pub fn clear_bindings(&mut self) {
self.clear_pending();
self.keymap.clear();
}
pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator<Item = &Binding> {
self.keymap.bindings_for_action(action_id)
}
pub fn clear_pending(&mut self) {
self.pending_keystrokes.clear();
self.pending_views.clear();
}
pub fn has_pending_keystrokes(&self) -> bool {
!self.pending_keystrokes.is_empty()
}
/// Pushes a keystroke onto the matcher.
/// The result of the new keystroke is returned:
/// MatchResult::None =>
/// No match is valid for this key given any pending keystrokes.
/// MatchResult::Pending =>
/// There exist bindings which are still waiting for more keys.
/// MatchResult::Complete(matches) =>
/// 1 or more bindings have received the necessary key presses.
/// The order of the matched actions is by position of the matching first,
// and order in the keymap second.
pub fn push_keystroke(
&mut self,
keystroke: Keystroke,
mut dispatch_path: Vec<(usize, KeymapContext)>,
) -> MatchResult {
// Collect matched bindings into an ordered list using the position in the matching binding first,
// and then the order the binding matched in the view tree second.
// The key is the reverse position of the binding in the bindings list so that later bindings
// match before earlier ones in the user's config
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Default::default();
let no_action_id = (NoAction {}).id();
let first_keystroke = self.pending_keystrokes.is_empty();
let mut pending_key = None;
let mut previous_keystrokes = self.pending_keystrokes.clone();
self.contexts.clear();
self.contexts
.extend(dispatch_path.iter_mut().map(|e| std::mem::take(&mut e.1)));
// Find the bindings which map the pending keystrokes and current context
for (i, (view_id, _)) in dispatch_path.iter().enumerate() {
// Don't require pending view entry if there are no pending keystrokes
if !first_keystroke && !self.pending_views.contains_key(view_id) {
continue;
}
// If there is a previous view context, invalidate that view if it
// has changed
if let Some(previous_view_context) = self.pending_views.remove(view_id) {
if previous_view_context != self.contexts[i] {
continue;
}
}
for binding in self.keymap.bindings().iter().rev() {
for possibility in keystroke.match_possibilities() {
previous_keystrokes.push(possibility.clone());
match binding.match_keys_and_context(&previous_keystrokes, &self.contexts[i..])
{
BindingMatchResult::Complete(action) => {
if action.id() != no_action_id {
matched_bindings.push((*view_id, action));
}
}
BindingMatchResult::Partial => {
if pending_key == None || pending_key == Some(possibility.clone()) {
self.pending_views
.insert(*view_id, self.contexts[i].clone());
pending_key = Some(possibility)
}
}
_ => {}
}
previous_keystrokes.pop();
}
}
}
if pending_key.is_some() {
self.pending_keystrokes.push(pending_key.unwrap());
} else {
self.clear_pending();
}
if !matched_bindings.is_empty() {
// Collect the sorted matched bindings into the final vec for ease of use
// Matched bindings are in order by precedence
MatchResult::Matches(matched_bindings)
} else if !self.pending_keystrokes.is_empty() {
MatchResult::Pending
} else {
MatchResult::None
}
}
pub fn keystrokes_for_action(
&self,
action: &dyn Action,
contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> {
self.keymap
.bindings()
.iter()
.rev()
.find_map(|binding| binding.keystrokes_for_action(action, contexts))
}
}
impl Default for KeymapMatcher {
fn default() -> Self {
Self::new(Keymap::default())
}
}
pub enum MatchResult {
None,
Pending,
Matches(Vec<(usize, Box<dyn Action>)>),
}
impl Debug for MatchResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatchResult::None => f.debug_struct("MatchResult::None").finish(),
MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
MatchResult::Matches(matches) => f
.debug_list()
.entries(
matches
.iter()
.map(|(view_id, action)| format!("{view_id}, {}", action.name())),
)
.finish(),
}
}
}
impl PartialEq for MatchResult {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(MatchResult::None, MatchResult::None) => true,
(MatchResult::Pending, MatchResult::Pending) => true,
(MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
matches.len() == other_matches.len()
&& matches.iter().zip(other_matches.iter()).all(
|((view_id, action), (other_view_id, other_action))| {
view_id == other_view_id && action.eq(other_action.as_ref())
},
)
}
_ => false,
}
}
}
impl Eq for MatchResult {}
impl Clone for MatchResult {
fn clone(&self) -> Self {
match self {
MatchResult::None => MatchResult::None,
MatchResult::Pending => MatchResult::Pending,
MatchResult::Matches(matches) => MatchResult::Matches(
matches
.iter()
.map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
.collect(),
),
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use serde::Deserialize;
use crate::{actions, impl_actions, keymap_matcher::KeymapContext};
use super::*;
#[test]
fn test_keymap_and_view_ordering() -> Result<()> {
actions!(test, [EditorAction, ProjectPanelAction]);
let mut editor = KeymapContext::default();
editor.add_identifier("Editor");
let mut project_panel = KeymapContext::default();
project_panel.add_identifier("ProjectPanel");
// Editor 'deeper' in than project panel
let dispatch_path = vec![(2, editor), (1, project_panel)];
// But editor actions 'higher' up in keymap
let keymap = Keymap::new(vec![
Binding::new("left", EditorAction, Some("Editor")),
Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
]);
let mut matcher = KeymapMatcher::new(keymap);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
MatchResult::Matches(vec![
(2, Box::new(EditorAction)),
(1, Box::new(ProjectPanelAction)),
]),
);
Ok(())
}
#[test]
fn test_push_keystroke() -> Result<()> {
actions!(test, [B, AB, C, D, DA, E, EF]);
let mut context1 = KeymapContext::default();
context1.add_identifier("1");
let mut context2 = KeymapContext::default();
context2.add_identifier("2");
let dispatch_path = vec![(2, context2), (1, context1)];
let keymap = Keymap::new(vec![
Binding::new("a b", AB, Some("1")),
Binding::new("b", B, Some("2")),
Binding::new("c", C, Some("2")),
Binding::new("d", D, Some("1")),
Binding::new("d", D, Some("2")),
Binding::new("d a", DA, Some("2")),
]);
let mut matcher = KeymapMatcher::new(keymap);
// Binding with pending prefix always takes precedence
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
MatchResult::Pending,
);
// B alone doesn't match because a was pending, so AB is returned instead
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
MatchResult::Matches(vec![(1, Box::new(AB))]),
);
assert!(!matcher.has_pending_keystrokes());
// Without an a prefix, B is dispatched like expected
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
MatchResult::Matches(vec![(2, Box::new(B))]),
);
assert!(!matcher.has_pending_keystrokes());
// If a is prefixed, C will not be dispatched because there
// was a pending binding for it
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
MatchResult::Pending,
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
MatchResult::None,
);
assert!(!matcher.has_pending_keystrokes());
// If a single keystroke matches multiple bindings in the tree
// all of them are returned so that we can fallback if the action
// handler decides to propagate the action
assert_eq!(
matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
);
// If none of the d action handlers consume the binding, a pending
// binding may then be used
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
MatchResult::Matches(vec![(2, Box::new(DA))]),
);
assert!(!matcher.has_pending_keystrokes());
Ok(())
}
#[test]
fn test_keystroke_parsing() -> Result<()> {
assert_eq!(
Keystroke::parse("ctrl-p")?,
Keystroke {
key: "p".into(),
ctrl: true,
alt: false,
shift: false,
cmd: false,
function: false,
ime_key: None,
}
);
assert_eq!(
Keystroke::parse("alt-shift-down")?,
Keystroke {
key: "down".into(),
ctrl: false,
alt: true,
shift: true,
cmd: false,
function: false,
ime_key: None,
}
);
assert_eq!(
Keystroke::parse("shift-cmd--")?,
Keystroke {
key: "-".into(),
ctrl: false,
alt: false,
shift: true,
cmd: true,
function: false,
ime_key: None,
}
);
Ok(())
}
#[test]
fn test_context_predicate_parsing() -> Result<()> {
use KeymapContextPredicate::*;
assert_eq!(
KeymapContextPredicate::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!(
KeymapContextPredicate::parse("!a")?,
Not(Box::new(Identifier("a".into())),)
);
Ok(())
}
#[test]
fn test_context_predicate_eval() {
let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
let mut context = KeymapContext::default();
context.add_identifier("a");
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.add_identifier("a");
context.add_identifier("b");
assert!(predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.add_identifier("a");
context.add_key("c", "x");
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.add_identifier("a");
context.add_key("c", "d");
assert!(predicate.eval(&[context]));
let predicate = KeymapContextPredicate::parse("!a").unwrap();
assert!(predicate.eval(&[KeymapContext::default()]));
}
#[test]
fn test_context_child_predicate_eval() {
let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();
let contexts = [
context_set(&["e", "f"]),
context_set(&["c", "d"]), // match this context
context_set(&["a", "b"]),
];
assert!(!predicate.eval(&contexts[0..]));
assert!(predicate.eval(&contexts[1..]));
assert!(!predicate.eval(&contexts[2..]));
let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();
let contexts = [
context_set(&["f"]),
context_set(&["e"]), // only match this context
context_set(&["c"]),
context_set(&["a", "b"]),
context_set(&["e"]),
context_set(&["c", "d"]),
context_set(&["a", "b"]),
];
assert!(!predicate.eval(&contexts[0..]));
assert!(predicate.eval(&contexts[1..]));
assert!(!predicate.eval(&contexts[2..]));
assert!(!predicate.eval(&contexts[3..]));
assert!(!predicate.eval(&contexts[4..]));
assert!(!predicate.eval(&contexts[5..]));
assert!(!predicate.eval(&contexts[6..]));
fn context_set(names: &[&str]) -> KeymapContext {
let mut keymap = KeymapContext::new();
names
.iter()
.for_each(|name| keymap.add_identifier(name.to_string()));
keymap
}
}
#[test]
fn test_matcher() -> Result<()> {
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
pub struct A(pub String);
impl_actions!(test, [A]);
actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
#[derive(Clone, Debug, Eq, PartialEq)]
struct ActionArg {
a: &'static str,
}
let keymap = Keymap::new(vec![
Binding::new("a", A("x".to_string()), Some("a")),
Binding::new("b", B, Some("a")),
Binding::new("a b", Ab, Some("a || b")),
Binding::new("$", Dollar, Some("a")),
Binding::new("\"", Quote, Some("a")),
Binding::new("alt-s", Ess, Some("a")),
Binding::new("ctrl-`", Backtick, Some("a")),
]);
let mut context_a = KeymapContext::default();
context_a.add_identifier("a");
let mut context_b = KeymapContext::default();
context_b.add_identifier("b");
let mut matcher = KeymapMatcher::new(keymap);
// Basic match
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
);
matcher.clear_pending();
// Multi-keystroke match
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
MatchResult::Pending
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
MatchResult::Matches(vec![(1, Box::new(Ab))])
);
matcher.clear_pending();
// Failed matches don't interfere with matching subsequent keys
assert_eq!(
matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
MatchResult::None
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
);
matcher.clear_pending();
// Pending keystrokes are cleared when the context changes
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
MatchResult::Pending
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
MatchResult::None
);
matcher.clear_pending();
let mut context_c = KeymapContext::default();
context_c.add_identifier("c");
// Pending keystrokes are maintained per-view
assert_eq!(
matcher.push_keystroke(
Keystroke::parse("a")?,
vec![(1, context_b.clone()), (2, context_c.clone())]
),
MatchResult::Pending
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
MatchResult::Matches(vec![(1, Box::new(Ab))])
);
// handle Czech $ (option + 4 key)
assert_eq!(
matcher.push_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(Dollar))])
);
// handle Brazillian quote (quote key then space key)
assert_eq!(
matcher.push_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(Quote))])
);
// handle ctrl+` on a brazillian keyboard
assert_eq!(
matcher.push_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(Backtick))])
);
// handle alt-s on a US keyboard
assert_eq!(
matcher.push_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(Ess))])
);
Ok(())
}
}

View file

@ -1,111 +0,0 @@
use anyhow::Result;
use smallvec::SmallVec;
use crate::Action;
use super::{KeymapContext, KeymapContextPredicate, Keystroke};
pub struct Binding {
action: Box<dyn Action>,
pub(super) keystrokes: SmallVec<[Keystroke; 2]>,
pub(super) context_predicate: Option<KeymapContextPredicate>,
}
impl std::fmt::Debug for Binding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Binding {{ keystrokes: {:?}, action: {}::{}, context_predicate: {:?} }}",
self.keystrokes,
self.action.namespace(),
self.action.name(),
self.context_predicate
)
}
}
impl Clone for Binding {
fn clone(&self) -> Self {
Self {
action: self.action.boxed_clone(),
keystrokes: self.keystrokes.clone(),
context_predicate: self.context_predicate.clone(),
}
}
}
impl Binding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
Self::load(keystrokes, Box::new(action), context).unwrap()
}
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
let context = if let Some(context) = context {
Some(KeymapContextPredicate::parse(context)?)
} else {
None
};
let keystrokes = keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<_>>()?;
Ok(Self {
keystrokes,
action,
context_predicate: context,
})
}
pub fn match_context(&self, contexts: &[KeymapContext]) -> bool {
self.context_predicate
.as_ref()
.map(|predicate| predicate.eval(contexts))
.unwrap_or(true)
}
pub fn match_keys_and_context(
&self,
pending_keystrokes: &Vec<Keystroke>,
contexts: &[KeymapContext],
) -> BindingMatchResult {
if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.match_context(contexts)
{
// If the binding is completed, push it onto the matches list
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
BindingMatchResult::Complete(self.action.boxed_clone())
} else {
BindingMatchResult::Partial
}
} else {
BindingMatchResult::Fail
}
}
pub fn keystrokes_for_action(
&self,
action: &dyn Action,
contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> {
if self.action.eq(action) && self.match_context(contexts) {
Some(self.keystrokes.clone())
} else {
None
}
}
pub fn keystrokes(&self) -> &[Keystroke] {
self.keystrokes.as_slice()
}
pub fn action(&self) -> &dyn Action {
self.action.as_ref()
}
}
pub enum BindingMatchResult {
Complete(Box<dyn Action>),
Partial,
Fail,
}

View file

@ -1,392 +0,0 @@
use collections::HashSet;
use smallvec::SmallVec;
use std::{any::TypeId, collections::HashMap};
use crate::{Action, NoAction};
use super::{Binding, KeymapContextPredicate, Keystroke};
#[derive(Default)]
pub struct Keymap {
bindings: Vec<Binding>,
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
disabled_keystrokes: HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeymapContextPredicate>>>,
}
impl Keymap {
#[cfg(test)]
pub(super) fn new(bindings: Vec<Binding>) -> Self {
let mut this = Self::default();
this.add_bindings(bindings);
this
}
pub(crate) fn bindings_for_action(
&self,
action_id: TypeId,
) -> impl Iterator<Item = &'_ Binding> {
self.binding_indices_by_action_id
.get(&action_id)
.map(SmallVec::as_slice)
.unwrap_or(&[])
.iter()
.map(|ix| &self.bindings[*ix])
.filter(|binding| !self.binding_disabled(binding))
}
pub(crate) fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
let no_action_id = (NoAction {}).id();
let mut new_bindings = Vec::new();
let mut has_new_disabled_keystrokes = false;
for binding in bindings {
if binding.action().id() == no_action_id {
has_new_disabled_keystrokes |= self
.disabled_keystrokes
.entry(binding.keystrokes)
.or_default()
.insert(binding.context_predicate);
} else {
new_bindings.push(binding);
}
}
if has_new_disabled_keystrokes {
self.binding_indices_by_action_id.retain(|_, indices| {
indices.retain(|ix| {
let binding = &self.bindings[*ix];
match self.disabled_keystrokes.get(&binding.keystrokes) {
Some(disabled_predicates) => {
!disabled_predicates.contains(&binding.context_predicate)
}
None => true,
}
});
!indices.is_empty()
});
}
for new_binding in new_bindings {
if !self.binding_disabled(&new_binding) {
self.binding_indices_by_action_id
.entry(new_binding.action().id())
.or_default()
.push(self.bindings.len());
self.bindings.push(new_binding);
}
}
}
pub(crate) fn clear(&mut self) {
self.bindings.clear();
self.binding_indices_by_action_id.clear();
self.disabled_keystrokes.clear();
}
pub fn bindings(&self) -> Vec<&Binding> {
self.bindings
.iter()
.filter(|binding| !self.binding_disabled(binding))
.collect()
}
fn binding_disabled(&self, binding: &Binding) -> bool {
match self.disabled_keystrokes.get(&binding.keystrokes) {
Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate),
None => false,
}
}
}
#[cfg(test)]
mod tests {
use crate::actions;
use super::*;
actions!(
keymap_test,
[Present1, Present2, Present3, Duplicate, Missing]
);
#[test]
fn regular_keymap() {
let present_1 = Binding::new("ctrl-q", Present1 {}, None);
let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
let missing = Binding::new("ctrl-r", Missing {}, None);
let all_bindings = [
&present_1,
&present_2,
&present_3,
&keystroke_duplicate_to_1,
&full_duplicate_to_2,
&missing,
];
let mut keymap = Keymap::default();
assert_absent(&keymap, &all_bindings);
assert!(keymap.bindings().is_empty());
keymap.add_bindings([present_1.clone(), present_2.clone(), present_3.clone()]);
assert_absent(&keymap, &[&keystroke_duplicate_to_1, &missing]);
assert_present(
&keymap,
&[(&present_1, "q"), (&present_2, "w"), (&present_3, "e")],
);
keymap.add_bindings([
keystroke_duplicate_to_1.clone(),
full_duplicate_to_2.clone(),
]);
assert_absent(&keymap, &[&missing]);
assert!(
!keymap.binding_disabled(&keystroke_duplicate_to_1),
"Duplicate binding 1 was added and should not be disabled"
);
assert!(
!keymap.binding_disabled(&full_duplicate_to_2),
"Duplicate binding 2 was added and should not be disabled"
);
assert_eq!(
keymap
.bindings_for_action(keystroke_duplicate_to_1.action().id())
.map(|binding| &binding.keystrokes)
.flatten()
.collect::<Vec<_>>(),
vec![&Keystroke {
ctrl: true,
alt: false,
shift: false,
cmd: false,
function: false,
key: "q".to_string(),
ime_key: None,
}],
"{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap"
);
assert_eq!(
keymap
.bindings_for_action(full_duplicate_to_2.action().id())
.map(|binding| &binding.keystrokes)
.flatten()
.collect::<Vec<_>>(),
vec![
&Keystroke {
ctrl: true,
alt: false,
shift: false,
cmd: false,
function: false,
key: "w".to_string(),
ime_key: None,
},
&Keystroke {
ctrl: true,
alt: false,
shift: false,
cmd: false,
function: false,
key: "w".to_string(),
ime_key: None,
}
],
"{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap"
);
let updated_bindings = keymap.bindings();
let expected_updated_bindings = vec![
&present_1,
&present_2,
&present_3,
&keystroke_duplicate_to_1,
&full_duplicate_to_2,
];
assert_eq!(
updated_bindings.len(),
expected_updated_bindings.len(),
"Unexpected updated keymap bindings {updated_bindings:?}"
);
for (i, expected) in expected_updated_bindings.iter().enumerate() {
let keymap_binding = &updated_bindings[i];
assert_eq!(
keymap_binding.context_predicate, expected.context_predicate,
"Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
);
assert_eq!(
keymap_binding.keystrokes, expected.keystrokes,
"Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
);
}
keymap.clear();
assert_absent(&keymap, &all_bindings);
assert!(keymap.bindings().is_empty());
}
#[test]
fn keymap_with_ignored() {
let present_1 = Binding::new("ctrl-q", Present1 {}, None);
let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
let ignored_1 = Binding::new("ctrl-q", NoAction {}, None);
let ignored_2 = Binding::new("ctrl-w", NoAction {}, Some("pane"));
let ignored_3_with_other_context =
Binding::new("ctrl-e", NoAction {}, Some("other_context"));
let mut keymap = Keymap::default();
keymap.add_bindings([
ignored_1.clone(),
ignored_2.clone(),
ignored_3_with_other_context.clone(),
]);
assert_absent(&keymap, &[&present_3]);
assert_disabled(
&keymap,
&[
&present_1,
&present_2,
&ignored_1,
&ignored_2,
&ignored_3_with_other_context,
],
);
assert!(keymap.bindings().is_empty());
keymap.clear();
keymap.add_bindings([
present_1.clone(),
present_2.clone(),
present_3.clone(),
ignored_1.clone(),
ignored_2.clone(),
ignored_3_with_other_context.clone(),
]);
assert_present(&keymap, &[(&present_3, "e")]);
assert_disabled(
&keymap,
&[
&present_1,
&present_2,
&ignored_1,
&ignored_2,
&ignored_3_with_other_context,
],
);
keymap.clear();
keymap.add_bindings([
present_1.clone(),
present_2.clone(),
present_3.clone(),
ignored_1.clone(),
]);
assert_present(&keymap, &[(&present_2, "w"), (&present_3, "e")]);
assert_disabled(&keymap, &[&present_1, &ignored_1]);
assert_absent(&keymap, &[&ignored_2, &ignored_3_with_other_context]);
keymap.clear();
keymap.add_bindings([
present_1.clone(),
present_2.clone(),
present_3.clone(),
keystroke_duplicate_to_1.clone(),
full_duplicate_to_2.clone(),
ignored_1.clone(),
ignored_2.clone(),
ignored_3_with_other_context.clone(),
]);
assert_present(&keymap, &[(&present_3, "e")]);
assert_disabled(
&keymap,
&[
&present_1,
&present_2,
&keystroke_duplicate_to_1,
&full_duplicate_to_2,
&ignored_1,
&ignored_2,
&ignored_3_with_other_context,
],
);
keymap.clear();
}
#[track_caller]
fn assert_present(keymap: &Keymap, expected_bindings: &[(&Binding, &str)]) {
let keymap_bindings = keymap.bindings();
assert_eq!(
expected_bindings.len(),
keymap_bindings.len(),
"Unexpected keymap bindings {keymap_bindings:?}"
);
for (i, (expected, expected_key)) in expected_bindings.iter().enumerate() {
assert!(
!keymap.binding_disabled(expected),
"{expected:?} should not be disabled as it was added into keymap for element {i}"
);
assert_eq!(
keymap
.bindings_for_action(expected.action().id())
.map(|binding| &binding.keystrokes)
.flatten()
.collect::<Vec<_>>(),
vec![&Keystroke {
ctrl: true,
alt: false,
shift: false,
cmd: false,
function: false,
key: expected_key.to_string(),
ime_key: None,
}],
"{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}"
);
let keymap_binding = &keymap_bindings[i];
assert_eq!(
keymap_binding.context_predicate, expected.context_predicate,
"Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
);
assert_eq!(
keymap_binding.keystrokes, expected.keystrokes,
"Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
);
}
}
#[track_caller]
fn assert_absent(keymap: &Keymap, bindings: &[&Binding]) {
for binding in bindings.iter() {
assert!(
!keymap.binding_disabled(binding),
"{binding:?} should not be disabled in the keymap where was not added"
);
assert_eq!(
keymap.bindings_for_action(binding.action().id()).count(),
0,
"{binding:?} should have no actions in the keymap where was not added"
);
}
}
#[track_caller]
fn assert_disabled(keymap: &Keymap, bindings: &[&Binding]) {
for binding in bindings.iter() {
assert!(
keymap.binding_disabled(binding),
"{binding:?} should be disabled in the keymap"
);
assert_eq!(
keymap.bindings_for_action(binding.action().id()).count(),
0,
"{binding:?} should have no actions in the keymap where it was disabled"
);
}
}
}

View file

@ -1,326 +0,0 @@
use std::borrow::Cow;
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct KeymapContext {
set: HashSet<Cow<'static, str>>,
map: HashMap<Cow<'static, str>, Cow<'static, str>>,
}
impl KeymapContext {
pub fn new() -> Self {
KeymapContext {
set: HashSet::default(),
map: HashMap::default(),
}
}
pub fn clear(&mut self) {
self.set.clear();
self.map.clear();
}
pub fn extend(&mut self, other: &Self) {
for v in &other.set {
self.set.insert(v.clone());
}
for (k, v) in &other.map {
self.map.insert(k.clone(), v.clone());
}
}
pub fn add_identifier<I: Into<Cow<'static, str>>>(&mut self, identifier: I) {
self.set.insert(identifier.into());
}
pub fn add_key<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
&mut self,
key: S1,
value: S2,
) {
self.map.insert(key.into(), value.into());
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum KeymapContextPredicate {
Identifier(String),
Equal(String, String),
NotEqual(String, String),
Child(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
Not(Box<KeymapContextPredicate>),
And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
}
impl KeymapContextPredicate {
pub fn parse(source: &str) -> Result<Self> {
let source = Self::skip_whitespace(source);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if let Some(next) = rest.chars().next() {
Err(anyhow!("unexpected character {next:?}"))
} else {
Ok(predicate)
}
}
pub fn eval(&self, contexts: &[KeymapContext]) -> bool {
let Some(context) = contexts.first() else {
return false;
};
match self {
Self::Identifier(name) => (&context.set).contains(name.as_str()),
Self::Equal(left, right) => context
.map
.get(left.as_str())
.map(|value| value == right)
.unwrap_or(false),
Self::NotEqual(left, right) => context
.map
.get(left.as_str())
.map(|value| value != right)
.unwrap_or(true),
Self::Not(pred) => !pred.eval(contexts),
Self::Child(parent, child) => parent.eval(&contexts[1..]) && child.eval(contexts),
Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
}
}
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
type Op =
fn(KeymapContextPredicate, KeymapContextPredicate) -> Result<KeymapContextPredicate>;
let (mut predicate, rest) = Self::parse_primary(source)?;
source = rest;
'parse: loop {
for (operator, precedence, constructor) in [
(">", PRECEDENCE_CHILD, Self::new_child as Op),
("&&", PRECEDENCE_AND, Self::new_and as Op),
("||", PRECEDENCE_OR, Self::new_or as Op),
("==", PRECEDENCE_EQ, Self::new_eq as Op),
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
] {
if source.starts_with(operator) && precedence >= min_precedence {
source = Self::skip_whitespace(&source[operator.len()..]);
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
predicate = constructor(predicate, right)?;
source = rest;
continue 'parse;
}
}
break;
}
Ok((predicate, source))
}
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
let next = source
.chars()
.next()
.ok_or_else(|| anyhow!("unexpected eof"))?;
match next {
'(' => {
source = Self::skip_whitespace(&source[1..]);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if rest.starts_with(')') {
source = Self::skip_whitespace(&rest[1..]);
Ok((predicate, source))
} else {
Err(anyhow!("expected a ')'"))
}
}
'!' => {
let source = Self::skip_whitespace(&source[1..]);
let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
Ok((KeymapContextPredicate::Not(Box::new(predicate)), source))
}
_ if next.is_alphanumeric() || next == '_' => {
let len = source
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
.unwrap_or(source.len());
let (identifier, rest) = source.split_at(len);
source = Self::skip_whitespace(rest);
Ok((
KeymapContextPredicate::Identifier(identifier.into()),
source,
))
}
_ => Err(anyhow!("unexpected character {next:?}")),
}
}
fn skip_whitespace(source: &str) -> &str {
let len = source
.find(|c: char| !c.is_whitespace())
.unwrap_or(source.len());
&source[len..]
}
fn new_or(self, other: Self) -> Result<Self> {
Ok(Self::Or(Box::new(self), Box::new(other)))
}
fn new_and(self, other: Self) -> Result<Self> {
Ok(Self::And(Box::new(self), Box::new(other)))
}
fn new_child(self, other: Self) -> Result<Self> {
Ok(Self::Child(Box::new(self), Box::new(other)))
}
fn new_eq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::Equal(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
fn new_neq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::NotEqual(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
}
const PRECEDENCE_CHILD: u32 = 1;
const PRECEDENCE_OR: u32 = 2;
const PRECEDENCE_AND: u32 = 3;
const PRECEDENCE_EQ: u32 = 4;
const PRECEDENCE_NOT: u32 = 5;
#[cfg(test)]
mod tests {
use super::KeymapContextPredicate::{self, *};
#[test]
fn test_parse_identifiers() {
// Identifiers
assert_eq!(
KeymapContextPredicate::parse("abc12").unwrap(),
Identifier("abc12".into())
);
assert_eq!(
KeymapContextPredicate::parse("_1a").unwrap(),
Identifier("_1a".into())
);
}
#[test]
fn test_parse_negations() {
assert_eq!(
KeymapContextPredicate::parse("!abc").unwrap(),
Not(Box::new(Identifier("abc".into())))
);
assert_eq!(
KeymapContextPredicate::parse(" ! ! abc").unwrap(),
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
);
}
#[test]
fn test_parse_equality_operators() {
assert_eq!(
KeymapContextPredicate::parse("a == b").unwrap(),
Equal("a".into(), "b".into())
);
assert_eq!(
KeymapContextPredicate::parse("c!=d").unwrap(),
NotEqual("c".into(), "d".into())
);
assert_eq!(
KeymapContextPredicate::parse("c == !d")
.unwrap_err()
.to_string(),
"operands must be identifiers"
);
}
#[test]
fn test_parse_boolean_operators() {
assert_eq!(
KeymapContextPredicate::parse("a || b").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)
);
assert_eq!(
KeymapContextPredicate::parse("a || !b && c").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(And(
Box::new(Not(Box::new(Identifier("b".into())))),
Box::new(Identifier("c".into()))
))
)
);
assert_eq!(
KeymapContextPredicate::parse("a && b || c&&d").unwrap(),
Or(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(And(
Box::new(Identifier("c".into())),
Box::new(Identifier("d".into()))
))
)
);
assert_eq!(
KeymapContextPredicate::parse("a == b && c || d == e && f").unwrap(),
Or(
Box::new(And(
Box::new(Equal("a".into(), "b".into())),
Box::new(Identifier("c".into()))
)),
Box::new(And(
Box::new(Equal("d".into(), "e".into())),
Box::new(Identifier("f".into()))
))
)
);
assert_eq!(
KeymapContextPredicate::parse("a && b && c && d").unwrap(),
And(
Box::new(And(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(Identifier("c".into())),
)),
Box::new(Identifier("d".into()))
),
);
}
#[test]
fn test_parse_parenthesized_expressions() {
assert_eq!(
KeymapContextPredicate::parse("a && (b == c || d != e)").unwrap(),
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!(
KeymapContextPredicate::parse(" ( a || b ) ").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into())),
)
);
}
}

View file

@ -1,37 +1,27 @@
mod event;
mod app_menu;
mod keystroke;
#[cfg(target_os = "macos")]
pub mod mac;
pub mod test;
pub mod current {
#[cfg(target_os = "macos")]
pub use super::mac::*;
}
mod mac;
#[cfg(any(test, feature = "test-support"))]
mod test;
use crate::{
executor,
fonts::{
Features as FontFeatures, FontId, GlyphId, Metrics as FontMetrics,
Properties as FontProperties,
},
geometry::{
rect::{RectF, RectI},
vector::Vector2F,
},
keymap_matcher::KeymapMatcher,
text_layout::{LineLayout, RunStyle},
Action, AnyWindowHandle, ClipboardItem, Menu, Scene,
point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap,
LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result,
Scene, SharedString, Size, TaskLabel,
};
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, bail};
use async_task::Runnable;
pub use event::*;
use pathfinder_geometry::vector::vec2f;
use postage::oneshot;
use schemars::JsonSchema;
use serde::Deserialize;
use sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use futures::channel::oneshot;
use parking::Unparker;
use seahash::SeaHasher;
use serde::{Deserialize, Serialize};
use sqlez::bindable::{Bind, Column, StaticColumnCount};
use sqlez::statement::Statement;
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use std::time::Duration;
use std::{
any::Any,
fmt::{self, Debug, Display},
@ -41,86 +31,294 @@ use std::{
str::FromStr,
sync::Arc,
};
use time::UtcOffset;
use uuid::Uuid;
pub trait Platform: Send + Sync {
fn dispatcher(&self) -> Arc<dyn Dispatcher>;
fn fonts(&self) -> Arc<dyn FontSystem>;
pub use app_menu::*;
pub use keystroke::*;
#[cfg(target_os = "macos")]
pub use mac::*;
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
pub use time::UtcOffset;
#[cfg(target_os = "macos")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
Rc::new(MacPlatform::new())
}
pub type DrawWindow = Box<dyn FnMut() -> Result<Scene>>;
pub(crate) trait Platform: 'static {
fn background_executor(&self) -> BackgroundExecutor;
fn foreground_executor(&self) -> ForegroundExecutor;
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
fn quit(&self);
fn restart(&self);
fn activate(&self, ignoring_other_apps: bool);
fn hide(&self);
fn hide_other_apps(&self);
fn unhide_other_apps(&self);
fn quit(&self);
fn screen_by_id(&self, id: Uuid) -> Option<Rc<dyn Screen>>;
fn screens(&self) -> Vec<Rc<dyn Screen>>;
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
fn active_window(&self) -> Option<AnyWindowHandle>;
fn open_window(
&self,
handle: AnyWindowHandle,
options: WindowOptions,
executor: Rc<executor::Foreground>,
) -> Box<dyn Window>;
fn main_window(&self) -> Option<AnyWindowHandle>;
draw: DrawWindow,
) -> Box<dyn PlatformWindow>;
fn add_status_item(&self, handle: AnyWindowHandle) -> Box<dyn Window>;
fn set_display_link_output_callback(
&self,
display_id: DisplayId,
callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>,
);
fn start_display_link(&self, display_id: DisplayId);
fn stop_display_link(&self, display_id: DisplayId);
// fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn PlatformWindow>;
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 should_auto_hide_scrollbars(&self) -> bool;
fn local_timezone(&self) -> UtcOffset;
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
fn app_path(&self) -> Result<PathBuf>;
fn app_version(&self) -> Result<AppVersion>;
fn os_name(&self) -> &'static str;
fn os_version(&self) -> Result<AppVersion>;
fn restart(&self);
}
pub(crate) trait ForegroundPlatform {
fn on_become_active(&self, callback: Box<dyn FnMut()>);
fn on_resign_active(&self, callback: Box<dyn FnMut()>);
fn on_quit(&self, callback: Box<dyn FnMut()>);
/// Handle the application being re-activated with no windows open.
fn on_reopen(&self, callback: Box<dyn FnMut()>);
fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
fn run(&self, on_finish_launching: Box<dyn FnOnce()>);
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, matcher: &KeymapMatcher);
fn prompt_for_paths(
&self,
options: PathPromptOptions,
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
fn reveal_path(&self, path: &Path);
fn on_become_active(&self, callback: Box<dyn FnMut()>);
fn on_resign_active(&self, callback: Box<dyn FnMut()>);
fn on_quit(&self, callback: Box<dyn FnMut()>);
fn on_reopen(&self, callback: Box<dyn FnMut()>);
fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn os_name(&self) -> &'static str;
fn os_version(&self) -> Result<SemanticVersion>;
fn app_version(&self) -> Result<SemanticVersion>;
fn app_path(&self) -> Result<PathBuf>;
fn local_timezone(&self) -> UtcOffset;
fn double_click_interval(&self) -> Duration;
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
fn set_cursor_style(&self, style: CursorStyle);
fn should_auto_hide_scrollbars(&self) -> bool;
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
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<()>;
}
pub trait Dispatcher: Send + Sync {
pub trait PlatformDisplay: Send + Sync + Debug {
fn id(&self) -> DisplayId;
/// Returns a stable identifier for this display that can be persisted and used
/// across system restarts.
fn uuid(&self) -> Result<Uuid>;
fn as_any(&self) -> &dyn Any;
fn bounds(&self) -> Bounds<GlobalPixels>;
}
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
pub struct DisplayId(pub(crate) u32);
impl Debug for DisplayId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DisplayId({})", self.0)
}
}
unsafe impl Send for DisplayId {}
pub trait PlatformWindow {
fn bounds(&self) -> WindowBounds;
fn content_size(&self) -> Size<Pixels>;
fn scale_factor(&self) -> f32;
fn titlebar_height(&self) -> Pixels;
fn appearance(&self) -> WindowAppearance;
fn display(&self) -> Rc<dyn PlatformDisplay>;
fn mouse_position(&self) -> Point<Pixels>;
fn modifiers(&self) -> Modifiers;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
fn clear_input_handler(&mut self);
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
fn activate(&self);
fn set_title(&mut self, title: &str);
fn set_edited(&mut self, edited: bool);
fn show_character_palette(&self);
fn minimize(&self);
fn zoom(&self);
fn toggle_full_screen(&self);
fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>);
fn on_moved(&self, callback: Box<dyn FnMut()>);
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
fn on_close(&self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn invalidate(&self);
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
#[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> {
None
}
}
pub trait PlatformDispatcher: Send + Sync {
fn is_main_thread(&self) -> bool;
fn run_on_main_thread(&self, task: Runnable);
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
fn dispatch_on_main_thread(&self, runnable: Runnable);
fn dispatch_after(&self, duration: Duration, runnable: Runnable);
fn tick(&self, background_only: bool) -> bool;
fn park(&self);
fn unparker(&self) -> Unparker;
#[cfg(any(test, feature = "test-support"))]
fn as_test(&self) -> Option<&TestDispatcher> {
None
}
}
pub trait InputHandler {
fn selected_text_range(&self) -> Option<Range<usize>>;
fn marked_text_range(&self) -> Option<Range<usize>>;
fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String>;
pub trait PlatformTextSystem: Send + Sync {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
fn all_font_families(&self) -> Vec<String>;
fn font_id(&self, descriptor: &Font) -> Result<FontId>;
fn font_metrics(&self, font_id: FontId) -> FontMetrics;
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
fn wrap_line(
&self,
text: &str,
font_id: FontId,
font_size: Pixels,
width: Pixels,
) -> Vec<usize>;
}
#[derive(Clone, Debug)]
pub struct AppMetadata {
pub os_name: &'static str,
pub os_version: Option<SemanticVersion>,
pub app_version: Option<SemanticVersion>,
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub enum AtlasKey {
Glyph(RenderGlyphParams),
Svg(RenderSvgParams),
Image(RenderImageParams),
}
impl AtlasKey {
pub(crate) fn texture_kind(&self) -> AtlasTextureKind {
match self {
AtlasKey::Glyph(params) => {
if params.is_emoji {
AtlasTextureKind::Polychrome
} else {
AtlasTextureKind::Monochrome
}
}
AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
}
}
}
impl From<RenderGlyphParams> for AtlasKey {
fn from(params: RenderGlyphParams) -> Self {
Self::Glyph(params)
}
}
impl From<RenderSvgParams> for AtlasKey {
fn from(params: RenderSvgParams) -> Self {
Self::Svg(params)
}
}
impl From<RenderImageParams> for AtlasKey {
fn from(params: RenderImageParams) -> Self {
Self::Image(params)
}
}
pub trait PlatformAtlas: Send + Sync {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
) -> Result<AtlasTile>;
fn clear(&self);
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct AtlasTile {
pub(crate) texture_id: AtlasTextureId,
pub(crate) tile_id: TileId,
pub(crate) bounds: Bounds<DevicePixels>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(C)]
pub(crate) struct AtlasTextureId {
// We use u32 instead of usize for Metal Shader Language compatibility
pub(crate) index: u32,
pub(crate) kind: AtlasTextureKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(C)]
pub(crate) enum AtlasTextureKind {
Monochrome = 0,
Polychrome = 1,
Path = 2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[repr(C)]
pub(crate) struct TileId(pub(crate) u32);
impl From<etagere::AllocId> for TileId {
fn from(id: etagere::AllocId) -> Self {
Self(id.serialize())
}
}
impl From<TileId> for etagere::AllocId {
fn from(id: TileId) -> Self {
Self::deserialize(id.0)
}
}
pub trait PlatformInputHandler: 'static {
fn selected_text_range(&mut self) -> Option<Range<usize>>;
fn marked_text_range(&mut self) -> Option<Range<usize>>;
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String>;
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
fn replace_and_mark_text_in_range(
&mut self,
@ -129,75 +327,45 @@ pub trait InputHandler {
new_selected_range: Option<Range<usize>>,
);
fn unmark_text(&mut self);
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF>;
}
pub trait Screen: Debug {
fn as_any(&self) -> &dyn Any;
fn bounds(&self) -> RectF;
fn content_bounds(&self) -> RectF;
fn display_uuid(&self) -> Option<Uuid>;
}
pub trait Window {
fn bounds(&self) -> WindowBounds;
fn content_size(&self) -> Vector2F;
fn scale_factor(&self) -> f32;
fn titlebar_height(&self) -> f32;
fn appearance(&self) -> Appearance;
fn screen(&self) -> Rc<dyn Screen>;
fn mouse_position(&self) -> Vector2F;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
fn activate(&self);
fn set_title(&mut self, title: &str);
fn set_edited(&mut self, edited: bool);
fn show_character_palette(&self);
fn minimize(&self);
fn zoom(&self);
fn present_scene(&mut self, scene: Scene);
fn toggle_full_screen(&self);
fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
fn on_resize(&mut self, callback: Box<dyn FnMut()>);
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>);
fn on_moved(&mut self, callback: Box<dyn FnMut()>);
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Vector2F) -> bool;
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>>;
}
#[derive(Debug)]
pub struct WindowOptions<'a> {
pub struct WindowOptions {
pub bounds: WindowBounds,
pub titlebar: Option<TitlebarOptions<'a>>,
pub titlebar: Option<TitlebarOptions>,
pub center: bool,
pub focus: bool,
pub show: bool,
pub kind: WindowKind,
pub is_movable: bool,
pub screen: Option<Rc<dyn Screen>>,
pub display_id: Option<DisplayId>,
}
impl<'a> WindowOptions<'a> {
pub fn with_bounds(bounds: Vector2F) -> Self {
impl Default for WindowOptions {
fn default() -> Self {
Self {
bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), bounds)),
center: true,
..Default::default()
bounds: WindowBounds::default(),
titlebar: Some(TitlebarOptions {
title: Default::default(),
appears_transparent: Default::default(),
traffic_light_position: Default::default(),
}),
center: false,
focus: true,
show: true,
kind: WindowKind::Normal,
is_movable: true,
display_id: None,
}
}
}
#[derive(Debug, Default)]
pub struct TitlebarOptions<'a> {
pub title: Option<&'a str>,
pub struct TitlebarOptions {
pub title: Option<SharedString>,
pub appears_transparent: bool,
pub traffic_light_position: Option<Vector2F>,
pub traffic_light_position: Option<Point<Pixels>>,
}
#[derive(Copy, Clone, Debug)]
@ -220,11 +388,12 @@ pub enum WindowKind {
PopUp,
}
#[derive(Copy, Clone, Debug, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub enum WindowBounds {
Fullscreen,
#[default]
Maximized,
Fixed(RectF),
Fixed(Bounds<GlobalPixels>),
}
impl StaticColumnCount for WindowBounds {
@ -253,10 +422,10 @@ impl Bind for WindowBounds {
statement.bind(
&region.map(|region| {
(
region.min_x(),
region.min_y(),
region.width(),
region.height(),
region.origin.x,
region.origin.y,
region.size.width,
region.size.height,
)
}),
next_index,
@ -272,10 +441,14 @@ impl Column for WindowBounds {
"Maximized" => WindowBounds::Maximized,
"Fixed" => {
let ((x, y, width, height), _) = Column::column(statement, next_index)?;
WindowBounds::Fixed(RectF::new(
Vector2F::new(x, y),
Vector2F::new(width, height),
))
let x: f64 = x;
let y: f64 = y;
let width: f64 = width;
let height: f64 = height;
WindowBounds::Fixed(Bounds {
origin: point(x.into(), y.into()),
size: size(width.into(), height.into()),
})
}
_ => bail!("Window State did not have a valid string"),
};
@ -284,25 +457,55 @@ impl Column for WindowBounds {
}
}
#[derive(Copy, Clone, Debug)]
pub enum WindowAppearance {
Light,
VibrantLight,
Dark,
VibrantDark,
}
impl Default for WindowAppearance {
fn default() -> Self {
Self::Light
}
}
#[derive(Copy, Clone, Debug)]
pub struct PathPromptOptions {
pub files: bool,
pub directories: bool,
pub multiple: bool,
}
#[derive(Copy, Clone, Debug)]
pub enum PromptLevel {
Info,
Warning,
Critical,
}
#[derive(Copy, Clone, Debug, Deserialize, JsonSchema)]
/// The style of the cursor (pointer)
#[derive(Copy, Clone, Debug)]
pub enum CursorStyle {
Arrow,
ResizeLeftRight,
ResizeUpDown,
PointingHand,
IBeam,
Crosshair,
ClosedHand,
OpenHand,
PointingHand,
ResizeLeft,
ResizeRight,
ResizeLeftRight,
ResizeUp,
ResizeDown,
ResizeUpDown,
DisappearingItem,
IBeamCursorForVerticalLayout,
OperationNotAllowed,
DragLink,
DragCopy,
ContextualMenu,
}
impl Default for CursorStyle {
@ -311,14 +514,14 @@ impl Default for CursorStyle {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct AppVersion {
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct SemanticVersion {
major: usize,
minor: usize,
patch: usize,
}
impl FromStr for AppVersion {
impl FromStr for SemanticVersion {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
@ -343,59 +546,47 @@ impl FromStr for AppVersion {
}
}
impl Display for AppVersion {
impl Display for SemanticVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Copy, Clone, Debug)]
pub enum RasterizationOptions {
Alpha,
Bgra,
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardItem {
pub(crate) text: String,
pub(crate) metadata: Option<String>,
}
pub trait FontSystem: Send + Sync {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
fn all_families(&self) -> Vec<String>;
fn load_family(&self, name: &str, features: &FontFeatures) -> 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 advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Vector2F>;
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,
options: RasterizationOptions,
) -> 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 {
impl ClipboardItem {
pub fn new(text: String) -> Self {
Self {
bounds: WindowBounds::Maximized,
titlebar: Some(TitlebarOptions {
title: Default::default(),
appears_transparent: Default::default(),
traffic_light_position: Default::default(),
}),
center: false,
focus: true,
show: true,
kind: WindowKind::Normal,
is_movable: true,
screen: None,
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()
}
}

View file

@ -0,0 +1,77 @@
use crate::{Action, AppContext, Platform};
use util::ResultExt;
pub struct Menu<'a> {
pub name: &'a str,
pub items: Vec<MenuItem<'a>>,
}
pub enum MenuItem<'a> {
Separator,
Submenu(Menu<'a>),
Action {
name: &'a str,
action: Box<dyn Action>,
os_action: Option<OsAction>,
},
}
impl<'a> MenuItem<'a> {
pub fn separator() -> Self {
Self::Separator
}
pub fn submenu(menu: Menu<'a>) -> Self {
Self::Submenu(menu)
}
pub fn action(name: &'a str, action: impl Action) -> Self {
Self::Action {
name,
action: Box::new(action),
os_action: None,
}
}
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
Self::Action {
name,
action: Box::new(action),
os_action: Some(os_action),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum OsAction {
Cut,
Copy,
Paste,
SelectAll,
Undo,
Redo,
}
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) {
platform.on_will_open_app_menu(Box::new({
let cx = cx.to_async();
move || {
cx.update(|cx| cx.clear_pending_keystrokes()).ok();
}
}));
platform.on_validate_app_menu_command(Box::new({
let cx = cx.to_async();
move |action| {
cx.update(|cx| cx.is_action_available(action))
.unwrap_or(false)
}
}));
platform.on_app_menu_action(Box::new({
let cx = cx.to_async();
move |action| {
cx.update(|cx| cx.dispatch_action(action)).log_err();
}
}));
}

View file

@ -1,236 +0,0 @@
use std::{any::Any, ops::Deref};
use pathfinder_geometry::vector::vec2f;
use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeyDownEvent {
pub keystroke: Keystroke,
pub is_held: bool,
}
#[derive(Clone, Debug)]
pub struct KeyUpEvent {
pub keystroke: Keystroke,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Modifiers {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub cmd: bool,
pub fun: bool,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ModifiersChangedEvent {
pub modifiers: Modifiers,
}
impl Deref for ModifiersChangedEvent {
type Target = Modifiers;
fn deref(&self) -> &Self::Target {
&self.modifiers
}
}
/// The phase of a touch motion event.
/// Based on the winit enum of the same name,
#[derive(Clone, Copy, Debug)]
pub enum TouchPhase {
Started,
Moved,
Ended,
}
#[derive(Clone, Copy, Debug)]
pub enum ScrollDelta {
Pixels(Vector2F),
Lines(Vector2F),
}
impl Default for ScrollDelta {
fn default() -> Self {
Self::Lines(Default::default())
}
}
impl ScrollDelta {
pub fn raw(&self) -> &Vector2F {
match self {
ScrollDelta::Pixels(v) => v,
ScrollDelta::Lines(v) => v,
}
}
pub fn precise(&self) -> bool {
match self {
ScrollDelta::Pixels(_) => true,
ScrollDelta::Lines(_) => false,
}
}
pub fn pixel_delta(&self, line_height: f32) -> Vector2F {
match self {
ScrollDelta::Pixels(delta) => *delta,
ScrollDelta::Lines(delta) => vec2f(delta.x() * line_height, delta.y() * line_height),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ScrollWheelEvent {
pub position: Vector2F,
pub delta: ScrollDelta,
pub modifiers: Modifiers,
/// If the platform supports returning the phase of a scroll wheel event, it will be stored here
pub phase: Option<TouchPhase>,
}
impl Deref for ScrollWheelEvent {
type Target = Modifiers;
fn deref(&self) -> &Self::Target {
&self.modifiers
}
}
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum NavigationDirection {
Back,
Forward,
}
impl Default for NavigationDirection {
fn default() -> Self {
Self::Back
}
}
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum MouseButton {
Left,
Right,
Middle,
Navigate(NavigationDirection),
}
impl MouseButton {
pub fn all() -> Vec<Self> {
vec![
MouseButton::Left,
MouseButton::Right,
MouseButton::Middle,
MouseButton::Navigate(NavigationDirection::Back),
MouseButton::Navigate(NavigationDirection::Forward),
]
}
}
impl Default for MouseButton {
fn default() -> Self {
Self::Left
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MouseButtonEvent {
pub button: MouseButton,
pub position: Vector2F,
pub modifiers: Modifiers,
pub click_count: usize,
pub is_down: bool,
}
impl Deref for MouseButtonEvent {
type Target = Modifiers;
fn deref(&self) -> &Self::Target {
&self.modifiers
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MouseMovedEvent {
pub position: Vector2F,
pub pressed_button: Option<MouseButton>,
pub modifiers: Modifiers,
}
impl Deref for MouseMovedEvent {
type Target = Modifiers;
fn deref(&self) -> &Self::Target {
&self.modifiers
}
}
impl MouseMovedEvent {
pub fn to_button_event(&self, button: MouseButton) -> MouseButtonEvent {
MouseButtonEvent {
position: self.position,
button: self.pressed_button.unwrap_or(button),
modifiers: self.modifiers,
click_count: 0,
is_down: self.pressed_button.is_some(),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MouseExitedEvent {
pub position: Vector2F,
pub pressed_button: Option<MouseButton>,
pub modifiers: Modifiers,
}
impl Deref for MouseExitedEvent {
type Target = Modifiers;
fn deref(&self) -> &Self::Target {
&self.modifiers
}
}
#[derive(Clone, Debug)]
pub enum Event {
KeyDown(KeyDownEvent),
KeyUp(KeyUpEvent),
ModifiersChanged(ModifiersChangedEvent),
MouseDown(MouseButtonEvent),
MouseUp(MouseButtonEvent),
MouseMoved(MouseMovedEvent),
MouseExited(MouseExitedEvent),
ScrollWheel(ScrollWheelEvent),
}
impl Event {
pub fn position(&self) -> Option<Vector2F> {
match self {
Event::KeyDown { .. } => None,
Event::KeyUp { .. } => None,
Event::ModifiersChanged { .. } => None,
Event::MouseDown(event) => Some(event.position),
Event::MouseUp(event) => Some(event.position),
Event::MouseMoved(event) => Some(event.position),
Event::MouseExited(event) => Some(event.position),
Event::ScrollWheel(event) => Some(event.position),
}
}
pub fn mouse_event<'a>(&'a self) -> Option<&'a dyn Any> {
match self {
Event::KeyDown { .. } => None,
Event::KeyUp { .. } => None,
Event::ModifiersChanged { .. } => None,
Event::MouseDown(event) => Some(event),
Event::MouseUp(event) => Some(event),
Event::MouseMoved(event) => Some(event),
Event::MouseExited(event) => Some(event),
Event::ScrollWheel(event) => Some(event),
}
}
}

View file

@ -1,16 +1,11 @@
use std::fmt::Write;
use anyhow::anyhow;
use serde::Deserialize;
use smallvec::SmallVec;
use std::fmt::Write;
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
pub struct Keystroke {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub cmd: bool,
pub function: bool,
pub modifiers: Modifiers,
/// key is the character printed on the key that was pressed
/// e.g. for option-s, key is "s"
pub key: String,
@ -25,17 +20,19 @@ impl Keystroke {
// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
// and on some keyboards the IME handler converts a sequence of keys into a
// specific character (for example `"` is typed as `" space` on a brazillian keyboard).
pub fn match_possibilities(&self) -> SmallVec<[Keystroke; 2]> {
pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
let mut possibilities = SmallVec::new();
match self.ime_key.as_ref() {
None => possibilities.push(self.clone()),
Some(ime_key) => {
possibilities.push(Keystroke {
ctrl: self.ctrl,
alt: false,
shift: false,
cmd: false,
function: false,
modifiers: Modifiers {
control: self.modifiers.control,
alt: false,
shift: false,
command: false,
function: false,
},
key: ime_key.to_string(),
ime_key: None,
});
@ -53,10 +50,10 @@ impl Keystroke {
/// ime_key is only used for generating test events,
/// when matching a key with an ime_key set will be matched without it.
pub fn parse(source: &str) -> anyhow::Result<Self> {
let mut ctrl = false;
let mut control = false;
let mut alt = false;
let mut shift = false;
let mut cmd = false;
let mut command = false;
let mut function = false;
let mut key = None;
let mut ime_key = None;
@ -64,10 +61,10 @@ impl Keystroke {
let mut components = source.split('-').peekable();
while let Some(component) = components.next() {
match component {
"ctrl" => ctrl = true,
"ctrl" => control = true,
"alt" => alt = true,
"shift" => shift = true,
"cmd" => cmd = true,
"cmd" => command = true,
"fn" => function = true,
_ => {
if let Some(next) = components.peek() {
@ -91,33 +88,31 @@ impl Keystroke {
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
Ok(Keystroke {
ctrl,
alt,
shift,
cmd,
function,
modifiers: Modifiers {
control,
alt,
shift,
command,
function,
},
key,
ime_key,
})
}
pub fn modified(&self) -> bool {
self.ctrl || self.alt || self.shift || self.cmd
}
}
impl std::fmt::Display for Keystroke {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.ctrl {
if self.modifiers.control {
f.write_char('^')?;
}
if self.alt {
if self.modifiers.alt {
f.write_char('⌥')?;
}
if self.cmd {
if self.modifiers.command {
f.write_char('⌘')?;
}
if self.shift {
if self.modifiers.shift {
f.write_char('⇧')?;
}
let key = match self.key.as_str() {
@ -139,3 +134,18 @@ impl std::fmt::Display for Keystroke {
f.write_char(key)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
pub struct Modifiers {
pub control: bool,
pub alt: bool,
pub shift: bool,
pub command: bool,
pub function: bool,
}
impl Modifiers {
pub fn modified(&self) -> bool {
self.control || self.alt || self.shift || self.command || self.function
}
}

View file

@ -1,39 +1,33 @@
mod appearance;
mod atlas;
//! Macos screen have a y axis that goings up from the bottom of the screen and
//! an origin at the bottom left of the main display.
mod dispatcher;
mod event;
mod fonts;
mod geometry;
mod image_cache;
mod display;
mod display_linker;
mod events;
mod metal_atlas;
mod metal_renderer;
mod open_type;
mod platform;
mod renderer;
mod screen;
mod sprite_cache;
mod status_item;
mod text_system;
mod window;
mod window_appearence;
use crate::{px, size, GlobalPixels, Pixels, Size};
use cocoa::{
base::{id, nil, BOOL, NO, YES},
foundation::{NSAutoreleasePool, NSNotFound, NSString, NSUInteger},
base::{id, nil},
foundation::{NSAutoreleasePool, NSNotFound, NSRect, NSSize, NSString, NSUInteger},
};
pub use dispatcher::Dispatcher;
pub use fonts::FontSystem;
use platform::{MacForegroundPlatform, MacPlatform};
pub use renderer::Surface;
use std::{ops::Range, rc::Rc, sync::Arc};
use window::MacWindow;
use metal_renderer::*;
use objc::runtime::{BOOL, NO, YES};
use std::ops::Range;
use crate::executor;
pub fn platform() -> Arc<dyn super::Platform> {
Arc::new(MacPlatform::new())
}
pub(crate) fn foreground_platform(
foreground: Rc<executor::Foreground>,
) -> Rc<dyn super::ForegroundPlatform> {
Rc::new(MacForegroundPlatform::new(foreground))
}
pub use dispatcher::*;
pub use display::*;
pub use display_linker::*;
pub use metal_atlas::*;
pub use platform::*;
pub use text_system::*;
pub use window::*;
trait BoolExt {
fn to_objc(self) -> BOOL;
@ -102,3 +96,44 @@ unsafe impl objc::Encode for NSRange {
unsafe fn ns_string(string: &str) -> id {
NSString::alloc(nil).init_str(string).autorelease()
}
impl From<NSSize> for Size<Pixels> {
fn from(value: NSSize) -> Self {
Size {
width: px(value.width as f32),
height: px(value.height as f32),
}
}
}
pub trait NSRectExt {
fn size(&self) -> Size<Pixels>;
fn intersects(&self, other: Self) -> bool;
}
impl From<NSRect> for Size<Pixels> {
fn from(rect: NSRect) -> Self {
let NSSize { width, height } = rect.size;
size(width.into(), height.into())
}
}
impl From<NSRect> for Size<GlobalPixels> {
fn from(rect: NSRect) -> Self {
let NSSize { width, height } = rect.size;
size(width.into(), height.into())
}
}
// impl NSRectExt for NSRect {
// fn intersects(&self, other: Self) -> bool {
// self.size.width > 0.
// && self.size.height > 0.
// && other.size.width > 0.
// && other.size.height > 0.
// && self.origin.x <= other.origin.x + other.size.width
// && self.origin.x + self.size.width >= other.origin.x
// && self.origin.y <= other.origin.y + other.size.height
// && self.origin.y + self.size.height >= other.origin.y
// }
// }

View file

@ -1,173 +0,0 @@
use crate::geometry::{
rect::RectI,
vector::{vec2i, Vector2I},
};
use etagere::BucketedAtlasAllocator;
use foreign_types::ForeignType;
use log::warn;
use metal::{Device, TextureDescriptor};
use objc::{msg_send, sel, sel_impl};
pub struct AtlasAllocator {
device: Device,
texture_descriptor: TextureDescriptor,
atlases: Vec<Atlas>,
last_used_atlas_id: usize,
}
#[derive(Copy, Clone, Debug)]
pub struct AllocId {
pub atlas_id: usize,
alloc_id: etagere::AllocId,
}
impl AtlasAllocator {
pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self {
let mut this = Self {
device,
texture_descriptor,
atlases: vec![],
last_used_atlas_id: 0,
};
let atlas = this.new_atlas(Vector2I::zero());
this.atlases.push(atlas);
this
}
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) -> Option<(AllocId, Vector2I)> {
let atlas_id = self.last_used_atlas_id;
if let Some((alloc_id, origin)) = self.atlases[atlas_id].allocate(requested_size) {
return Some((AllocId { atlas_id, alloc_id }, origin));
}
for (atlas_id, atlas) in self.atlases.iter_mut().enumerate() {
if atlas_id == self.last_used_atlas_id {
continue;
}
if let Some((alloc_id, origin)) = atlas.allocate(requested_size) {
self.last_used_atlas_id = atlas_id;
return Some((AllocId { atlas_id, alloc_id }, origin));
}
}
let atlas_id = self.atlases.len();
let mut atlas = self.new_atlas(requested_size);
let allocation = atlas
.allocate(requested_size)
.map(|(alloc_id, origin)| (AllocId { atlas_id, alloc_id }, origin));
self.atlases.push(atlas);
if allocation.is_none() {
warn!(
"allocation of size {:?} could not be created",
requested_size,
);
}
allocation
}
pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> Option<(AllocId, RectI)> {
let (alloc_id, origin) = self.allocate(size)?;
let bounds = RectI::new(origin, size);
self.atlases[alloc_id.atlas_id].upload(bounds, bytes);
Some((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);
}
}
pub fn clear(&mut self) {
for atlas in &mut self.atlases {
atlas.clear();
}
}
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 {
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 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 clear(&mut self) {
self.allocator.clear();
}
}

View file

@ -2,15 +2,16 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use crate::{PlatformDispatcher, TaskLabel};
use async_task::Runnable;
use objc::{
class, msg_send,
runtime::{BOOL, YES},
sel, sel_impl,
};
use std::ffi::c_void;
use crate::platform;
use parking::{Parker, Unparker};
use parking_lot::Mutex;
use std::{ffi::c_void, sync::Arc, time::Duration};
include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
@ -18,15 +19,41 @@ pub fn dispatch_get_main_queue() -> dispatch_queue_t {
unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
}
pub struct Dispatcher;
pub struct MacDispatcher {
parker: Arc<Mutex<Parker>>,
}
impl platform::Dispatcher for Dispatcher {
impl Default for MacDispatcher {
fn default() -> Self {
Self::new()
}
}
impl MacDispatcher {
pub fn new() -> Self {
MacDispatcher {
parker: Arc::new(Mutex::new(Parker::new())),
}
}
}
impl PlatformDispatcher for MacDispatcher {
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) {
fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
unsafe {
dispatch_async_f(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
runnable.into_raw() as *mut c_void,
Some(trampoline),
);
}
}
fn dispatch_on_main_thread(&self, runnable: Runnable) {
unsafe {
dispatch_async_f(
dispatch_get_main_queue(),
@ -34,10 +61,36 @@ impl platform::Dispatcher for Dispatcher {
Some(trampoline),
);
}
}
extern "C" fn trampoline(runnable: *mut c_void) {
let task = unsafe { Runnable::from_raw(runnable as *mut ()) };
task.run();
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
unsafe {
let queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0);
let when = dispatch_time(DISPATCH_TIME_NOW as u64, duration.as_nanos() as i64);
dispatch_after_f(
when,
queue,
runnable.into_raw() as *mut c_void,
Some(trampoline),
);
}
}
fn tick(&self, _background_only: bool) -> bool {
false
}
fn park(&self) {
self.parker.lock().park()
}
fn unparker(&self) -> Unparker {
self.parker.lock().unparker()
}
}
extern "C" fn trampoline(runnable: *mut c_void) {
let task = unsafe { Runnable::from_raw(runnable as *mut ()) };
task.run();
}

View file

@ -0,0 +1,143 @@
use crate::{point, size, Bounds, DisplayId, GlobalPixels, PlatformDisplay};
use anyhow::Result;
use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
use core_graphics::{
display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList},
geometry::{CGPoint, CGRect, CGSize},
};
use std::any::Any;
use uuid::Uuid;
#[derive(Debug)]
pub struct MacDisplay(pub(crate) CGDirectDisplayID);
unsafe impl Send for MacDisplay {}
impl MacDisplay {
/// Get the screen with the given [DisplayId].
pub fn find_by_id(id: DisplayId) -> Option<Self> {
Self::all().find(|screen| screen.id() == id)
}
/// Get the screen with the given persistent [Uuid].
pub fn find_by_uuid(uuid: Uuid) -> Option<Self> {
Self::all().find(|screen| screen.uuid().ok() == Some(uuid))
}
/// Get the primary screen - the one with the menu bar, and whose bottom left
/// corner is at the origin of the AppKit coordinate system.
pub fn primary() -> Self {
Self::all().next().unwrap()
}
/// Obtains an iterator over all currently active system displays.
pub fn all() -> impl Iterator<Item = Self> {
unsafe {
let mut display_count: u32 = 0;
let result = CGGetActiveDisplayList(0, std::ptr::null_mut(), &mut display_count);
if result == 0 {
let mut displays = Vec::with_capacity(display_count as usize);
CGGetActiveDisplayList(display_count, displays.as_mut_ptr(), &mut display_count);
displays.set_len(display_count as usize);
displays.into_iter().map(MacDisplay)
} else {
panic!("Failed to get active display list");
}
}
}
}
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
}
/// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space.
///
/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
/// with the Y axis pointing upwards.
///
/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
/// screen, with the Y axis pointing downwards.
pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds<GlobalPixels> {
let primary_screen_size = unsafe { CGDisplayBounds(MacDisplay::primary().id().0) }.size;
Bounds {
origin: point(
GlobalPixels(rect.origin.x as f32),
GlobalPixels(
primary_screen_size.height as f32 - rect.origin.y as f32 - rect.size.height as f32,
),
),
size: size(
GlobalPixels(rect.size.width as f32),
GlobalPixels(rect.size.height as f32),
),
}
}
/// Convert the given rectangle from GPUI's coordinate system to CoreGraphics' native coordinate space.
///
/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
/// with the Y axis pointing upwards.
///
/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
/// screen, with the Y axis pointing downwards.
pub(crate) fn display_bounds_to_native(bounds: Bounds<GlobalPixels>) -> CGRect {
let primary_screen_height = MacDisplay::primary().bounds().size.height;
CGRect::new(
&CGPoint::new(
bounds.origin.x.into(),
(primary_screen_height - bounds.origin.y - bounds.size.height).into(),
),
&CGSize::new(bounds.size.width.into(), bounds.size.height.into()),
)
}
impl PlatformDisplay for MacDisplay {
fn id(&self) -> DisplayId {
DisplayId(self.0)
}
fn uuid(&self) -> Result<Uuid> {
let cfuuid = unsafe { CGDisplayCreateUUIDFromDisplayID(self.0 as CGDirectDisplayID) };
anyhow::ensure!(
!cfuuid.is_null(),
"AppKit returned a null from CGDisplayCreateUUIDFromDisplayID"
);
let bytes = unsafe { CFUUIDGetUUIDBytes(cfuuid) };
Ok(Uuid::from_bytes([
bytes.byte0,
bytes.byte1,
bytes.byte2,
bytes.byte3,
bytes.byte4,
bytes.byte5,
bytes.byte6,
bytes.byte7,
bytes.byte8,
bytes.byte9,
bytes.byte10,
bytes.byte11,
bytes.byte12,
bytes.byte13,
bytes.byte14,
bytes.byte15,
]))
}
fn as_any(&self) -> &dyn Any {
self
}
fn bounds(&self) -> Bounds<GlobalPixels> {
unsafe {
let native_bounds = CGDisplayBounds(self.0);
display_bounds_from_native(native_bounds)
}
}
}

View file

@ -0,0 +1,274 @@
use std::{
ffi::c_void,
mem,
sync::{Arc, Weak},
};
use crate::DisplayId;
use collections::HashMap;
use parking_lot::Mutex;
pub use sys::CVSMPTETime as SmtpeTime;
pub use sys::CVTimeStamp as VideoTimestamp;
pub(crate) struct MacDisplayLinker {
links: HashMap<DisplayId, MacDisplayLink>,
}
struct MacDisplayLink {
system_link: sys::DisplayLink,
_output_callback: Arc<OutputCallback>,
}
impl MacDisplayLinker {
pub fn new() -> Self {
MacDisplayLinker {
links: Default::default(),
}
}
}
type OutputCallback = Mutex<Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>>;
impl MacDisplayLinker {
pub fn set_output_callback(
&mut self,
display_id: DisplayId,
output_callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp) + Send>,
) {
if let Some(mut system_link) = unsafe { sys::DisplayLink::on_display(display_id.0) } {
let callback = Arc::new(Mutex::new(output_callback));
let weak_callback_ptr: *const OutputCallback = Arc::downgrade(&callback).into_raw();
unsafe { system_link.set_output_callback(trampoline, weak_callback_ptr as *mut c_void) }
self.links.insert(
display_id,
MacDisplayLink {
_output_callback: callback,
system_link,
},
);
} else {
log::warn!("DisplayLink could not be obtained for {:?}", display_id);
}
}
pub fn start(&mut self, display_id: DisplayId) {
if let Some(link) = self.links.get_mut(&display_id) {
unsafe {
link.system_link.start();
}
} else {
log::warn!("No DisplayLink callback registered for {:?}", display_id)
}
}
pub fn stop(&mut self, display_id: DisplayId) {
if let Some(link) = self.links.get_mut(&display_id) {
unsafe {
link.system_link.stop();
}
} else {
log::warn!("No DisplayLink callback registered for {:?}", display_id)
}
}
}
unsafe extern "C" fn trampoline(
_display_link_out: *mut sys::CVDisplayLink,
current_time: *const sys::CVTimeStamp,
output_time: *const sys::CVTimeStamp,
_flags_in: i64,
_flags_out: *mut i64,
user_data: *mut c_void,
) -> i32 {
if let Some((current_time, output_time)) = current_time.as_ref().zip(output_time.as_ref()) {
let output_callback: Weak<OutputCallback> =
Weak::from_raw(user_data as *mut OutputCallback);
if let Some(output_callback) = output_callback.upgrade() {
(output_callback.lock())(current_time, output_time)
}
mem::forget(output_callback);
}
0
}
mod sys {
//! Derived from display-link crate under the fololwing license:
//! https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT
//! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc)
#![allow(dead_code, non_upper_case_globals)]
use foreign_types::{foreign_type, ForeignType};
use std::{
ffi::c_void,
fmt::{Debug, Formatter, Result},
};
#[derive(Debug)]
pub enum CVDisplayLink {}
foreign_type! {
type CType = CVDisplayLink;
fn drop = CVDisplayLinkRelease;
fn clone = CVDisplayLinkRetain;
pub struct DisplayLink;
pub struct DisplayLinkRef;
}
impl Debug for DisplayLink {
fn fmt(&self, formatter: &mut Formatter) -> Result {
formatter
.debug_tuple("DisplayLink")
.field(&self.as_ptr())
.finish()
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct CVTimeStamp {
pub version: u32,
pub video_time_scale: i32,
pub video_time: i64,
pub host_time: u64,
pub rate_scalar: f64,
pub video_refresh_period: i64,
pub smpte_time: CVSMPTETime,
pub flags: u64,
pub reserved: u64,
}
pub type CVTimeStampFlags = u64;
pub const kCVTimeStampVideoTimeValid: CVTimeStampFlags = 1 << 0;
pub const kCVTimeStampHostTimeValid: CVTimeStampFlags = 1 << 1;
pub const kCVTimeStampSMPTETimeValid: CVTimeStampFlags = 1 << 2;
pub const kCVTimeStampVideoRefreshPeriodValid: CVTimeStampFlags = 1 << 3;
pub const kCVTimeStampRateScalarValid: CVTimeStampFlags = 1 << 4;
pub const kCVTimeStampTopField: CVTimeStampFlags = 1 << 16;
pub const kCVTimeStampBottomField: CVTimeStampFlags = 1 << 17;
pub const kCVTimeStampVideoHostTimeValid: CVTimeStampFlags =
kCVTimeStampVideoTimeValid | kCVTimeStampHostTimeValid;
pub const kCVTimeStampIsInterlaced: CVTimeStampFlags =
kCVTimeStampTopField | kCVTimeStampBottomField;
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct CVSMPTETime {
pub subframes: i16,
pub subframe_divisor: i16,
pub counter: u32,
pub time_type: u32,
pub flags: u32,
pub hours: i16,
pub minutes: i16,
pub seconds: i16,
pub frames: i16,
}
pub type CVSMPTETimeType = u32;
pub const kCVSMPTETimeType24: CVSMPTETimeType = 0;
pub const kCVSMPTETimeType25: CVSMPTETimeType = 1;
pub const kCVSMPTETimeType30Drop: CVSMPTETimeType = 2;
pub const kCVSMPTETimeType30: CVSMPTETimeType = 3;
pub const kCVSMPTETimeType2997: CVSMPTETimeType = 4;
pub const kCVSMPTETimeType2997Drop: CVSMPTETimeType = 5;
pub const kCVSMPTETimeType60: CVSMPTETimeType = 6;
pub const kCVSMPTETimeType5994: CVSMPTETimeType = 7;
pub type CVSMPTETimeFlags = u32;
pub const kCVSMPTETimeValid: CVSMPTETimeFlags = 1 << 0;
pub const kCVSMPTETimeRunning: CVSMPTETimeFlags = 1 << 1;
pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn(
display_link_out: *mut CVDisplayLink,
// A pointer to the current timestamp. This represents the timestamp when the callback is called.
current_time: *const CVTimeStamp,
// A pointer to the output timestamp. This represents the timestamp for when the frame will be displayed.
output_time: *const CVTimeStamp,
// Unused
flags_in: i64,
// Unused
flags_out: *mut i64,
// A pointer to app-defined data.
display_link_context: *mut c_void,
) -> i32;
#[link(name = "CoreFoundation", kind = "framework")]
#[link(name = "CoreVideo", kind = "framework")]
#[allow(improper_ctypes)]
extern "C" {
pub fn CVDisplayLinkCreateWithActiveCGDisplays(
display_link_out: *mut *mut CVDisplayLink,
) -> i32;
pub fn CVDisplayLinkCreateWithCGDisplay(
display_id: u32,
display_link_out: *mut *mut CVDisplayLink,
) -> i32;
pub fn CVDisplayLinkSetOutputCallback(
display_link: &mut DisplayLinkRef,
callback: CVDisplayLinkOutputCallback,
user_info: *mut c_void,
) -> i32;
pub fn CVDisplayLinkSetCurrentCGDisplay(
display_link: &mut DisplayLinkRef,
display_id: u32,
) -> i32;
pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32;
pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32;
pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink);
pub fn CVDisplayLinkRetain(display_link: *mut CVDisplayLink) -> *mut CVDisplayLink;
}
impl DisplayLink {
/// Apple docs: [CVDisplayLinkCreateWithActiveCGDisplays](https://developer.apple.com/documentation/corevideo/1456863-cvdisplaylinkcreatewithactivecgd?language=objc)
pub unsafe fn new() -> Option<Self> {
let mut display_link: *mut CVDisplayLink = 0 as _;
let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link);
if code == 0 {
Some(DisplayLink::from_ptr(display_link))
} else {
None
}
}
/// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc)
pub unsafe fn on_display(display_id: u32) -> Option<Self> {
let mut display_link: *mut CVDisplayLink = 0 as _;
let code = CVDisplayLinkCreateWithCGDisplay(display_id, &mut display_link);
if code == 0 {
Some(DisplayLink::from_ptr(display_link))
} else {
None
}
}
}
impl DisplayLinkRef {
/// Apple docs: [CVDisplayLinkSetOutputCallback](https://developer.apple.com/documentation/corevideo/1457096-cvdisplaylinksetoutputcallback?language=objc)
pub unsafe fn set_output_callback(
&mut self,
callback: CVDisplayLinkOutputCallback,
user_info: *mut c_void,
) {
assert_eq!(CVDisplayLinkSetOutputCallback(self, callback, user_info), 0);
}
/// Apple docs: [CVDisplayLinkSetCurrentCGDisplay](https://developer.apple.com/documentation/corevideo/1456768-cvdisplaylinksetcurrentcgdisplay?language=objc)
pub unsafe fn set_current_display(&mut self, display_id: u32) {
assert_eq!(CVDisplayLinkSetCurrentCGDisplay(self, display_id), 0);
}
/// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc)
pub unsafe fn start(&mut self) {
assert_eq!(CVDisplayLinkStart(self), 0);
}
/// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc)
pub unsafe fn stop(&mut self) {
assert_eq!(CVDisplayLinkStop(self), 0);
}
}
}

View file

@ -1,11 +1,7 @@
use crate::{
geometry::vector::vec2f,
keymap_matcher::Keystroke,
platform::{
Event, KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton,
MouseButtonEvent, MouseExitedEvent, MouseMovedEvent, NavigationDirection, ScrollDelta,
ScrollWheelEvent, TouchPhase,
},
point, px, InputEvent, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent,
MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection,
Pixels, ScrollDelta, ScrollWheelEvent, TouchPhase,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
@ -71,23 +67,23 @@ pub fn key_to_native(key: &str) -> Cow<str> {
unsafe fn read_modifiers(native_event: id) -> Modifiers {
let modifiers = native_event.modifierFlags();
let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let fun = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
Modifiers {
ctrl,
control,
alt,
shift,
cmd,
fun,
command,
function,
}
}
impl Event {
pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {
impl InputEvent {
pub unsafe fn from_native(native_event: id, window_height: Option<Pixels>) -> Option<Self> {
let event_type = native_event.eventType();
// Filter out event types that aren't in the NSEventType enum.
@ -123,16 +119,15 @@ impl Event {
_ => return None,
};
window_height.map(|window_height| {
Self::MouseDown(MouseButtonEvent {
Self::MouseDown(MouseDownEvent {
button,
position: vec2f(
native_event.locationInWindow().x as f32,
position: point(
px(native_event.locationInWindow().x as f32),
// MacOS screen coordinates are relative to bottom left
window_height - native_event.locationInWindow().y as f32,
window_height - px(native_event.locationInWindow().y as f32),
),
modifiers: read_modifiers(native_event),
click_count: native_event.clickCount() as usize,
is_down: true,
})
})
}
@ -150,46 +145,44 @@ impl Event {
};
window_height.map(|window_height| {
Self::MouseUp(MouseButtonEvent {
Self::MouseUp(MouseUpEvent {
button,
position: vec2f(
native_event.locationInWindow().x as f32,
// MacOS view coordinates are relative to bottom left
window_height - native_event.locationInWindow().y as f32,
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
modifiers: read_modifiers(native_event),
click_count: native_event.clickCount() as usize,
is_down: false,
})
})
}
NSEventType::NSScrollWheel => window_height.map(|window_height| {
let phase = match native_event.phase() {
NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
Some(TouchPhase::Started)
TouchPhase::Started
}
NSEventPhase::NSEventPhaseEnded => Some(TouchPhase::Ended),
_ => Some(TouchPhase::Moved),
NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended,
_ => TouchPhase::Moved,
};
let raw_data = vec2f(
let raw_data = point(
native_event.scrollingDeltaX() as f32,
native_event.scrollingDeltaY() as f32,
);
let delta = if native_event.hasPreciseScrollingDeltas() == YES {
ScrollDelta::Pixels(raw_data)
ScrollDelta::Pixels(raw_data.map(px))
} else {
ScrollDelta::Lines(raw_data)
};
Self::ScrollWheel(ScrollWheelEvent {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
delta,
phase,
touch_phase: phase,
modifiers: read_modifiers(native_event),
})
}),
@ -207,32 +200,33 @@ impl Event {
};
window_height.map(|window_height| {
Self::MouseMoved(MouseMovedEvent {
Self::MouseMove(MouseMoveEvent {
pressed_button: Some(pressed_button),
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
modifiers: read_modifiers(native_event),
})
})
}
NSEventType::NSMouseMoved => window_height.map(|window_height| {
Self::MouseMoved(MouseMovedEvent {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
Self::MouseMove(MouseMoveEvent {
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
pressed_button: None,
modifiers: read_modifiers(native_event),
})
}),
NSEventType::NSMouseExited => window_height.map(|window_height| {
Self::MouseExited(MouseExitedEvent {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
Self::MouseExited(MouseExitEvent {
position: point(
px(native_event.locationInWindow().x as f32),
window_height - px(native_event.locationInWindow().y as f32),
),
pressed_button: None,
modifiers: read_modifiers(native_event),
})
@ -253,10 +247,10 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags();
let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
&& first_char.map_or(true, |ch| {
!(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)
@ -297,7 +291,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
// Honor ⌘ when Dvorak-QWERTY is used.
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
if cmd && chars_ignoring_modifiers_and_shift != chars_with_cmd {
if command && chars_ignoring_modifiers_and_shift != chars_with_cmd {
chars_ignoring_modifiers =
chars_for_modified_key(native_event.keyCode(), true, shift);
chars_ignoring_modifiers_and_shift = chars_with_cmd;
@ -321,11 +315,13 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
};
Keystroke {
ctrl,
alt,
shift,
cmd,
function,
modifiers: Modifiers {
control,
alt,
shift,
command,
function,
},
key,
ime_key: None,
}

View file

@ -1,671 +0,0 @@
mod open_type;
use crate::{
fonts::{Features, FontId, GlyphId, Metrics, Properties},
geometry::{
rect::{RectF, RectI},
transform2d::Transform2F,
vector::{vec2f, Vector2F},
},
platform::{self, RasterizationOptions},
text_layout::{Glyph, LineLayout, Run, RunStyle},
};
use cocoa::appkit::{CGFloat, CGPoint};
use collections::HashMap;
use core_foundation::{
array::CFIndex,
attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
base::{CFRange, TCFType},
string::CFString,
};
use core_graphics::{
base::{kCGImageAlphaPremultipliedLast, CGGlyph},
color_space::CGColorSpace,
context::CGContext,
};
use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
use font_kit::{
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>,
font_ids_by_postscript_name: HashMap<String, FontId>,
postscript_names_by_font_id: HashMap<FontId, String>,
}
impl FontSystem {
pub fn new() -> Self {
Self(RwLock::new(FontSystemState {
memory_source: MemSource::empty(),
system_source: SystemSource::new(),
fonts: Vec::new(),
font_ids_by_postscript_name: Default::default(),
postscript_names_by_font_id: Default::default(),
}))
}
}
impl Default for FontSystem {
fn default() -> Self {
Self::new()
}
}
impl platform::FontSystem for FontSystem {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()> {
self.0.write().add_fonts(fonts)
}
fn all_families(&self) -> Vec<String> {
self.0
.read()
.system_source
.all_families()
.expect("core text should never return an error")
}
fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
self.0.write().load_family(name, features)
}
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 advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Vector2F> {
self.0.read().advance(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,
options: RasterizationOptions,
) -> Option<(RectI, Vec<u8>)> {
self.0.read().rasterize_glyph(
font_id,
font_size,
glyph_id,
subpixel_shift,
scale_factor,
options,
)
}
fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
self.0.write().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, features: &Features) -> 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 mut font = font.load()?;
open_type::apply_features(&mut font, features);
let font_id = FontId(self.fonts.len());
font_ids.push(font_id);
let postscript_name = font.postscript_name().unwrap();
self.font_ids_by_postscript_name
.insert(postscript_name.clone(), font_id);
self.postscript_names_by_font_id
.insert(font_id, postscript_name);
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 advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result<Vector2F> {
Ok(self.fonts[font_id.0].advance(glyph_id)?)
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
self.fonts[font_id.0].glyph_for_char(ch)
}
fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId {
let postscript_name = requested_font.postscript_name();
if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) {
*font_id
} else {
let font_id = FontId(self.fonts.len());
self.font_ids_by_postscript_name
.insert(postscript_name.clone(), font_id);
self.postscript_names_by_font_id
.insert(font_id, postscript_name);
self.fonts
.push(font_kit::font::Font::from_core_graphics_font(
requested_font.copy_to_CGFont(),
));
font_id
}
}
fn is_emoji(&self, font_id: FontId) -> bool {
self.postscript_names_by_font_id
.get(&font_id)
.map_or(false, |postscript_name| {
postscript_name == "AppleColorEmoji"
})
}
fn rasterize_glyph(
&self,
font_id: FontId,
font_size: f32,
glyph_id: GlyphId,
subpixel_shift: Vector2F,
scale_factor: f32,
options: RasterizationOptions,
) -> Option<(RectI, Vec<u8>)> {
let font = &self.fonts[font_id.0];
let scale = Transform2F::from_scale(scale_factor);
let glyph_bounds = font
.raster_bounds(
glyph_id,
font_size,
scale,
HintingOptions::None,
font_kit::canvas::RasterizationOptions::GrayscaleAa,
)
.ok()?;
if glyph_bounds.width() == 0 || glyph_bounds.height() == 0 {
None
} else {
// Make room for subpixel variants.
let subpixel_padding = subpixel_shift.ceil().to_i32();
let cx_bounds = RectI::new(
glyph_bounds.origin(),
glyph_bounds.size() + subpixel_padding,
);
let mut bytes;
let cx;
match options {
RasterizationOptions::Alpha => {
bytes = vec![0; cx_bounds.width() as usize * cx_bounds.height() as usize];
cx = CGContext::create_bitmap_context(
Some(bytes.as_mut_ptr() as *mut _),
cx_bounds.width() as usize,
cx_bounds.height() as usize,
8,
cx_bounds.width() as usize,
&CGColorSpace::create_device_gray(),
kCGImageAlphaOnly,
);
}
RasterizationOptions::Bgra => {
bytes = vec![0; cx_bounds.width() as usize * 4 * cx_bounds.height() as usize];
cx = CGContext::create_bitmap_context(
Some(bytes.as_mut_ptr() as *mut _),
cx_bounds.width() as usize,
cx_bounds.height() as usize,
8,
cx_bounds.width() as usize * 4,
&CGColorSpace::create_device_rgb(),
kCGImageAlphaPremultipliedLast,
);
}
}
// Move the origin to bottom left and account for scaling, this
// makes drawing text consistent with the font-kit's raster_bounds.
cx.translate(
-glyph_bounds.origin_x() as CGFloat,
(glyph_bounds.origin_y() + glyph_bounds.height()) as CGFloat,
);
cx.scale(scale_factor as CGFloat, scale_factor as CGFloat);
cx.set_allows_font_subpixel_positioning(true);
cx.set_should_subpixel_position_fonts(true);
cx.set_allows_font_subpixel_quantization(false);
cx.set_should_subpixel_quantize_fonts(false);
font.native_font()
.clone_with_font_size(font_size as CGFloat)
.draw_glyphs(
&[glyph_id as CGGlyph],
&[CGPoint::new(
(subpixel_shift.x() / scale_factor) as CGFloat,
(subpixel_shift.y() / scale_factor) as CGFloat,
)],
cx,
);
if let RasterizationOptions::Bgra = options {
// Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
for pixel in bytes.chunks_exact_mut(4) {
pixel.swap(0, 2);
let a = pixel[3] as f32 / 255.;
pixel[0] = (pixel[0] as f32 / a) as u8;
pixel[1] = (pixel[1] as f32 / a) as u8;
pixel[2] = (pixel[2] as f32 / a) as u8;
}
}
Some((cx_bounds, bytes))
}
}
fn layout_line(
&mut self,
text: &str,
font_size: f32,
runs: &[(usize, RunStyle)],
) -> LineLayout {
// 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),
);
}
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 attributes = run.attributes().unwrap();
let font = unsafe {
attributes
.get(kCTFontAttributeName)
.downcast::<CTFont>()
.unwrap()
};
let font_id = self.id_for_native_font(font);
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,
is_emoji: self.is_emoji(font_id),
});
}
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, 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::AppContext;
use font_kit::properties::{Style, Weight};
use platform::FontSystem as _;
#[crate::test(self, retries = 5)]
fn test_layout_str(_: &mut AppContext) {
// 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", &Default::default()).unwrap();
let menlo_regular = RunStyle {
font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
color: Default::default(),
underline: Default::default(),
};
let menlo_italic = RunStyle {
font_id: fonts
.select_font(&menlo, Properties::new().style(Style::Italic))
.unwrap(),
color: Default::default(),
underline: Default::default(),
};
let menlo_bold = RunStyle {
font_id: fonts
.select_font(&menlo, Properties::new().weight(Weight::BOLD))
.unwrap(),
color: Default::default(),
underline: Default::default(),
};
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", &Default::default())?;
let zapfino_regular = RunStyle {
font_id: fonts.select_font(&zapfino, &Properties::new())?,
color: Default::default(),
underline: Default::default(),
};
let menlo = fonts.load_family("Menlo", &Default::default())?;
let menlo_regular = RunStyle {
font_id: fonts.select_font(&menlo, &Properties::new())?,
color: Default::default(),
underline: Default::default(),
};
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", &Default::default()).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.,
RasterizationOptions::Alpha,
)
.unwrap();
let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
let path = Path::new(&name);
let file = File::create(path).unwrap();
let w = &mut 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", &Default::default()).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", &Default::default()).unwrap();
let style = RunStyle {
font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
color: Default::default(),
underline: Default::default(),
};
let line = "\u{feff}";
let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
assert_eq!(layout.len, line.len());
assert!(layout.runs.is_empty());
let line = "a\u{feff}b";
let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
assert_eq!(layout.len, line.len());
assert_eq!(layout.runs.len(), 1);
assert_eq!(layout.runs[0].glyphs.len(), 2);
assert_eq!(layout.runs[0].glyphs[0].id, 68); // a
// There's no glyph for \u{feff}
assert_eq!(layout.runs[0].glyphs[1].id, 69); // b
}
}

View file

@ -1,45 +0,0 @@
use cocoa::{
base::id,
foundation::{NSPoint, NSRect},
};
use objc::{msg_send, sel, sel_impl};
use pathfinder_geometry::vector::{vec2f, Vector2F};
///! Macos screen have a y axis that goings up from the bottom of the screen and
///! an origin at the bottom left of the main display.
pub trait Vector2FExt {
/// Converts self to an NSPoint with y axis pointing up.
fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint;
}
impl Vector2FExt for Vector2F {
fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint {
unsafe {
let point = NSPoint::new(self.x() as f64, window_height - self.y() as f64);
msg_send![native_window, convertPointToScreen: point]
}
}
}
pub trait NSRectExt {
fn size_vec(&self) -> Vector2F;
fn intersects(&self, other: Self) -> bool;
}
impl NSRectExt for NSRect {
fn size_vec(&self) -> Vector2F {
vec2f(self.size.width as f32, self.size.height as f32)
}
fn intersects(&self, other: Self) -> bool {
self.size.width > 0.
&& self.size.height > 0.
&& other.size.width > 0.
&& other.size.height > 0.
&& self.origin.x <= other.origin.x + other.size.width
&& self.origin.x + self.size.width >= other.origin.x
&& self.origin.y <= other.origin.y + other.size.height
&& self.origin.y + self.size.height >= other.origin.y
}
}

View file

@ -1,115 +0,0 @@
use super::atlas::{AllocId, AtlasAllocator};
use crate::{
fonts::{FontId, GlyphId},
geometry::{rect::RectI, vector::Vector2I},
platform::{FontSystem, RasterizationOptions},
scene::ImageGlyph,
ImageData,
};
use anyhow::anyhow;
use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
use ordered_float::OrderedFloat;
use std::{collections::HashMap, mem, sync::Arc};
#[derive(Hash, Eq, PartialEq)]
struct GlyphDescriptor {
font_id: FontId,
font_size: OrderedFloat<f32>,
glyph_id: GlyphId,
}
pub struct ImageCache {
prev_frame: HashMap<usize, (AllocId, RectI)>,
curr_frame: HashMap<usize, (AllocId, RectI)>,
image_glyphs: HashMap<GlyphDescriptor, Option<(AllocId, RectI, Vector2I)>>,
atlases: AtlasAllocator,
scale_factor: f32,
fonts: Arc<dyn FontSystem>,
}
impl ImageCache {
pub fn new(
device: metal::Device,
size: Vector2I,
scale_factor: f32,
fonts: Arc<dyn FontSystem>,
) -> 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(),
image_glyphs: Default::default(),
atlases: AtlasAllocator::new(device, descriptor),
scale_factor,
fonts,
}
}
pub fn set_scale_factor(&mut self, scale_factor: f32) {
if scale_factor != self.scale_factor {
self.scale_factor = scale_factor;
for (_, glyph) in self.image_glyphs.drain() {
if let Some((alloc_id, _, _)) = glyph {
self.atlases.deallocate(alloc_id);
}
}
}
}
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())
.or_else(|| self.atlases.upload(image.size(), image.as_bytes()))
.ok_or_else(|| anyhow!("could not upload image of size {:?}", image.size()))
.unwrap();
self.curr_frame.insert(image.id, (alloc_id, atlas_bounds));
(alloc_id, atlas_bounds)
}
pub fn render_glyph(&mut self, image_glyph: &ImageGlyph) -> Option<(AllocId, RectI, Vector2I)> {
*self
.image_glyphs
.entry(GlyphDescriptor {
font_id: image_glyph.font_id,
font_size: OrderedFloat(image_glyph.font_size),
glyph_id: image_glyph.id,
})
.or_insert_with(|| {
let (glyph_bounds, bytes) = self.fonts.rasterize_glyph(
image_glyph.font_id,
image_glyph.font_size,
image_glyph.id,
Default::default(),
self.scale_factor,
RasterizationOptions::Bgra,
)?;
let (alloc_id, atlas_bounds) = self
.atlases
.upload(glyph_bounds.size(), &bytes)
.ok_or_else(|| {
anyhow!(
"could not upload image glyph of size {:?}",
glyph_bounds.size()
)
})
.unwrap();
Some((alloc_id, atlas_bounds, glyph_bounds.origin()))
})
}
pub fn finish_frame(&mut self) {
mem::swap(&mut self.prev_frame, &mut self.curr_frame);
for (_, (id, _)) in self.curr_frame.drain() {
self.atlases.deallocate(id);
}
}
pub fn atlas_texture(&self, atlas_id: usize) -> Option<&TextureRef> {
self.atlases.texture(atlas_id)
}
}

View file

@ -0,0 +1,256 @@
use crate::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
Point, Size,
};
use anyhow::Result;
use collections::FxHashMap;
use derive_more::{Deref, DerefMut};
use etagere::BucketedAtlasAllocator;
use metal::Device;
use parking_lot::Mutex;
use std::borrow::Cow;
pub struct MetalAtlas(Mutex<MetalAtlasState>);
impl MetalAtlas {
pub fn new(device: Device) -> Self {
MetalAtlas(Mutex::new(MetalAtlasState {
device: AssertSend(device),
monochrome_textures: Default::default(),
polychrome_textures: Default::default(),
path_textures: Default::default(),
tiles_by_key: Default::default(),
}))
}
pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture {
self.0.lock().texture(id).metal_texture.clone()
}
pub(crate) fn allocate(
&self,
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> AtlasTile {
self.0.lock().allocate(size, texture_kind)
}
pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) {
let mut lock = self.0.lock();
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
AtlasTextureKind::Path => &mut lock.path_textures,
};
for texture in textures {
texture.clear();
}
}
}
struct MetalAtlasState {
device: AssertSend<Device>,
monochrome_textures: Vec<MetalAtlasTexture>,
polychrome_textures: Vec<MetalAtlasTexture>,
path_textures: Vec<MetalAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
}
impl PlatformAtlas for MetalAtlas {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
) -> Result<AtlasTile> {
let mut lock = self.0.lock();
if let Some(tile) = lock.tiles_by_key.get(key) {
Ok(tile.clone())
} else {
let (size, bytes) = build()?;
let tile = lock.allocate(size, key.texture_kind());
let texture = lock.texture(tile.texture_id);
texture.upload(tile.bounds, &bytes);
lock.tiles_by_key.insert(key.clone(), tile.clone());
Ok(tile)
}
}
fn clear(&self) {
let mut lock = self.0.lock();
lock.tiles_by_key.clear();
for texture in &mut lock.monochrome_textures {
texture.clear();
}
for texture in &mut lock.polychrome_textures {
texture.clear();
}
for texture in &mut lock.path_textures {
texture.clear();
}
}
}
impl MetalAtlasState {
fn allocate(&mut self, size: Size<DevicePixels>, texture_kind: AtlasTextureKind) -> AtlasTile {
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
.unwrap_or_else(|| {
let texture = self.push_texture(size, texture_kind);
texture.allocate(size).unwrap()
})
}
fn push_texture(
&mut self,
min_size: Size<DevicePixels>,
kind: AtlasTextureKind,
) -> &mut MetalAtlasTexture {
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(1024),
height: DevicePixels(1024),
};
let size = min_size.max(&DEFAULT_ATLAS_SIZE);
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(size.width.into());
texture_descriptor.set_height(size.height.into());
let pixel_format;
let usage;
match kind {
AtlasTextureKind::Monochrome => {
pixel_format = metal::MTLPixelFormat::A8Unorm;
usage = metal::MTLTextureUsage::ShaderRead;
}
AtlasTextureKind::Polychrome => {
pixel_format = metal::MTLPixelFormat::BGRA8Unorm;
usage = metal::MTLTextureUsage::ShaderRead;
}
AtlasTextureKind::Path => {
pixel_format = metal::MTLPixelFormat::R16Float;
usage = metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead;
}
}
texture_descriptor.set_pixel_format(pixel_format);
texture_descriptor.set_usage(usage);
let metal_texture = self.device.new_texture(&texture_descriptor);
let textures = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
let atlas_texture = MetalAtlasTexture {
id: AtlasTextureId {
index: textures.len() as u32,
kind,
},
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
metal_texture: AssertSend(metal_texture),
};
textures.push(atlas_texture);
textures.last_mut().unwrap()
}
fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture {
let textures = match id.kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
crate::AtlasTextureKind::Path => &self.path_textures,
};
&textures[id.index as usize]
}
}
struct MetalAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
metal_texture: AssertSend<metal::Texture>,
}
impl MetalAtlasTexture {
fn clear(&mut self) {
self.allocator.clear();
}
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
let allocation = self.allocator.allocate(size.into())?;
let tile = AtlasTile {
texture_id: self.id,
tile_id: allocation.id.into(),
bounds: Bounds {
origin: allocation.rectangle.min.into(),
size,
},
};
Some(tile)
}
fn upload(&self, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
let region = metal::MTLRegion::new_2d(
bounds.origin.x.into(),
bounds.origin.y.into(),
bounds.size.width.into(),
bounds.size.height.into(),
);
self.metal_texture.replace_region(
region,
0,
bytes.as_ptr() as *const _,
bounds.size.width.to_bytes(self.bytes_per_pixel()) as u64,
);
}
fn bytes_per_pixel(&self) -> u8 {
use metal::MTLPixelFormat::*;
match self.metal_texture.pixel_format() {
A8Unorm | R8Unorm => 1,
RGBA8Unorm | BGRA8Unorm => 4,
_ => unimplemented!(),
}
}
}
impl From<Size<DevicePixels>> for etagere::Size {
fn from(size: Size<DevicePixels>) -> Self {
etagere::Size::new(size.width.into(), size.height.into())
}
}
impl From<etagere::Point> for Point<DevicePixels> {
fn from(value: etagere::Point) -> Self {
Point {
x: DevicePixels::from(value.x),
y: DevicePixels::from(value.y),
}
}
}
impl From<etagere::Size> for Size<DevicePixels> {
fn from(size: etagere::Size) -> Self {
Size {
width: DevicePixels::from(size.width),
height: DevicePixels::from(size.height),
}
}
}
impl From<etagere::Rectangle> for Bounds<DevicePixels> {
fn from(rectangle: etagere::Rectangle) -> Self {
Bounds {
origin: rectangle.min.into(),
size: rectangle.size().into(),
}
}
}
#[derive(Deref, DerefMut)]
struct AssertSend<T>(T);
unsafe impl<T> Send for AssertSend<T> {}

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,6 @@
#![allow(unused, non_upper_case_globals)]
use std::ptr;
use crate::fonts::Features;
use crate::FontFeatures;
use cocoa::appkit::CGFloat;
use core_foundation::{base::TCFType, number::CFNumber};
use core_graphics::geometry::CGAffineTransform;
@ -13,6 +11,7 @@ use core_text::{
},
};
use font_kit::font::Font;
use std::ptr;
const kCaseSensitiveLayoutOffSelector: i32 = 1;
const kCaseSensitiveLayoutOnSelector: i32 = 0;
@ -108,243 +107,243 @@ const kTypographicExtrasType: i32 = 14;
const kVerticalFractionsSelector: i32 = 1;
const kVerticalPositionType: i32 = 10;
pub fn apply_features(font: &mut Font, features: &Features) {
pub fn apply_features(font: &mut Font, features: FontFeatures) {
// See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc
// for a reference implementation.
toggle_open_type_feature(
font,
features.calt,
features.calt(),
kContextualAlternatesType,
kContextualAlternatesOnSelector,
kContextualAlternatesOffSelector,
);
toggle_open_type_feature(
font,
features.case,
features.case(),
kCaseSensitiveLayoutType,
kCaseSensitiveLayoutOnSelector,
kCaseSensitiveLayoutOffSelector,
);
toggle_open_type_feature(
font,
features.cpsp,
features.cpsp(),
kCaseSensitiveLayoutType,
kCaseSensitiveSpacingOnSelector,
kCaseSensitiveSpacingOffSelector,
);
toggle_open_type_feature(
font,
features.frac,
features.frac(),
kFractionsType,
kDiagonalFractionsSelector,
kNoFractionsSelector,
);
toggle_open_type_feature(
font,
features.liga,
features.liga(),
kLigaturesType,
kCommonLigaturesOnSelector,
kCommonLigaturesOffSelector,
);
toggle_open_type_feature(
font,
features.onum,
features.onum(),
kNumberCaseType,
kLowerCaseNumbersSelector,
2,
);
toggle_open_type_feature(
font,
features.ordn,
features.ordn(),
kVerticalPositionType,
kOrdinalsSelector,
kNormalPositionSelector,
);
toggle_open_type_feature(
font,
features.pnum,
features.pnum(),
kNumberSpacingType,
kProportionalNumbersSelector,
4,
);
toggle_open_type_feature(
font,
features.ss01,
features.ss01(),
kStylisticAlternativesType,
kStylisticAltOneOnSelector,
kStylisticAltOneOffSelector,
);
toggle_open_type_feature(
font,
features.ss02,
features.ss02(),
kStylisticAlternativesType,
kStylisticAltTwoOnSelector,
kStylisticAltTwoOffSelector,
);
toggle_open_type_feature(
font,
features.ss03,
features.ss03(),
kStylisticAlternativesType,
kStylisticAltThreeOnSelector,
kStylisticAltThreeOffSelector,
);
toggle_open_type_feature(
font,
features.ss04,
features.ss04(),
kStylisticAlternativesType,
kStylisticAltFourOnSelector,
kStylisticAltFourOffSelector,
);
toggle_open_type_feature(
font,
features.ss05,
features.ss05(),
kStylisticAlternativesType,
kStylisticAltFiveOnSelector,
kStylisticAltFiveOffSelector,
);
toggle_open_type_feature(
font,
features.ss06,
features.ss06(),
kStylisticAlternativesType,
kStylisticAltSixOnSelector,
kStylisticAltSixOffSelector,
);
toggle_open_type_feature(
font,
features.ss07,
features.ss07(),
kStylisticAlternativesType,
kStylisticAltSevenOnSelector,
kStylisticAltSevenOffSelector,
);
toggle_open_type_feature(
font,
features.ss08,
features.ss08(),
kStylisticAlternativesType,
kStylisticAltEightOnSelector,
kStylisticAltEightOffSelector,
);
toggle_open_type_feature(
font,
features.ss09,
features.ss09(),
kStylisticAlternativesType,
kStylisticAltNineOnSelector,
kStylisticAltNineOffSelector,
);
toggle_open_type_feature(
font,
features.ss10,
features.ss10(),
kStylisticAlternativesType,
kStylisticAltTenOnSelector,
kStylisticAltTenOffSelector,
);
toggle_open_type_feature(
font,
features.ss11,
features.ss11(),
kStylisticAlternativesType,
kStylisticAltElevenOnSelector,
kStylisticAltElevenOffSelector,
);
toggle_open_type_feature(
font,
features.ss12,
features.ss12(),
kStylisticAlternativesType,
kStylisticAltTwelveOnSelector,
kStylisticAltTwelveOffSelector,
);
toggle_open_type_feature(
font,
features.ss13,
features.ss13(),
kStylisticAlternativesType,
kStylisticAltThirteenOnSelector,
kStylisticAltThirteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss14,
features.ss14(),
kStylisticAlternativesType,
kStylisticAltFourteenOnSelector,
kStylisticAltFourteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss15,
features.ss15(),
kStylisticAlternativesType,
kStylisticAltFifteenOnSelector,
kStylisticAltFifteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss16,
features.ss16(),
kStylisticAlternativesType,
kStylisticAltSixteenOnSelector,
kStylisticAltSixteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss17,
features.ss17(),
kStylisticAlternativesType,
kStylisticAltSeventeenOnSelector,
kStylisticAltSeventeenOffSelector,
);
toggle_open_type_feature(
font,
features.ss18,
features.ss18(),
kStylisticAlternativesType,
kStylisticAltEighteenOnSelector,
kStylisticAltEighteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss19,
features.ss19(),
kStylisticAlternativesType,
kStylisticAltNineteenOnSelector,
kStylisticAltNineteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss20,
features.ss20(),
kStylisticAlternativesType,
kStylisticAltTwentyOnSelector,
kStylisticAltTwentyOffSelector,
);
toggle_open_type_feature(
font,
features.subs,
features.subs(),
kVerticalPositionType,
kInferiorsSelector,
kNormalPositionSelector,
);
toggle_open_type_feature(
font,
features.sups,
features.sups(),
kVerticalPositionType,
kSuperiorsSelector,
kNormalPositionSelector,
);
toggle_open_type_feature(
font,
features.swsh,
features.swsh(),
kContextualAlternatesType,
kSwashAlternatesOnSelector,
kSwashAlternatesOffSelector,
);
toggle_open_type_feature(
font,
features.titl,
features.titl(),
kStyleOptionsType,
kTitlingCapsSelector,
kNoStyleOptionsSelector,
);
toggle_open_type_feature(
font,
features.tnum,
features.tnum(),
kNumberSpacingType,
kMonospacedNumbersSelector,
4,
);
toggle_open_type_feature(
font,
features.zero,
features.zero(),
kTypographicExtrasType,
kSlashedZeroOnSelector,
kSlashedZeroOffSelector,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,144 +0,0 @@
use super::ns_string;
use crate::platform;
use cocoa::{
appkit::NSScreen,
base::{id, nil},
foundation::{NSArray, NSDictionary, NSPoint, NSRect, NSSize},
};
use core_foundation::{
number::{kCFNumberIntType, CFNumberGetValue, CFNumberRef},
uuid::{CFUUIDGetUUIDBytes, CFUUIDRef},
};
use core_graphics::display::CGDirectDisplayID;
use pathfinder_geometry::{rect::RectF, vector::vec2f};
use std::{any::Any, ffi::c_void};
use uuid::Uuid;
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
}
#[derive(Debug)]
pub struct Screen {
pub(crate) native_screen: id,
}
impl Screen {
/// Get the screen with the given UUID.
pub fn find_by_id(uuid: Uuid) -> Option<Self> {
Self::all().find(|screen| platform::Screen::display_uuid(screen) == Some(uuid))
}
/// Get the primary screen - the one with the menu bar, and whose bottom left
/// corner is at the origin of the AppKit coordinate system.
fn primary() -> Self {
Self::all().next().unwrap()
}
pub fn all() -> impl Iterator<Item = Self> {
unsafe {
let native_screens = NSScreen::screens(nil);
(0..NSArray::count(native_screens)).map(move |ix| Screen {
native_screen: native_screens.objectAtIndex(ix),
})
}
}
/// Convert the given rectangle in screen coordinates from GPUI's
/// coordinate system to the AppKit coordinate system.
///
/// In GPUI's coordinates, the origin is at the top left of the primary screen, with
/// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the
/// bottom left of the primary screen, with the Y axis pointing upward.
pub(crate) fn screen_rect_to_native(rect: RectF) -> NSRect {
let primary_screen_height = unsafe { Self::primary().native_screen.frame().size.height };
NSRect::new(
NSPoint::new(
rect.origin_x() as f64,
primary_screen_height - rect.origin_y() as f64 - rect.height() as f64,
),
NSSize::new(rect.width() as f64, rect.height() as f64),
)
}
/// Convert the given rectangle in screen coordinates from the AppKit
/// coordinate system to GPUI's coordinate system.
///
/// In GPUI's coordinates, the origin is at the top left of the primary screen, with
/// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the
/// bottom left of the primary screen, with the Y axis pointing upward.
pub(crate) fn screen_rect_from_native(rect: NSRect) -> RectF {
let primary_screen_height = unsafe { Self::primary().native_screen.frame().size.height };
RectF::new(
vec2f(
rect.origin.x as f32,
(primary_screen_height - rect.origin.y - rect.size.height) as f32,
),
vec2f(rect.size.width as f32, rect.size.height as f32),
)
}
}
impl platform::Screen for Screen {
fn as_any(&self) -> &dyn Any {
self
}
fn display_uuid(&self) -> Option<uuid::Uuid> {
unsafe {
// Screen ids are not stable. Further, the default device id is also unstable across restarts.
// CGDisplayCreateUUIDFromDisplayID is stable but not exposed in the bindings we use.
// This approach is similar to that which winit takes
// https://github.com/rust-windowing/winit/blob/402cbd55f932e95dbfb4e8b5e8551c49e56ff9ac/src/platform_impl/macos/monitor.rs#L99
let device_description = self.native_screen.deviceDescription();
let key = ns_string("NSScreenNumber");
let device_id_obj = device_description.objectForKey_(key);
if device_id_obj.is_null() {
// Under some circumstances, especially display re-arrangements or display locking, we seem to get a null pointer
// to the device id. See: https://linear.app/zed-industries/issue/Z-257/lock-screen-crash-with-multiple-monitors
return None;
}
let mut device_id: u32 = 0;
CFNumberGetValue(
device_id_obj as CFNumberRef,
kCFNumberIntType,
(&mut device_id) as *mut _ as *mut c_void,
);
let cfuuid = CGDisplayCreateUUIDFromDisplayID(device_id as CGDirectDisplayID);
if cfuuid.is_null() {
return None;
}
let bytes = CFUUIDGetUUIDBytes(cfuuid);
Some(Uuid::from_bytes([
bytes.byte0,
bytes.byte1,
bytes.byte2,
bytes.byte3,
bytes.byte4,
bytes.byte5,
bytes.byte6,
bytes.byte7,
bytes.byte8,
bytes.byte9,
bytes.byte10,
bytes.byte11,
bytes.byte12,
bytes.byte13,
bytes.byte14,
bytes.byte15,
]))
}
}
fn bounds(&self) -> RectF {
unsafe { Self::screen_rect_from_native(self.native_screen.frame()) }
}
fn content_bounds(&self) -> RectF {
unsafe { Self::screen_rect_from_native(self.native_screen.visibleFrame()) }
}
}

View file

@ -0,0 +1,655 @@
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
float4 hsla_to_rgba(Hsla hsla);
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
constant Size_DevicePixels *viewport_size);
float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
constant Size_DevicePixels *atlas_size);
float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds,
Bounds_ScaledPixels clip_bounds);
float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
Corners_ScaledPixels corner_radii);
float gaussian(float x, float sigma);
float2 erf(float2 x);
float blur_along_x(float x, float y, float sigma, float corner,
float2 half_size);
struct QuadVertexOutput {
float4 position [[position]];
float4 background_color [[flat]];
float4 border_color [[flat]];
uint quad_id [[flat]];
float clip_distance [[clip_distance]][4];
};
struct QuadFragmentInput {
float4 position [[position]];
float4 background_color [[flat]];
float4 border_color [[flat]];
uint quad_id [[flat]];
};
vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
uint quad_id [[instance_id]],
constant float2 *unit_vertices
[[buffer(QuadInputIndex_Vertices)]],
constant Quad *quads
[[buffer(QuadInputIndex_Quads)]],
constant Size_DevicePixels *viewport_size
[[buffer(QuadInputIndex_ViewportSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
Quad quad = quads[quad_id];
float4 device_position =
to_device_position(unit_vertex, quad.bounds, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds,
quad.content_mask.bounds);
float4 background_color = hsla_to_rgba(quad.background);
float4 border_color = hsla_to_rgba(quad.border_color);
return QuadVertexOutput{
device_position,
background_color,
border_color,
quad_id,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
}
fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
constant Quad *quads
[[buffer(QuadInputIndex_Quads)]]) {
Quad quad = quads[input.quad_id];
float2 half_size =
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
float2 center =
float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
float2 center_to_point = input.position.xy - center;
float corner_radius;
if (center_to_point.x < 0.) {
if (center_to_point.y < 0.) {
corner_radius = quad.corner_radii.top_left;
} else {
corner_radius = quad.corner_radii.bottom_left;
}
} else {
if (center_to_point.y < 0.) {
corner_radius = quad.corner_radii.top_right;
} else {
corner_radius = quad.corner_radii.bottom_right;
}
}
float2 rounded_edge_to_point =
fabs(center_to_point) - half_size + corner_radius;
float distance =
length(max(0., rounded_edge_to_point)) +
min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
corner_radius;
float vertical_border = center_to_point.x <= 0. ? quad.border_widths.left
: quad.border_widths.right;
float horizontal_border = center_to_point.y <= 0. ? quad.border_widths.top
: quad.border_widths.bottom;
float2 inset_size =
half_size - corner_radius - float2(vertical_border, horizontal_border);
float2 point_to_inset_corner = fabs(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 {
float inset_distance = distance + border_width;
// Decrease border's opacity as we move inside the background.
input.border_color.a *= 1. - saturate(0.5 - inset_distance);
// Alpha-blend the border and the background.
float output_alpha = input.border_color.a +
input.background_color.a * (1. - input.border_color.a);
float3 premultiplied_border_rgb =
input.border_color.rgb * input.border_color.a;
float3 premultiplied_background_rgb =
input.background_color.rgb * input.background_color.a;
float3 premultiplied_output_rgb =
premultiplied_border_rgb +
premultiplied_background_rgb * (1. - input.border_color.a);
color = float4(premultiplied_output_rgb, output_alpha);
}
return color * float4(1., 1., 1., saturate(0.5 - distance));
}
struct ShadowVertexOutput {
float4 position [[position]];
float4 color [[flat]];
uint shadow_id [[flat]];
float clip_distance [[clip_distance]][4];
};
struct ShadowFragmentInput {
float4 position [[position]];
float4 color [[flat]];
uint shadow_id [[flat]];
};
vertex ShadowVertexOutput shadow_vertex(
uint unit_vertex_id [[vertex_id]], uint shadow_id [[instance_id]],
constant float2 *unit_vertices [[buffer(ShadowInputIndex_Vertices)]],
constant Shadow *shadows [[buffer(ShadowInputIndex_Shadows)]],
constant Size_DevicePixels *viewport_size
[[buffer(ShadowInputIndex_ViewportSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
Shadow shadow = shadows[shadow_id];
float margin = 3. * shadow.blur_radius;
// Set the bounds of the shadow and adjust its size based on the shadow's
// spread radius to achieve the spreading effect
Bounds_ScaledPixels bounds = shadow.bounds;
bounds.origin.x -= margin;
bounds.origin.y -= margin;
bounds.size.width += 2. * margin;
bounds.size.height += 2. * margin;
float4 device_position =
to_device_position(unit_vertex, bounds, viewport_size);
float4 clip_distance =
distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask.bounds);
float4 color = hsla_to_rgba(shadow.color);
return ShadowVertexOutput{
device_position,
color,
shadow_id,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
}
fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]],
constant Shadow *shadows
[[buffer(ShadowInputIndex_Shadows)]]) {
Shadow shadow = shadows[input.shadow_id];
float2 origin = float2(shadow.bounds.origin.x, shadow.bounds.origin.y);
float2 size = float2(shadow.bounds.size.width, shadow.bounds.size.height);
float2 half_size = size / 2.;
float2 center = origin + half_size;
float2 point = input.position.xy - center;
float corner_radius;
if (point.x < 0.) {
if (point.y < 0.) {
corner_radius = shadow.corner_radii.top_left;
} else {
corner_radius = shadow.corner_radii.bottom_left;
}
} else {
if (point.y < 0.) {
corner_radius = shadow.corner_radii.top_right;
} else {
corner_radius = shadow.corner_radii.bottom_right;
}
}
// 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. * shadow.blur_radius, low, high);
float end = clamp(3. * shadow.blur_radius, 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, shadow.blur_radius,
corner_radius, half_size) *
gaussian(y, shadow.blur_radius) * step;
y += step;
}
return input.color * float4(1., 1., 1., alpha);
}
struct UnderlineVertexOutput {
float4 position [[position]];
float4 color [[flat]];
uint underline_id [[flat]];
float clip_distance [[clip_distance]][4];
};
struct UnderlineFragmentInput {
float4 position [[position]];
float4 color [[flat]];
uint underline_id [[flat]];
};
vertex UnderlineVertexOutput underline_vertex(
uint unit_vertex_id [[vertex_id]], uint underline_id [[instance_id]],
constant float2 *unit_vertices [[buffer(UnderlineInputIndex_Vertices)]],
constant Underline *underlines [[buffer(UnderlineInputIndex_Underlines)]],
constant Size_DevicePixels *viewport_size
[[buffer(ShadowInputIndex_ViewportSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
Underline underline = underlines[underline_id];
float4 device_position =
to_device_position(unit_vertex, underline.bounds, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds,
underline.content_mask.bounds);
float4 color = hsla_to_rgba(underline.color);
return UnderlineVertexOutput{
device_position,
color,
underline_id,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
}
fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]],
constant Underline *underlines
[[buffer(UnderlineInputIndex_Underlines)]]) {
Underline underline = underlines[input.underline_id];
if (underline.wavy) {
float half_thickness = underline.thickness * 0.5;
float2 origin =
float2(underline.bounds.origin.x, underline.bounds.origin.y);
float2 st = ((input.position.xy - origin) / underline.bounds.size.height) -
float2(0., 0.5);
float frequency = (M_PI_F * (3. * underline.thickness)) / 8.;
float amplitude = 1. / (2. * underline.thickness);
float sine = sin(st.x * frequency) * amplitude;
float dSine = cos(st.x * frequency) * amplitude * frequency;
float distance = (st.y - sine) / sqrt(1. + dSine * dSine);
float distance_in_pixels = distance * underline.bounds.size.height;
float distance_from_top_border = distance_in_pixels - half_thickness;
float distance_from_bottom_border = distance_in_pixels + half_thickness;
float alpha = saturate(
0.5 - max(-distance_from_bottom_border, distance_from_top_border));
return input.color * float4(1., 1., 1., alpha);
} else {
return input.color;
}
}
struct MonochromeSpriteVertexOutput {
float4 position [[position]];
float2 tile_position;
float4 color [[flat]];
float clip_distance [[clip_distance]][4];
};
struct MonochromeSpriteFragmentInput {
float4 position [[position]];
float2 tile_position;
float4 color [[flat]];
};
vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex(
uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]],
constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]],
constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
constant Size_DevicePixels *viewport_size
[[buffer(SpriteInputIndex_ViewportSize)]],
constant Size_DevicePixels *atlas_size
[[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
MonochromeSprite sprite = sprites[sprite_id];
float4 device_position =
to_device_position(unit_vertex, sprite.bounds, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds,
sprite.content_mask.bounds);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
float4 color = hsla_to_rgba(sprite.color);
return MonochromeSpriteVertexOutput{
device_position,
tile_position,
color,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
}
fragment float4 monochrome_sprite_fragment(
MonochromeSpriteFragmentInput input [[stage_in]],
constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
constexpr sampler atlas_texture_sampler(mag_filter::linear,
min_filter::linear);
float4 sample =
atlas_texture.sample(atlas_texture_sampler, input.tile_position);
float4 color = input.color;
color.a *= sample.a;
return color;
}
struct PolychromeSpriteVertexOutput {
float4 position [[position]];
float2 tile_position;
uint sprite_id [[flat]];
float clip_distance [[clip_distance]][4];
};
struct PolychromeSpriteFragmentInput {
float4 position [[position]];
float2 tile_position;
uint sprite_id [[flat]];
};
vertex PolychromeSpriteVertexOutput polychrome_sprite_vertex(
uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]],
constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]],
constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
constant Size_DevicePixels *viewport_size
[[buffer(SpriteInputIndex_ViewportSize)]],
constant Size_DevicePixels *atlas_size
[[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
PolychromeSprite sprite = sprites[sprite_id];
float4 device_position =
to_device_position(unit_vertex, sprite.bounds, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds,
sprite.content_mask.bounds);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
return PolychromeSpriteVertexOutput{
device_position,
tile_position,
sprite_id,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
}
fragment float4 polychrome_sprite_fragment(
PolychromeSpriteFragmentInput input [[stage_in]],
constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
PolychromeSprite sprite = sprites[input.sprite_id];
constexpr sampler atlas_texture_sampler(mag_filter::linear,
min_filter::linear);
float4 sample =
atlas_texture.sample(atlas_texture_sampler, input.tile_position);
float distance =
quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii);
float4 color = sample;
if (sprite.grayscale) {
float grayscale = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
color.r = grayscale;
color.g = grayscale;
color.b = grayscale;
}
color.a *= saturate(0.5 - distance);
return color;
}
struct PathRasterizationVertexOutput {
float4 position [[position]];
float2 st_position;
float clip_rect_distance [[clip_distance]][4];
};
struct PathRasterizationFragmentInput {
float4 position [[position]];
float2 st_position;
};
vertex PathRasterizationVertexOutput path_rasterization_vertex(
uint vertex_id [[vertex_id]],
constant PathVertex_ScaledPixels *vertices
[[buffer(PathRasterizationInputIndex_Vertices)]],
constant Size_DevicePixels *atlas_size
[[buffer(PathRasterizationInputIndex_AtlasTextureSize)]]) {
PathVertex_ScaledPixels v = vertices[vertex_id];
float2 vertex_position = float2(v.xy_position.x, v.xy_position.y);
float2 viewport_size = float2(atlas_size->width, atlas_size->height);
return PathRasterizationVertexOutput{
float4(vertex_position / viewport_size * float2(2., -2.) +
float2(-1., 1.),
0., 1.),
float2(v.st_position.x, v.st_position.y),
{v.xy_position.x - v.content_mask.bounds.origin.x,
v.content_mask.bounds.origin.x + v.content_mask.bounds.size.width -
v.xy_position.x,
v.xy_position.y - v.content_mask.bounds.origin.y,
v.content_mask.bounds.origin.y + v.content_mask.bounds.size.height -
v.xy_position.y}};
}
fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput 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);
return float4(alpha, 0., 0., 1.);
}
struct PathSpriteVertexOutput {
float4 position [[position]];
float2 tile_position;
float4 color [[flat]];
};
vertex PathSpriteVertexOutput path_sprite_vertex(
uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]],
constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]],
constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
constant Size_DevicePixels *viewport_size
[[buffer(SpriteInputIndex_ViewportSize)]],
constant Size_DevicePixels *atlas_size
[[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
PathSprite sprite = sprites[sprite_id];
// Don't apply content mask because it was already accounted for when
// rasterizing the path.
float4 device_position =
to_device_position(unit_vertex, sprite.bounds, viewport_size);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
float4 color = hsla_to_rgba(sprite.color);
return PathSpriteVertexOutput{device_position, tile_position, color};
}
fragment float4 path_sprite_fragment(
PathSpriteVertexOutput input [[stage_in]],
constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
constexpr sampler atlas_texture_sampler(mag_filter::linear,
min_filter::linear);
float4 sample =
atlas_texture.sample(atlas_texture_sampler, input.tile_position);
float mask = 1. - abs(1. - fmod(sample.r, 2.));
float4 color = input.color;
color.a *= mask;
return color;
}
struct SurfaceVertexOutput {
float4 position [[position]];
float2 texture_position;
float clip_distance [[clip_distance]][4];
};
struct SurfaceFragmentInput {
float4 position [[position]];
float2 texture_position;
};
vertex SurfaceVertexOutput surface_vertex(
uint unit_vertex_id [[vertex_id]], uint surface_id [[instance_id]],
constant float2 *unit_vertices [[buffer(SurfaceInputIndex_Vertices)]],
constant SurfaceBounds *surfaces [[buffer(SurfaceInputIndex_Surfaces)]],
constant Size_DevicePixels *viewport_size
[[buffer(SurfaceInputIndex_ViewportSize)]],
constant Size_DevicePixels *texture_size
[[buffer(SurfaceInputIndex_TextureSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
SurfaceBounds surface = surfaces[surface_id];
float4 device_position =
to_device_position(unit_vertex, surface.bounds, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, surface.bounds,
surface.content_mask.bounds);
// We are going to copy the whole texture, so the texture position corresponds
// to the current vertex of the unit triangle.
float2 texture_position = unit_vertex;
return SurfaceVertexOutput{
device_position,
texture_position,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
}
fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]],
texture2d<float> y_texture
[[texture(SurfaceInputIndex_YTexture)]],
texture2d<float> cb_cr_texture
[[texture(SurfaceInputIndex_CbCrTexture)]]) {
constexpr sampler texture_sampler(mag_filter::linear, min_filter::linear);
const float4x4 ycbcrToRGBTransform =
float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f));
float4 ycbcr = float4(
y_texture.sample(texture_sampler, input.texture_position).r,
cb_cr_texture.sample(texture_sampler, input.texture_position).rg, 1.0);
return ycbcrToRGBTransform * ycbcr;
}
float4 hsla_to_rgba(Hsla hsla) {
float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
float s = hsla.s;
float l = hsla.l;
float a = hsla.a;
float c = (1.0 - fabs(2.0 * l - 1.0)) * s;
float x = c * (1.0 - fabs(fmod(h, 2.0) - 1.0));
float m = l - c / 2.0;
float r = 0.0;
float g = 0.0;
float b = 0.0;
if (h >= 0.0 && h < 1.0) {
r = c;
g = x;
b = 0.0;
} else if (h >= 1.0 && h < 2.0) {
r = x;
g = c;
b = 0.0;
} else if (h >= 2.0 && h < 3.0) {
r = 0.0;
g = c;
b = x;
} else if (h >= 3.0 && h < 4.0) {
r = 0.0;
g = x;
b = c;
} else if (h >= 4.0 && h < 5.0) {
r = x;
g = 0.0;
b = c;
} else {
r = c;
g = 0.0;
b = x;
}
float4 rgba;
rgba.x = (r + m);
rgba.y = (g + m);
rgba.z = (b + m);
rgba.w = a;
return rgba;
}
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
constant Size_DevicePixels *input_viewport_size) {
float2 position =
unit_vertex * float2(bounds.size.width, bounds.size.height) +
float2(bounds.origin.x, bounds.origin.y);
float2 viewport_size = float2((float)input_viewport_size->width,
(float)input_viewport_size->height);
float2 device_position =
position / viewport_size * float2(2., -2.) + float2(-1., 1.);
return float4(device_position, 0., 1.);
}
float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
constant Size_DevicePixels *atlas_size) {
float2 tile_origin = float2(tile.bounds.origin.x, tile.bounds.origin.y);
float2 tile_size = float2(tile.bounds.size.width, tile.bounds.size.height);
return (tile_origin + unit_vertex * tile_size) /
float2((float)atlas_size->width, (float)atlas_size->height);
}
float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
Corners_ScaledPixels corner_radii) {
float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
float2 center_to_point = point - center;
float corner_radius;
if (center_to_point.x < 0.) {
if (center_to_point.y < 0.) {
corner_radius = corner_radii.top_left;
} else {
corner_radius = corner_radii.bottom_left;
}
} else {
if (center_to_point.y < 0.) {
corner_radius = corner_radii.top_right;
} else {
corner_radius = corner_radii.bottom_right;
}
}
float2 rounded_edge_to_point =
abs(center_to_point) - half_size + corner_radius;
float distance =
length(max(0., rounded_edge_to_point)) +
min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
corner_radius;
return distance;
}
// 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 half_size) {
float delta = min(half_size.y - corner - abs(y), 0.);
float curved =
half_size.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;
}
float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds,
Bounds_ScaledPixels clip_bounds) {
float2 position =
unit_vertex * float2(bounds.size.width, bounds.size.height) +
float2(bounds.origin.x, bounds.origin.y);
return float4(position.x - clip_bounds.origin.x,
clip_bounds.origin.x + clip_bounds.size.width - position.x,
position.y - clip_bounds.origin.y,
clip_bounds.origin.y + clip_bounds.size.height - position.y);
}

View file

@ -1,135 +0,0 @@
#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_top_left;
float corner_radius_top_right;
float corner_radius_bottom_right;
float corner_radius_bottom_left;
} GPUIQuad;
typedef enum {
GPUIShadowInputIndexVertices = 0,
GPUIShadowInputIndexShadows = 1,
GPUIShadowInputIndexUniforms = 2,
} GPUIShadowInputIndex;
typedef struct {
vector_float2 origin;
vector_float2 size;
float corner_radius_top_left;
float corner_radius_top_right;
float corner_radius_bottom_right;
float corner_radius_bottom_left;
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_top_left;
float corner_radius_top_right;
float corner_radius_bottom_right;
float corner_radius_bottom_left;
uint8_t grayscale;
} GPUIImage;
typedef enum {
GPUISurfaceVertexInputIndexVertices = 0,
GPUISurfaceVertexInputIndexSurfaces = 1,
GPUISurfaceVertexInputIndexViewportSize = 2,
GPUISurfaceVertexInputIndexAtlasSize = 3,
} GPUISurfaceVertexInputIndex;
typedef enum {
GPUISurfaceFragmentInputIndexYAtlas = 0,
GPUISurfaceFragmentInputIndexCbCrAtlas = 1,
} GPUISurfaceFragmentInputIndex;
typedef struct {
vector_float2 origin;
vector_float2 target_size;
vector_float2 source_size;
} GPUISurface;
typedef enum {
GPUIUnderlineInputIndexVertices = 0,
GPUIUnderlineInputIndexUnderlines = 1,
GPUIUnderlineInputIndexUniforms = 2,
} GPUIUnderlineInputIndex;
typedef struct {
vector_float2 origin;
vector_float2 size;
float thickness;
vector_uchar4 color;
uint8_t squiggly;
} GPUIUnderline;

View file

@ -1,464 +0,0 @@
#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_top_left;
float corner_radius_top_right;
float corner_radius_bottom_right;
float corner_radius_bottom_left;
uchar grayscale; // only used in image shader
};
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;
float corner_radius;
if (center_to_point.x < 0.) {
if (center_to_point.y < 0.) {
corner_radius = input.corner_radius_top_left;
} else {
corner_radius = input.corner_radius_bottom_left;
}
} else {
if (center_to_point.y < 0.) {
corner_radius = input.corner_radius_top_right;
} else {
corner_radius = input.corner_radius_bottom_right;
}
}
float2 rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
float distance = length(max(0., rounded_edge_to_point)) + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - 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 - 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 {
float inset_distance = distance + border_width;
// Decrease border's opacity as we move inside the background.
input.border_color.a *= 1. - saturate(0.5 - inset_distance);
// Alpha-blend the border and the background.
float output_alpha = input.border_color.a + input.background_color.a * (1. - input.border_color.a);
float3 premultiplied_border_rgb = input.border_color.rgb * input.border_color.a;
float3 premultiplied_background_rgb = input.background_color.rgb * input.background_color.a;
float3 premultiplied_output_rgb = premultiplied_border_rgb + premultiplied_background_rgb * (1. - input.border_color.a);
color = float4(premultiplied_output_rgb / output_alpha, output_alpha);
}
return color * float4(1., 1., 1., saturate(0.5 - distance));
}
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_top_left,
quad.corner_radius_top_right,
quad.corner_radius_bottom_right,
quad.corner_radius_bottom_left,
0,
};
}
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_top_left;
float corner_radius_top_right;
float corner_radius_bottom_right;
float corner_radius_bottom_left;
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_top_left,
shadow.corner_radius_top_right,
shadow.corner_radius_bottom_right,
shadow.corner_radius_bottom_left,
shadow.sigma,
shadow.color,
};
}
fragment float4 shadow_fragment(
ShadowFragmentInput input [[stage_in]]
) {
float sigma = input.sigma;
float2 half_size = input.size / 2.;
float2 center = input.origin + half_size;
float2 point = input.position.xy - center;
float2 center_to_point = input.position.xy - center;
float corner_radius;
if (center_to_point.x < 0.) {
if (center_to_point.y < 0.) {
corner_radius = input.corner_radius_top_left;
} else {
corner_radius = input.corner_radius_bottom_left;
}
} else {
if (center_to_point.y < 0.) {
corner_radius = input.corner_radius_top_right;
} else {
corner_radius = input.corner_radius_bottom_right;
}
}
// 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
};
}
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, 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_top_left,
image.corner_radius_top_right,
image.corner_radius_bottom_right,
image.corner_radius_bottom_left,
image.grayscale,
};
}
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);
if (input.grayscale) {
float grayscale =
0.2126 * input.background_color.r +
0.7152 * input.background_color.g +
0.0722 * input.background_color.b;
input.background_color = float4(grayscale, grayscale, grayscale, input.background_color.a);
}
return quad_sdf(input);
}
vertex QuadFragmentInput surface_vertex(
uint unit_vertex_id [[vertex_id]],
uint image_id [[instance_id]],
constant float2 *unit_vertices [[buffer(GPUISurfaceVertexInputIndexVertices)]],
constant GPUISurface *images [[buffer(GPUISurfaceVertexInputIndexSurfaces)]],
constant float2 *viewport_size [[buffer(GPUISurfaceVertexInputIndexViewportSize)]],
constant float2 *atlas_size [[buffer(GPUISurfaceVertexInputIndexAtlasSize)]]
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUISurface 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) / *atlas_size;
return QuadFragmentInput {
device_position,
atlas_position,
image.origin,
image.target_size,
float4(0.),
0.,
0.,
0.,
0.,
float4(0.),
0.,
0,
};
}
fragment float4 surface_fragment(
QuadFragmentInput input [[stage_in]],
texture2d<float> y_atlas [[ texture(GPUISurfaceFragmentInputIndexYAtlas) ]],
texture2d<float> cb_cr_atlas [[ texture(GPUISurfaceFragmentInputIndexCbCrAtlas) ]]
) {
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
const float4x4 ycbcrToRGBTransform = float4x4(
float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
);
float4 ycbcr = float4(y_atlas.sample(atlas_sampler, input.atlas_position).r,
cb_cr_atlas.sample(atlas_sampler, input.atlas_position).rg, 1.0);
input.background_color = ycbcrToRGBTransform * ycbcr;
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);
return float4(alpha, 0., 0., 1.);
}
struct UnderlineFragmentInput {
float4 position [[position]];
float2 origin;
float2 size;
float thickness;
float4 color;
bool squiggly;
};
vertex UnderlineFragmentInput underline_vertex(
uint unit_vertex_id [[vertex_id]],
uint underline_id [[instance_id]],
constant float2 *unit_vertices [[buffer(GPUIUnderlineInputIndexVertices)]],
constant GPUIUnderline *underlines [[buffer(GPUIUnderlineInputIndexUnderlines)]],
constant GPUIUniforms *uniforms [[buffer(GPUIUnderlineInputIndexUniforms)]]
) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
GPUIUnderline underline = underlines[underline_id];
float2 position = unit_vertex * underline.size + underline.origin;
float4 device_position = to_device_position(position, uniforms->viewport_size);
return UnderlineFragmentInput {
device_position,
underline.origin,
underline.size,
underline.thickness,
coloru_to_colorf(underline.color),
underline.squiggly != 0,
};
}
fragment float4 underline_fragment(
UnderlineFragmentInput input [[stage_in]]
) {
if (input.squiggly) {
float half_thickness = input.thickness * 0.5;
float2 st = ((input.position.xy - input.origin) / input.size.y) - float2(0., 0.5);
float frequency = (M_PI_F * (3. * input.thickness)) / 8.;
float amplitude = 1. / (2. * input.thickness);
float sine = sin(st.x * frequency) * amplitude;
float dSine = cos(st.x * frequency) * amplitude * frequency;
float distance = (st.y - sine) / sqrt(1. + dSine * dSine);
float distance_in_pixels = distance * input.size.y;
float distance_from_top_border = distance_in_pixels - half_thickness;
float distance_from_bottom_border = distance_in_pixels + half_thickness;
float alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
return input.color * float4(1., 1., 1., alpha);
} else {
return input.color;
}
}

View file

@ -1,164 +0,0 @@
use super::atlas::AtlasAllocator;
use crate::{
fonts::{FontId, GlyphId},
geometry::vector::{vec2f, Vector2F, Vector2I},
platform::{self, RasterizationOptions},
};
use collections::hash_map::Entry;
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>,
scale_factor: f32,
}
impl SpriteCache {
pub fn new(
device: metal::Device,
size: Vector2I,
scale_factor: f32,
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(),
scale_factor,
}
}
pub fn set_scale_factor(&mut self, scale_factor: f32) {
if scale_factor != self.scale_factor {
self.icons.clear();
self.glyphs.clear();
self.atlases.clear();
}
self.scale_factor = scale_factor;
}
pub fn render_glyph(
&mut self,
font_id: FontId,
font_size: f32,
glyph_id: GlyphId,
target_position: Vector2F,
) -> Option<GlyphSprite> {
const SUBPIXEL_VARIANTS: u8 = 4;
let target_position = target_position * self.scale_factor;
let subpixel_variant = (
(target_position.x().fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
(target_position.y().fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
);
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) = self.fonts.rasterize_glyph(
font_id,
font_size,
glyph_id,
subpixel_shift,
self.scale_factor,
RasterizationOptions::Alpha,
)?;
let (alloc_id, atlas_bounds) = self
.atlases
.upload(glyph_bounds.size(), &mask)
.expect("could not upload glyph");
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,
) -> Option<IconSprite> {
let atlases = &mut self.atlases;
match self.icons.entry(IconDescriptor {
path,
width: size.x(),
height: size.y(),
}) {
Entry::Occupied(entry) => Some(entry.get().clone()),
Entry::Vacant(entry) => {
let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32)?;
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)?;
let icon_sprite = IconSprite {
atlas_id: alloc_id.atlas_id,
atlas_origin: atlas_bounds.origin(),
size,
};
Some(entry.insert(icon_sprite).clone())
}
}
}
pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
self.atlases.texture(atlas_id)
}
}

View file

@ -0,0 +1,760 @@
use crate::{
point, px, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun,
FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point,
RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS,
};
use anyhow::anyhow;
use cocoa::appkit::{CGFloat, CGPoint};
use collections::HashMap;
use core_foundation::{
array::CFIndex,
attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
base::{CFRange, TCFType},
string::CFString,
};
use core_graphics::{
base::{kCGImageAlphaPremultipliedLast, CGGlyph},
color_space::CGColorSpace,
context::CGContext,
};
use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
use font_kit::{
font::Font as FontKitFont,
handle::Handle,
hinting::HintingOptions,
metrics::Metrics,
properties::{Style as FontkitStyle, Weight as FontkitWeight},
source::SystemSource,
sources::mem::MemSource,
};
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use pathfinder_geometry::{
rect::{RectF, RectI},
transform2d::Transform2F,
vector::{Vector2F, Vector2I},
};
use smallvec::SmallVec;
use std::{char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
use super::open_type;
#[allow(non_upper_case_globals)]
const kCGImageAlphaOnly: u32 = 7;
pub struct MacTextSystem(RwLock<MacTextSystemState>);
struct MacTextSystemState {
memory_source: MemSource,
system_source: SystemSource,
fonts: Vec<FontKitFont>,
font_selections: HashMap<Font, FontId>,
font_ids_by_postscript_name: HashMap<String, FontId>,
font_ids_by_family_name: HashMap<SharedString, SmallVec<[FontId; 4]>>,
postscript_names_by_font_id: HashMap<FontId, String>,
}
impl MacTextSystem {
pub fn new() -> Self {
Self(RwLock::new(MacTextSystemState {
memory_source: MemSource::empty(),
system_source: SystemSource::new(),
fonts: Vec::new(),
font_selections: HashMap::default(),
font_ids_by_postscript_name: HashMap::default(),
font_ids_by_family_name: HashMap::default(),
postscript_names_by_font_id: HashMap::default(),
}))
}
}
impl Default for MacTextSystem {
fn default() -> Self {
Self::new()
}
}
impl PlatformTextSystem for MacTextSystem {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
self.0.write().add_fonts(fonts)
}
fn all_font_families(&self) -> Vec<String> {
self.0
.read()
.system_source
.all_families()
.expect("core text should never return an error")
}
fn font_id(&self, font: &Font) -> Result<FontId> {
let lock = self.0.upgradable_read();
if let Some(font_id) = lock.font_selections.get(font) {
Ok(*font_id)
} else {
let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
let candidates = if let Some(font_ids) = lock.font_ids_by_family_name.get(&font.family)
{
font_ids.as_slice()
} else {
let font_ids = lock.load_family(&font.family, font.features)?;
lock.font_ids_by_family_name
.insert(font.family.clone(), font_ids);
lock.font_ids_by_family_name[&font.family].as_ref()
};
let candidate_properties = candidates
.iter()
.map(|font_id| lock.fonts[font_id.0].properties())
.collect::<SmallVec<[_; 4]>>();
let ix = font_kit::matching::find_best_match(
&candidate_properties,
&font_kit::properties::Properties {
style: font.style.into(),
weight: font.weight.into(),
stretch: Default::default(),
},
)?;
let font_id = candidates[ix];
lock.font_selections.insert(font.clone(), font_id);
Ok(font_id)
}
}
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
self.0.read().fonts[font_id.0].metrics().into()
}
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
Ok(self.0.read().fonts[font_id.0]
.typographic_bounds(glyph_id.into())?
.into())
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
self.0.read().advance(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 glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
self.0.read().raster_bounds(params)
}
fn rasterize_glyph(
&self,
glyph_id: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
self.0.read().rasterize_glyph(glyph_id, raster_bounds)
}
fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
self.0.write().layout_line(text, font_size, font_runs)
}
fn wrap_line(
&self,
text: &str,
font_id: FontId,
font_size: Pixels,
width: Pixels,
) -> Vec<usize> {
self.0.read().wrap_line(text, font_id, font_size, width)
}
}
impl MacTextSystemState {
fn add_fonts(&mut self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
self.memory_source.add_fonts(
fonts
.iter()
.map(|bytes| Handle::from_memory(bytes.clone(), 0)),
)?;
Ok(())
}
fn load_family(
&mut self,
name: &SharedString,
features: FontFeatures,
) -> Result<SmallVec<[FontId; 4]>> {
let mut font_ids = SmallVec::new();
let family = self
.memory_source
.select_family_by_name(name.as_ref())
.or_else(|_| self.system_source.select_family_by_name(name.as_ref()))?;
for font in family.fonts() {
let mut font = font.load()?;
open_type::apply_features(&mut font, features);
let font_id = FontId(self.fonts.len());
font_ids.push(font_id);
let postscript_name = font.postscript_name().unwrap();
self.font_ids_by_postscript_name
.insert(postscript_name.clone(), font_id);
self.postscript_names_by_font_id
.insert(font_id, postscript_name);
self.fonts.push(font);
}
Ok(font_ids)
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
Ok(self.fonts[font_id.0].advance(glyph_id.into())?.into())
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
self.fonts[font_id.0].glyph_for_char(ch).map(Into::into)
}
fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId {
let postscript_name = requested_font.postscript_name();
if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) {
*font_id
} else {
let font_id = FontId(self.fonts.len());
self.font_ids_by_postscript_name
.insert(postscript_name.clone(), font_id);
self.postscript_names_by_font_id
.insert(font_id, postscript_name);
self.fonts
.push(font_kit::font::Font::from_core_graphics_font(
requested_font.copy_to_CGFont(),
));
font_id
}
}
fn is_emoji(&self, font_id: FontId) -> bool {
self.postscript_names_by_font_id
.get(&font_id)
.map_or(false, |postscript_name| {
postscript_name == "AppleColorEmoji"
})
}
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let font = &self.fonts[params.font_id.0];
let scale = Transform2F::from_scale(params.scale_factor);
Ok(font
.raster_bounds(
params.glyph_id.into(),
params.font_size.into(),
scale,
HintingOptions::None,
font_kit::canvas::RasterizationOptions::GrayscaleAa,
)?
.into())
}
fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
glyph_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
Err(anyhow!("glyph bounds are empty"))
} else {
// Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing.
let mut bitmap_size = glyph_bounds.size;
if params.subpixel_variant.x > 0 {
bitmap_size.width += DevicePixels(1);
}
if params.subpixel_variant.y > 0 {
bitmap_size.height += DevicePixels(1);
}
let bitmap_size = bitmap_size;
let mut bytes;
let cx;
if params.is_emoji {
bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize];
cx = CGContext::create_bitmap_context(
Some(bytes.as_mut_ptr() as *mut _),
bitmap_size.width.0 as usize,
bitmap_size.height.0 as usize,
8,
bitmap_size.width.0 as usize * 4,
&CGColorSpace::create_device_rgb(),
kCGImageAlphaPremultipliedLast,
);
} else {
bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize];
cx = CGContext::create_bitmap_context(
Some(bytes.as_mut_ptr() as *mut _),
bitmap_size.width.0 as usize,
bitmap_size.height.0 as usize,
8,
bitmap_size.width.0 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(
-glyph_bounds.origin.x.0 as CGFloat,
(glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat,
);
cx.scale(
params.scale_factor as CGFloat,
params.scale_factor as CGFloat,
);
let subpixel_shift = params
.subpixel_variant
.map(|v| v as f32 / SUBPIXEL_VARIANTS as f32);
cx.set_allows_font_subpixel_positioning(true);
cx.set_should_subpixel_position_fonts(true);
cx.set_allows_font_subpixel_quantization(false);
cx.set_should_subpixel_quantize_fonts(false);
self.fonts[params.font_id.0]
.native_font()
.clone_with_font_size(f32::from(params.font_size) as CGFloat)
.draw_glyphs(
&[u32::from(params.glyph_id) as CGGlyph],
&[CGPoint::new(
(subpixel_shift.x / params.scale_factor) as CGFloat,
(subpixel_shift.y / params.scale_factor) as CGFloat,
)],
cx,
);
if params.is_emoji {
// Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
for pixel in bytes.chunks_exact_mut(4) {
pixel.swap(0, 2);
let a = pixel[3] as f32 / 255.;
pixel[0] = (pixel[0] as f32 / a) as u8;
pixel[1] = (pixel[1] as f32 / a) as u8;
pixel[2] = (pixel[2] as f32 / a) as u8;
}
}
Ok((bitmap_size, bytes))
}
}
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
// 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 mut ix_converter = StringIndexConverter::new(text);
for run 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: &FontKitFont = &self.fonts[run.font_id.0];
unsafe {
string.set_attribute(
cf_range,
kCTFontAttributeName,
&font.native_font().clone_with_font_size(font_size.into()),
);
}
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 attributes = run.attributes().unwrap();
let font = unsafe {
attributes
.get(kCTFontAttributeName)
.downcast::<CTFont>()
.unwrap()
};
let font_id = self.id_for_native_font(font);
let mut ix_converter = StringIndexConverter::new(text);
let mut glyphs = SmallVec::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(ShapedGlyph {
id: (*glyph_id).into(),
position: point(position.x as f32, position.y as f32).map(px),
index: ix_converter.utf8_ix,
is_emoji: self.is_emoji(font_id),
});
}
runs.push(ShapedRun { font_id, glyphs })
}
let typographic_bounds = line.get_typographic_bounds();
LineLayout {
runs,
font_size,
width: typographic_bounds.width.into(),
ascent: typographic_bounds.ascent.into(),
descent: typographic_bounds.descent.into(),
len: text.len(),
}
}
fn wrap_line(
&self,
text: &str,
font_id: FontId,
font_size: Pixels,
width: Pixels,
) -> Vec<usize> {
let mut string = CFMutableAttributedString::new();
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
let cf_range = CFRange::init(0, 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.into()),
);
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.into(),
) 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);
}
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;
}
impl From<Metrics> for FontMetrics {
fn from(metrics: Metrics) -> Self {
FontMetrics {
units_per_em: metrics.units_per_em,
ascent: metrics.ascent,
descent: metrics.descent,
line_gap: metrics.line_gap,
underline_position: metrics.underline_position,
underline_thickness: metrics.underline_thickness,
cap_height: metrics.cap_height,
x_height: metrics.x_height,
bounding_box: metrics.bounding_box.into(),
}
}
}
impl From<RectF> for Bounds<f32> {
fn from(rect: RectF) -> Self {
Bounds {
origin: point(rect.origin_x(), rect.origin_y()),
size: size(rect.width(), rect.height()),
}
}
}
impl From<RectI> for Bounds<DevicePixels> {
fn from(rect: RectI) -> Self {
Bounds {
origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
}
}
}
impl From<Vector2I> for Size<DevicePixels> {
fn from(value: Vector2I) -> Self {
size(value.x().into(), value.y().into())
}
}
impl From<RectI> for Bounds<i32> {
fn from(rect: RectI) -> Self {
Bounds {
origin: point(rect.origin_x(), rect.origin_y()),
size: size(rect.width(), rect.height()),
}
}
}
impl From<Point<u32>> for Vector2I {
fn from(size: Point<u32>) -> Self {
Vector2I::new(size.x as i32, size.y as i32)
}
}
impl From<Vector2F> for Size<f32> {
fn from(vec: Vector2F) -> Self {
size(vec.x(), vec.y())
}
}
impl From<FontWeight> for FontkitWeight {
fn from(value: FontWeight) -> Self {
FontkitWeight(value.0)
}
}
impl From<FontStyle> for FontkitStyle {
fn from(style: FontStyle) -> Self {
match style {
FontStyle::Normal => FontkitStyle::Normal,
FontStyle::Italic => FontkitStyle::Italic,
FontStyle::Oblique => FontkitStyle::Oblique,
}
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// use crate::AppContext;
// use font_kit::properties::{Style, Weight};
// use platform::FontSystem as _;
// #[crate::test(self, retries = 5)]
// fn test_layout_str(_: &mut AppContext) {
// // 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", &Default::default()).unwrap();
// let menlo_regular = RunStyle {
// font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
// color: Default::default(),
// underline: Default::default(),
// };
// let menlo_italic = RunStyle {
// font_id: fonts
// .select_font(&menlo, Properties::new().style(Style::Italic))
// .unwrap(),
// color: Default::default(),
// underline: Default::default(),
// };
// let menlo_bold = RunStyle {
// font_id: fonts
// .select_font(&menlo, Properties::new().weight(Weight::BOLD))
// .unwrap(),
// color: Default::default(),
// underline: Default::default(),
// };
// 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() -> crate::Result<()> {
// let fonts = FontSystem::new();
// let zapfino = fonts.load_family("Zapfino", &Default::default())?;
// let zapfino_regular = RunStyle {
// font_id: fonts.select_font(&zapfino, &Properties::new())?,
// color: Default::default(),
// underline: Default::default(),
// };
// let menlo = fonts.load_family("Menlo", &Default::default())?;
// let menlo_regular = RunStyle {
// font_id: fonts.select_font(&menlo, &Properties::new())?,
// color: Default::default(),
// underline: Default::default(),
// };
// 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", &Default::default()).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.,
// RasterizationOptions::Alpha,
// )
// .unwrap();
// let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
// let path = Path::new(&name);
// let file = File::create(path).unwrap();
// let w = &mut 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", &Default::default()).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", &Default::default()).unwrap();
// let style = RunStyle {
// font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
// color: Default::default(),
// underline: Default::default(),
// };
// 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
// }
// }

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,13 @@
use std::ffi::CStr;
use crate::platform::Appearance;
use crate::WindowAppearance;
use cocoa::{
appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight},
base::id,
foundation::NSString,
};
use objc::{msg_send, sel, sel_impl};
use std::ffi::CStr;
impl Appearance {
impl WindowAppearance {
pub unsafe fn from_native(appearance: id) -> Self {
let name: id = msg_send![appearance, name];
if name == NSAppearanceNameVibrantLight {

View file

@ -1,434 +1,9 @@
use super::{AppVersion, CursorStyle, WindowBounds};
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
keymap_matcher::KeymapMatcher,
Action, AnyWindowHandle, ClipboardItem, Menu,
};
use anyhow::{anyhow, Result};
use collections::VecDeque;
use parking_lot::Mutex;
use postage::oneshot;
use std::{
any::Any,
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use time::UtcOffset;
struct Dispatcher;
impl super::Dispatcher for Dispatcher {
fn is_main_thread(&self) -> bool {
true
}
fn run_on_main_thread(&self, task: async_task::Runnable) {
task.run();
}
}
pub fn foreground_platform() -> ForegroundPlatform {
ForegroundPlatform::default()
}
#[derive(Default)]
pub struct ForegroundPlatform {
last_prompt_for_new_path_args: RefCell<Option<(PathBuf, oneshot::Sender<Option<PathBuf>>)>>,
}
#[cfg(any(test, feature = "test-support"))]
impl ForegroundPlatform {
pub(crate) fn simulate_new_path_selection(
&self,
result: impl FnOnce(PathBuf) -> Option<PathBuf>,
) {
let (dir_path, mut done_tx) = self
.last_prompt_for_new_path_args
.take()
.expect("prompt_for_new_path was not called");
let _ = postage::sink::Sink::try_send(&mut done_tx, 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_quit(&self, _: Box<dyn FnMut()>) {}
fn on_reopen(&self, _: Box<dyn FnMut()>) {}
fn on_event(&self, _: Box<dyn FnMut(crate::platform::Event) -> bool>) {}
fn on_open_urls(&self, _: Box<dyn FnMut(Vec<String>)>) {}
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
unimplemented!()
}
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
fn set_menus(&self, _: Vec<Menu>, _: &KeymapMatcher) {}
fn prompt_for_paths(
&self,
_: super::PathPromptOptions,
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
let (_done_tx, done_rx) = oneshot::channel();
done_rx
}
fn prompt_for_new_path(&self, path: &Path) -> oneshot::Receiver<Option<PathBuf>> {
let (done_tx, done_rx) = oneshot::channel();
*self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), done_tx));
done_rx
}
fn reveal_path(&self, _: &Path) {}
}
pub fn platform() -> Platform {
Platform::new()
}
pub struct Platform {
dispatcher: Arc<dyn super::Dispatcher>,
fonts: Arc<dyn super::FontSystem>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
cursor: Mutex<CursorStyle>,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
active_screen: Screen,
}
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),
active_window: Default::default(),
active_screen: Screen::new(),
}
}
}
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 hide(&self) {}
fn hide_other_apps(&self) {}
fn unhide_other_apps(&self) {}
fn quit(&self) {}
fn screen_by_id(&self, uuid: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> {
if self.active_screen.uuid == uuid {
Some(Rc::new(self.active_screen.clone()))
} else {
None
}
}
fn screens(&self) -> Vec<Rc<dyn crate::platform::Screen>> {
vec![Rc::new(self.active_screen.clone())]
}
fn open_window(
&self,
handle: AnyWindowHandle,
options: super::WindowOptions,
_executor: Rc<super::executor::Foreground>,
) -> Box<dyn super::Window> {
*self.active_window.lock() = Some(handle);
Box::new(Window::new(
handle,
match options.bounds {
WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.),
WindowBounds::Fixed(rect) => rect.size(),
},
self.active_window.clone(),
Rc::new(self.active_screen.clone()),
))
}
fn main_window(&self) -> Option<AnyWindowHandle> {
self.active_window.lock().clone()
}
fn add_status_item(&self, handle: AnyWindowHandle) -> Box<dyn crate::platform::Window> {
Box::new(Window::new(
handle,
vec2f(24., 24.),
self.active_window.clone(),
Rc::new(self.active_screen.clone()),
))
}
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 should_auto_hide_scrollbars(&self) -> bool {
false
}
fn local_timezone(&self) -> UtcOffset {
UtcOffset::UTC
}
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
Err(anyhow!("app not running inside a bundle"))
}
fn app_path(&self) -> Result<PathBuf> {
Err(anyhow!("app not running inside a bundle"))
}
fn app_version(&self) -> Result<AppVersion> {
Ok(AppVersion {
major: 1,
minor: 0,
patch: 0,
})
}
fn os_name(&self) -> &'static str {
"test"
}
fn os_version(&self) -> Result<AppVersion> {
Ok(AppVersion {
major: 1,
minor: 0,
patch: 0,
})
}
fn restart(&self) {}
}
#[derive(Debug, Clone)]
pub struct Screen {
uuid: uuid::Uuid,
}
impl Screen {
fn new() -> Self {
Self {
uuid: uuid::Uuid::new_v4(),
}
}
}
impl super::Screen for Screen {
fn as_any(&self) -> &dyn Any {
self
}
fn bounds(&self) -> RectF {
RectF::new(Vector2F::zero(), Vector2F::new(1920., 1080.))
}
fn content_bounds(&self) -> RectF {
RectF::new(Vector2F::zero(), Vector2F::new(1920., 1080.))
}
fn display_uuid(&self) -> Option<uuid::Uuid> {
Some(self.uuid)
}
}
pub struct Window {
handle: AnyWindowHandle,
pub(crate) size: Vector2F,
scale_factor: f32,
current_scene: Option<crate::Scene>,
event_handlers: Vec<Box<dyn FnMut(super::Event) -> bool>>,
pub(crate) resize_handlers: Vec<Box<dyn FnMut()>>,
pub(crate) moved_handlers: Vec<Box<dyn FnMut()>>,
close_handlers: Vec<Box<dyn FnOnce()>>,
fullscreen_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) active_status_change_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
pub(crate) title: Option<String>,
pub(crate) edited: bool,
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
screen: Rc<Screen>,
}
impl Window {
pub fn new(
handle: AnyWindowHandle,
size: Vector2F,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
screen: Rc<Screen>,
) -> Self {
Self {
handle,
size,
event_handlers: Default::default(),
resize_handlers: Default::default(),
moved_handlers: Default::default(),
close_handlers: Default::default(),
should_close_handler: Default::default(),
active_status_change_handlers: Default::default(),
fullscreen_handlers: Default::default(),
scale_factor: 1.0,
current_scene: None,
title: None,
edited: false,
pending_prompts: Default::default(),
active_window,
screen,
}
}
pub fn title(&self) -> Option<String> {
self.title.clone()
}
}
impl super::Window for Window {
fn bounds(&self) -> WindowBounds {
WindowBounds::Fixed(RectF::new(Vector2F::zero(), self.size))
}
fn content_size(&self) -> Vector2F {
self.size
}
fn scale_factor(&self) -> f32 {
self.scale_factor
}
fn titlebar_height(&self) -> f32 {
24.
}
fn appearance(&self) -> crate::platform::Appearance {
crate::platform::Appearance::Light
}
fn screen(&self) -> Rc<dyn crate::platform::Screen> {
self.screen.clone()
}
fn mouse_position(&self) -> Vector2F {
Vector2F::zero()
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn set_input_handler(&mut self, _: Box<dyn crate::platform::InputHandler>) {}
fn prompt(
&self,
_: crate::platform::PromptLevel,
_: &str,
_: &[&str],
) -> oneshot::Receiver<usize> {
let (done_tx, done_rx) = oneshot::channel();
self.pending_prompts.borrow_mut().push_back(done_tx);
done_rx
}
fn activate(&self) {
*self.active_window.lock() = Some(self.handle);
}
fn set_title(&mut self, title: &str) {
self.title = Some(title.to_string())
}
fn set_edited(&mut self, edited: bool) {
self.edited = edited;
}
fn show_character_palette(&self) {}
fn minimize(&self) {}
fn zoom(&self) {}
fn present_scene(&mut self, scene: crate::Scene) {
self.current_scene = Some(scene);
}
fn toggle_full_screen(&self) {}
fn on_event(&mut self, callback: Box<dyn FnMut(crate::platform::Event) -> bool>) {
self.event_handlers.push(callback);
}
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
self.active_status_change_handlers.push(callback);
}
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.resize_handlers.push(callback);
}
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
self.fullscreen_handlers.push(callback)
}
fn on_moved(&mut self, callback: Box<dyn FnMut()>) {
self.moved_handlers.push(callback);
}
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
self.should_close_handler = Some(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.close_handlers.push(callback);
}
fn on_appearance_changed(&mut self, _: Box<dyn FnMut()>) {}
fn is_topmost_for_position(&self, _position: Vector2F) -> bool {
true
}
}
mod dispatcher;
mod display;
mod platform;
mod window;
pub use dispatcher::*;
pub use display::*;
pub use platform::*;
pub use window::*;

View file

@ -0,0 +1,283 @@
use crate::{PlatformDispatcher, TaskLabel};
use async_task::Runnable;
use backtrace::Backtrace;
use collections::{HashMap, HashSet, VecDeque};
use parking::{Parker, Unparker};
use parking_lot::Mutex;
use rand::prelude::*;
use std::{
future::Future,
ops::RangeInclusive,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
use util::post_inc;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
struct TestDispatcherId(usize);
pub struct TestDispatcher {
id: TestDispatcherId,
state: Arc<Mutex<TestDispatcherState>>,
parker: Arc<Mutex<Parker>>,
unparker: Unparker,
}
struct TestDispatcherState {
random: StdRng,
foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
background: Vec<Runnable>,
deprioritized_background: Vec<Runnable>,
delayed: Vec<(Duration, Runnable)>,
time: Duration,
is_main_thread: bool,
next_id: TestDispatcherId,
allow_parking: bool,
waiting_backtrace: Option<Backtrace>,
deprioritized_task_labels: HashSet<TaskLabel>,
block_on_ticks: RangeInclusive<usize>,
}
impl TestDispatcher {
pub fn new(random: StdRng) -> Self {
let (parker, unparker) = parking::pair();
let state = TestDispatcherState {
random,
foreground: HashMap::default(),
background: Vec::new(),
deprioritized_background: Vec::new(),
delayed: Vec::new(),
time: Duration::ZERO,
is_main_thread: true,
next_id: TestDispatcherId(1),
allow_parking: false,
waiting_backtrace: None,
deprioritized_task_labels: Default::default(),
block_on_ticks: 0..=1000,
};
TestDispatcher {
id: TestDispatcherId(0),
state: Arc::new(Mutex::new(state)),
parker: Arc::new(Mutex::new(parker)),
unparker,
}
}
pub fn advance_clock(&self, by: Duration) {
let new_now = self.state.lock().time + by;
loop {
self.run_until_parked();
let state = self.state.lock();
let next_due_time = state.delayed.first().map(|(time, _)| *time);
drop(state);
if let Some(due_time) = next_due_time {
if due_time <= new_now {
self.state.lock().time = due_time;
continue;
}
}
break;
}
self.state.lock().time = new_now;
}
pub fn simulate_random_delay(&self) -> impl 'static + Send + Future<Output = ()> {
struct YieldNow {
pub(crate) count: usize,
}
impl Future for YieldNow {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
if self.count > 0 {
self.count -= 1;
cx.waker().wake_by_ref();
Poll::Pending
} else {
Poll::Ready(())
}
}
}
YieldNow {
count: self.state.lock().random.gen_range(0..10),
}
}
pub fn deprioritize(&self, task_label: TaskLabel) {
self.state
.lock()
.deprioritized_task_labels
.insert(task_label);
}
pub fn run_until_parked(&self) {
while self.tick(false) {}
}
pub fn parking_allowed(&self) -> bool {
self.state.lock().allow_parking
}
pub fn allow_parking(&self) {
self.state.lock().allow_parking = true
}
pub fn start_waiting(&self) {
self.state.lock().waiting_backtrace = Some(Backtrace::new_unresolved());
}
pub fn finish_waiting(&self) {
self.state.lock().waiting_backtrace.take();
}
pub fn waiting_backtrace(&self) -> Option<Backtrace> {
self.state.lock().waiting_backtrace.take().map(|mut b| {
b.resolve();
b
})
}
pub fn rng(&self) -> StdRng {
self.state.lock().random.clone()
}
pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
self.state.lock().block_on_ticks = range;
}
pub fn gen_block_on_ticks(&self) -> usize {
let mut lock = self.state.lock();
let block_on_ticks = lock.block_on_ticks.clone();
lock.random.gen_range(block_on_ticks)
}
}
impl Clone for TestDispatcher {
fn clone(&self) -> Self {
let id = post_inc(&mut self.state.lock().next_id.0);
Self {
id: TestDispatcherId(id),
state: self.state.clone(),
parker: self.parker.clone(),
unparker: self.unparker.clone(),
}
}
}
impl PlatformDispatcher for TestDispatcher {
fn is_main_thread(&self) -> bool {
self.state.lock().is_main_thread
}
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
{
let mut state = self.state.lock();
if label.map_or(false, |label| {
state.deprioritized_task_labels.contains(&label)
}) {
state.deprioritized_background.push(runnable);
} else {
state.background.push(runnable);
}
}
self.unparker.unpark();
}
fn dispatch_on_main_thread(&self, runnable: Runnable) {
self.state
.lock()
.foreground
.entry(self.id)
.or_default()
.push_back(runnable);
self.unparker.unpark();
}
fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) {
let mut state = self.state.lock();
let next_time = state.time + duration;
let ix = match state.delayed.binary_search_by_key(&next_time, |e| e.0) {
Ok(ix) | Err(ix) => ix,
};
state.delayed.insert(ix, (next_time, runnable));
}
fn tick(&self, background_only: bool) -> bool {
let mut state = self.state.lock();
while let Some((deadline, _)) = state.delayed.first() {
if *deadline > state.time {
break;
}
let (_, runnable) = state.delayed.remove(0);
state.background.push(runnable);
}
let foreground_len: usize = if background_only {
0
} else {
state
.foreground
.values()
.map(|runnables| runnables.len())
.sum()
};
let background_len = state.background.len();
let runnable;
let main_thread;
if foreground_len == 0 && background_len == 0 {
let deprioritized_background_len = state.deprioritized_background.len();
if deprioritized_background_len == 0 {
return false;
}
let ix = state.random.gen_range(0..deprioritized_background_len);
main_thread = false;
runnable = state.deprioritized_background.swap_remove(ix);
} else {
main_thread = state.random.gen_ratio(
foreground_len as u32,
(foreground_len + background_len) as u32,
);
if main_thread {
let state = &mut *state;
runnable = state
.foreground
.values_mut()
.filter(|runnables| !runnables.is_empty())
.choose(&mut state.random)
.unwrap()
.pop_front()
.unwrap();
} else {
let ix = state.random.gen_range(0..background_len);
runnable = state.background.swap_remove(ix);
};
};
let was_main_thread = state.is_main_thread;
state.is_main_thread = main_thread;
drop(state);
runnable.run();
self.state.lock().is_main_thread = was_main_thread;
true
}
fn park(&self) {
self.parker.lock().park();
}
fn unparker(&self) -> Unparker {
self.unparker.clone()
}
fn as_test(&self) -> Option<&TestDispatcher> {
Some(self)
}
}

View file

@ -0,0 +1,41 @@
use anyhow::{Ok, Result};
use crate::{Bounds, DisplayId, GlobalPixels, PlatformDisplay, Point};
#[derive(Debug)]
pub struct TestDisplay {
id: DisplayId,
uuid: uuid::Uuid,
bounds: Bounds<GlobalPixels>,
}
impl TestDisplay {
pub fn new() -> Self {
TestDisplay {
id: DisplayId(1),
uuid: uuid::Uuid::new_v4(),
bounds: Bounds::from_corners(
Point::default(),
Point::new(GlobalPixels(1920.), GlobalPixels(1080.)),
),
}
}
}
impl PlatformDisplay for TestDisplay {
fn id(&self) -> crate::DisplayId {
self.id
}
fn uuid(&self) -> Result<uuid::Uuid> {
Ok(self.uuid)
}
fn as_any(&self) -> &dyn std::any::Any {
todo!()
}
fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {
self.bounds
}
}

View file

@ -0,0 +1,282 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
Keymap, Platform, PlatformDisplay, PlatformTextSystem, Scene, TestDisplay, TestWindow,
WindowOptions,
};
use anyhow::{anyhow, Result};
use collections::VecDeque;
use futures::channel::oneshot;
use parking_lot::Mutex;
use std::{
cell::RefCell,
path::PathBuf,
rc::{Rc, Weak},
sync::Arc,
time::Duration,
};
pub struct TestPlatform {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
active_display: Rc<dyn PlatformDisplay>,
active_cursor: Mutex<CursorStyle>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
pub(crate) prompts: RefCell<TestPrompts>,
weak: Weak<Self>,
}
#[derive(Default)]
pub(crate) struct TestPrompts {
multiple_choice: VecDeque<oneshot::Sender<usize>>,
new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
}
impl TestPlatform {
pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
Rc::new_cyclic(|weak| TestPlatform {
background_executor: executor,
foreground_executor,
prompts: Default::default(),
active_cursor: Default::default(),
active_display: Rc::new(TestDisplay::new()),
active_window: Default::default(),
current_clipboard_item: Mutex::new(None),
weak: weak.clone(),
})
}
pub(crate) fn simulate_new_path_selection(
&self,
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
) {
let (path, tx) = self
.prompts
.borrow_mut()
.new_path
.pop_front()
.expect("no pending new path prompt");
tx.send(select_path(&path)).ok();
}
pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
let tx = self
.prompts
.borrow_mut()
.multiple_choice
.pop_front()
.expect("no pending multiple choice prompt");
tx.send(response_ix).ok();
}
pub(crate) fn has_pending_prompt(&self) -> bool {
!self.prompts.borrow().multiple_choice.is_empty()
}
pub(crate) fn prompt(&self) -> oneshot::Receiver<usize> {
let (tx, rx) = oneshot::channel();
self.prompts.borrow_mut().multiple_choice.push_back(tx);
rx
}
}
// todo!("implement out what our tests needed in GPUI 1")
impl Platform for TestPlatform {
fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
}
fn foreground_executor(&self) -> ForegroundExecutor {
self.foreground_executor.clone()
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
Arc::new(crate::platform::mac::MacTextSystem::new())
}
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
unimplemented!()
}
fn quit(&self) {}
fn restart(&self) {
unimplemented!()
}
fn activate(&self, _ignoring_other_apps: bool) {
unimplemented!()
}
fn hide(&self) {
unimplemented!()
}
fn hide_other_apps(&self) {
unimplemented!()
}
fn unhide_other_apps(&self) {
unimplemented!()
}
fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
vec![self.active_display.clone()]
}
fn display(&self, id: DisplayId) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
self.displays().iter().find(|d| d.id() == id).cloned()
}
fn active_window(&self) -> Option<crate::AnyWindowHandle> {
self.active_window.lock().clone()
}
fn open_window(
&self,
handle: AnyWindowHandle,
options: WindowOptions,
_draw: Box<dyn FnMut() -> Result<Scene>>,
) -> Box<dyn crate::PlatformWindow> {
*self.active_window.lock() = Some(handle);
Box::new(TestWindow::new(
options,
self.weak.clone(),
self.active_display.clone(),
))
}
fn set_display_link_output_callback(
&self,
_display_id: DisplayId,
mut callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
) {
let timestamp = crate::VideoTimestamp {
version: 0,
video_time_scale: 0,
video_time: 0,
host_time: 0,
rate_scalar: 0.0,
video_refresh_period: 0,
smpte_time: crate::SmtpeTime::default(),
flags: 0,
reserved: 0,
};
callback(&timestamp, &timestamp)
}
fn start_display_link(&self, _display_id: DisplayId) {}
fn stop_display_link(&self, _display_id: DisplayId) {}
fn open_url(&self, _url: &str) {
unimplemented!()
}
fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
unimplemented!()
}
fn prompt_for_paths(
&self,
_options: crate::PathPromptOptions,
) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
unimplemented!()
}
fn prompt_for_new_path(
&self,
directory: &std::path::Path,
) -> oneshot::Receiver<Option<std::path::PathBuf>> {
let (tx, rx) = oneshot::channel();
self.prompts
.borrow_mut()
.new_path
.push_back((directory.to_path_buf(), tx));
rx
}
fn reveal_path(&self, _path: &std::path::Path) {
unimplemented!()
}
fn on_become_active(&self, _callback: Box<dyn FnMut()>) {}
fn on_resign_active(&self, _callback: Box<dyn FnMut()>) {}
fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
unimplemented!()
}
fn on_event(&self, _callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
unimplemented!()
}
fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
fn os_name(&self) -> &'static str {
"test"
}
fn os_version(&self) -> Result<crate::SemanticVersion> {
Err(anyhow!("os_version called on TestPlatform"))
}
fn app_version(&self) -> Result<crate::SemanticVersion> {
Err(anyhow!("app_version called on TestPlatform"))
}
fn app_path(&self) -> Result<std::path::PathBuf> {
unimplemented!()
}
fn local_timezone(&self) -> time::UtcOffset {
unimplemented!()
}
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
unimplemented!()
}
fn set_cursor_style(&self, style: crate::CursorStyle) {
*self.active_cursor.lock() = style;
}
fn should_auto_hide_scrollbars(&self) -> bool {
// todo()
true
}
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 write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Result<()> {
Ok(())
}
fn read_credentials(&self, _url: &str) -> Result<Option<(String, Vec<u8>)>> {
Ok(None)
}
fn delete_credentials(&self, _url: &str) -> Result<()> {
Ok(())
}
fn double_click_interval(&self) -> std::time::Duration {
Duration::from_millis(500)
}
}

View file

@ -0,0 +1,248 @@
use crate::{
px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance,
WindowBounds, WindowOptions,
};
use collections::HashMap;
use parking_lot::Mutex;
use std::{
rc::{Rc, Weak},
sync::{self, Arc},
};
#[derive(Default)]
pub(crate) struct TestWindowHandlers {
pub(crate) active_status_change: Vec<Box<dyn FnMut(bool)>>,
pub(crate) input: Vec<Box<dyn FnMut(crate::InputEvent) -> bool>>,
pub(crate) moved: Vec<Box<dyn FnMut()>>,
pub(crate) resize: Vec<Box<dyn FnMut(Size<Pixels>, f32)>>,
}
pub struct TestWindow {
pub(crate) bounds: WindowBounds,
display: Rc<dyn PlatformDisplay>,
pub(crate) title: Option<String>,
pub(crate) edited: bool,
pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
pub(crate) handlers: Arc<Mutex<TestWindowHandlers>>,
platform: Weak<TestPlatform>,
sprite_atlas: Arc<dyn PlatformAtlas>,
}
impl TestWindow {
pub fn new(
options: WindowOptions,
platform: Weak<TestPlatform>,
display: Rc<dyn PlatformDisplay>,
) -> Self {
Self {
bounds: options.bounds,
display,
platform,
input_handler: None,
sprite_atlas: Arc::new(TestAtlas::new()),
handlers: Default::default(),
title: Default::default(),
edited: false,
}
}
}
impl PlatformWindow for TestWindow {
fn bounds(&self) -> WindowBounds {
self.bounds
}
fn content_size(&self) -> Size<Pixels> {
let bounds = match self.bounds {
WindowBounds::Fixed(bounds) => bounds,
WindowBounds::Maximized | WindowBounds::Fullscreen => self.display().bounds(),
};
bounds.size.map(|p| px(p.0))
}
fn scale_factor(&self) -> f32 {
2.0
}
fn titlebar_height(&self) -> Pixels {
unimplemented!()
}
fn appearance(&self) -> WindowAppearance {
unimplemented!()
}
fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
self.display.clone()
}
fn mouse_position(&self) -> Point<Pixels> {
Point::default()
}
fn modifiers(&self) -> crate::Modifiers {
crate::Modifiers::default()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) {
self.input_handler = Some(Arc::new(Mutex::new(input_handler)));
}
fn clear_input_handler(&mut self) {
self.input_handler = None;
}
fn prompt(
&self,
_level: crate::PromptLevel,
_msg: &str,
_answers: &[&str],
) -> futures::channel::oneshot::Receiver<usize> {
self.platform.upgrade().expect("platform dropped").prompt()
}
fn activate(&self) {
unimplemented!()
}
fn set_title(&mut self, title: &str) {
self.title = Some(title.to_owned());
}
fn set_edited(&mut self, edited: bool) {
self.edited = edited;
}
fn show_character_palette(&self) {
unimplemented!()
}
fn minimize(&self) {
unimplemented!()
}
fn zoom(&self) {
unimplemented!()
}
fn toggle_full_screen(&self) {
unimplemented!()
}
fn on_input(&self, callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
self.handlers.lock().input.push(callback)
}
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.handlers.lock().active_status_change.push(callback)
}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.handlers.lock().resize.push(callback)
}
fn on_fullscreen(&self, _callback: Box<dyn FnMut(bool)>) {
unimplemented!()
}
fn on_moved(&self, callback: Box<dyn FnMut()>) {
self.handlers.lock().moved.push(callback)
}
fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) {
unimplemented!()
}
fn on_close(&self, _callback: Box<dyn FnOnce()>) {
unimplemented!()
}
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {
unimplemented!()
}
fn is_topmost_for_position(&self, _position: crate::Point<Pixels>) -> bool {
unimplemented!()
}
fn invalidate(&self) {
// (self.draw.lock())().unwrap();
}
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
self.sprite_atlas.clone()
}
fn as_test(&mut self) -> Option<&mut TestWindow> {
Some(self)
}
}
pub struct TestAtlasState {
next_id: u32,
tiles: HashMap<AtlasKey, AtlasTile>,
}
pub struct TestAtlas(Mutex<TestAtlasState>);
impl TestAtlas {
pub fn new() -> Self {
TestAtlas(Mutex::new(TestAtlasState {
next_id: 0,
tiles: HashMap::default(),
}))
}
}
impl PlatformAtlas for TestAtlas {
fn get_or_insert_with<'a>(
&self,
key: &crate::AtlasKey,
build: &mut dyn FnMut() -> anyhow::Result<(
Size<crate::DevicePixels>,
std::borrow::Cow<'a, [u8]>,
)>,
) -> anyhow::Result<crate::AtlasTile> {
let mut state = self.0.lock();
if let Some(tile) = state.tiles.get(key) {
return Ok(tile.clone());
}
state.next_id += 1;
let texture_id = state.next_id;
state.next_id += 1;
let tile_id = state.next_id;
drop(state);
let (size, _) = build()?;
let mut state = self.0.lock();
state.tiles.insert(
key.clone(),
crate::AtlasTile {
texture_id: AtlasTextureId {
index: texture_id,
kind: crate::AtlasTextureKind::Path,
},
tile_id: TileId(tile_id),
bounds: crate::Bounds {
origin: Point::default(),
size,
},
},
);
Ok(state.tiles[key].clone())
}
fn clear(&self) {
let mut state = self.0.lock();
state.tiles = HashMap::default();
state.next_id = 0;
}
}

View file

@ -0,0 +1,5 @@
pub use crate::{
BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement,
IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
VisualContext,
};

File diff suppressed because it is too large Load diff

View file

@ -1,270 +0,0 @@
use crate::{
platform::{MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent},
scene::mouse_region::HandlerKey,
};
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use std::{
mem::{discriminant, Discriminant},
ops::Deref,
};
#[derive(Debug, Default, Clone)]
pub struct MouseMove {
pub region: RectF,
pub platform_event: MouseMovedEvent,
}
impl Deref for MouseMove {
type Target = MouseMovedEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseMoveOut {
pub region: RectF,
}
#[derive(Debug, Default, Clone)]
pub struct MouseDrag {
pub region: RectF,
pub prev_mouse_position: Vector2F,
pub platform_event: MouseMovedEvent,
pub end: bool,
}
impl Deref for MouseDrag {
type Target = MouseMovedEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseHover {
pub region: RectF,
pub started: bool,
pub platform_event: MouseMovedEvent,
}
impl Deref for MouseHover {
type Target = MouseMovedEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseDown {
pub region: RectF,
pub platform_event: MouseButtonEvent,
}
impl Deref for MouseDown {
type Target = MouseButtonEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseUp {
pub region: RectF,
pub platform_event: MouseButtonEvent,
}
impl Deref for MouseUp {
type Target = MouseButtonEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseClick {
pub region: RectF,
pub platform_event: MouseButtonEvent,
}
impl Deref for MouseClick {
type Target = MouseButtonEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseClickOut {
pub region: RectF,
pub platform_event: MouseButtonEvent,
}
impl Deref for MouseClickOut {
type Target = MouseButtonEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseDownOut {
pub region: RectF,
pub platform_event: MouseButtonEvent,
}
impl Deref for MouseDownOut {
type Target = MouseButtonEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseUpOut {
pub region: RectF,
pub platform_event: MouseButtonEvent,
}
impl Deref for MouseUpOut {
type Target = MouseButtonEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseScrollWheel {
pub region: RectF,
pub platform_event: ScrollWheelEvent,
}
impl Deref for MouseScrollWheel {
type Target = ScrollWheelEvent;
fn deref(&self) -> &Self::Target {
&self.platform_event
}
}
#[derive(Debug, Clone)]
pub enum MouseEvent {
Move(MouseMove),
MoveOut(MouseMoveOut),
Drag(MouseDrag),
Hover(MouseHover),
Down(MouseDown),
Up(MouseUp),
Click(MouseClick),
ClickOut(MouseClickOut),
DownOut(MouseDownOut),
UpOut(MouseUpOut),
ScrollWheel(MouseScrollWheel),
}
impl MouseEvent {
pub fn set_region(&mut self, region: RectF) {
match self {
MouseEvent::Move(r) => r.region = region,
MouseEvent::MoveOut(r) => r.region = region,
MouseEvent::Drag(r) => r.region = region,
MouseEvent::Hover(r) => r.region = region,
MouseEvent::Down(r) => r.region = region,
MouseEvent::Up(r) => r.region = region,
MouseEvent::Click(r) => r.region = region,
MouseEvent::ClickOut(r) => r.region = region,
MouseEvent::DownOut(r) => r.region = region,
MouseEvent::UpOut(r) => r.region = region,
MouseEvent::ScrollWheel(r) => r.region = region,
}
}
/// When true, mouse event handlers must call cx.propagate_event() to bubble
/// the event to handlers they are painted on top of.
pub fn is_capturable(&self) -> bool {
match self {
MouseEvent::Move(_) => true,
MouseEvent::MoveOut(_) => false,
MouseEvent::Drag(_) => true,
MouseEvent::Hover(_) => false,
MouseEvent::Down(_) => true,
MouseEvent::Up(_) => true,
MouseEvent::Click(_) => true,
MouseEvent::ClickOut(_) => true,
MouseEvent::DownOut(_) => false,
MouseEvent::UpOut(_) => false,
MouseEvent::ScrollWheel(_) => true,
}
}
}
impl MouseEvent {
pub fn move_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::Move(Default::default()))
}
pub fn move_out_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::MoveOut(Default::default()))
}
pub fn drag_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::Drag(Default::default()))
}
pub fn hover_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::Hover(Default::default()))
}
pub fn down_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::Down(Default::default()))
}
pub fn up_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::Up(Default::default()))
}
pub fn up_out_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::UpOut(Default::default()))
}
pub fn click_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::Click(Default::default()))
}
pub fn click_out_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::ClickOut(Default::default()))
}
pub fn down_out_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::DownOut(Default::default()))
}
pub fn scroll_wheel_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::ScrollWheel(Default::default()))
}
pub fn handler_key(&self) -> HandlerKey {
match self {
MouseEvent::Move(_) => HandlerKey::new(Self::move_disc(), None),
MouseEvent::MoveOut(_) => HandlerKey::new(Self::move_out_disc(), None),
MouseEvent::Drag(e) => HandlerKey::new(Self::drag_disc(), e.pressed_button),
MouseEvent::Hover(_) => HandlerKey::new(Self::hover_disc(), None),
MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
MouseEvent::ClickOut(e) => HandlerKey::new(Self::click_out_disc(), Some(e.button)),
MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),
}
}
}

View file

@ -1,555 +0,0 @@
use crate::{platform::MouseButton, window::WindowContext, EventContext, TypeTag, ViewContext};
use collections::HashMap;
use pathfinder_geometry::rect::RectF;
use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, mem::Discriminant, rc::Rc};
use super::{
mouse_event::{
MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp,
MouseUpOut,
},
MouseClickOut, MouseMoveOut, MouseScrollWheel,
};
#[derive(Clone)]
pub struct MouseRegion {
pub id: MouseRegionId,
pub bounds: RectF,
pub handlers: HandlerSet,
pub hoverable: bool,
pub notify_on_hover: bool,
pub notify_on_click: bool,
}
impl MouseRegion {
/// Region ID is used to track semantically equivalent mouse regions across render passes.
/// e.g. if you have mouse handlers attached to a list item type, then each item of the list
/// should pass a different (consistent) region_id. If you have one big region that covers your
/// whole component, just pass the view_id again.
pub fn new<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
Self::from_handlers(
TypeTag::new::<Tag>(),
view_id,
region_id,
bounds,
Default::default(),
)
}
pub fn handle_all<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
Self::from_handlers(
TypeTag::new::<Tag>(),
view_id,
region_id,
bounds,
HandlerSet::capture_all(),
)
}
pub fn from_handlers(
tag: TypeTag,
view_id: usize,
region_id: usize,
bounds: RectF,
handlers: HandlerSet,
) -> Self {
Self {
id: MouseRegionId {
view_id,
tag,
region_id,
},
bounds,
handlers,
hoverable: true,
notify_on_hover: false,
notify_on_click: false,
}
}
pub fn on_down<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseDown, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_down(button, handler);
self
}
pub fn on_up<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseUp, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_up(button, handler);
self
}
pub fn on_click<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_click(button, handler);
self
}
pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_click_out(button, handler);
self
}
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseDownOut, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_down_out(button, handler);
self
}
pub fn on_up_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseUpOut, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_up_out(button, handler);
self
}
pub fn on_drag<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseDrag, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_drag(button, handler);
self
}
pub fn on_hover<V, F>(mut self, handler: F) -> Self
where
V: 'static,
F: Fn(MouseHover, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_hover(handler);
self
}
pub fn on_move<V, F>(mut self, handler: F) -> Self
where
V: 'static,
F: Fn(MouseMove, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_move(handler);
self
}
pub fn on_move_out<V, F>(mut self, handler: F) -> Self
where
V: 'static,
F: Fn(MouseMoveOut, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_move_out(handler);
self
}
pub fn on_scroll<V, F>(mut self, handler: F) -> Self
where
V: 'static,
F: Fn(MouseScrollWheel, &mut V, &mut EventContext<V>) + 'static,
{
self.handlers = self.handlers.on_scroll(handler);
self
}
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
self.hoverable = is_hoverable;
self
}
pub fn with_notify_on_hover(mut self, notify: bool) -> Self {
self.notify_on_hover = notify;
self
}
pub fn with_notify_on_click(mut self, notify: bool) -> Self {
self.notify_on_click = notify;
self
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
pub struct MouseRegionId {
view_id: usize,
tag: TypeTag,
region_id: usize,
}
impl MouseRegionId {
pub(crate) fn new(tag: TypeTag, view_id: usize, region_id: usize) -> Self {
MouseRegionId {
view_id,
region_id,
tag,
}
}
pub fn view_id(&self) -> usize {
self.view_id
}
#[cfg(debug_assertions)]
pub fn tag_type_name(&self) -> &'static str {
self.tag.type_name()
}
}
pub type HandlerCallback = Rc<dyn Fn(MouseEvent, &mut dyn Any, &mut WindowContext, usize) -> bool>;
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct HandlerKey {
event_kind: Discriminant<MouseEvent>,
button: Option<MouseButton>,
}
impl HandlerKey {
pub fn new(event_kind: Discriminant<MouseEvent>, button: Option<MouseButton>) -> HandlerKey {
HandlerKey { event_kind, button }
}
}
#[derive(Clone, Default)]
pub struct HandlerSet {
set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>>,
}
impl HandlerSet {
pub fn capture_all() -> Self {
let mut set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>> = HashMap::default();
set.insert(
HandlerKey::new(MouseEvent::move_disc(), None),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::hover_disc(), None),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
for button in MouseButton::all() {
set.insert(
HandlerKey::new(MouseEvent::drag_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::down_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::up_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::click_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::click_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::up_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
}
set.insert(
HandlerKey::new(MouseEvent::scroll_wheel_disc(), None),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
HandlerSet { set }
}
pub fn get(&self, key: &HandlerKey) -> Option<&[HandlerCallback]> {
self.set.get(key).map(|vec| vec.as_slice())
}
pub fn contains(
&self,
discriminant: Discriminant<MouseEvent>,
button: Option<MouseButton>,
) -> bool {
self.set
.contains_key(&HandlerKey::new(discriminant, button))
}
fn insert(
&mut self,
event_kind: Discriminant<MouseEvent>,
button: Option<MouseButton>,
callback: HandlerCallback,
) {
use std::collections::hash_map::Entry;
match self.set.entry(HandlerKey::new(event_kind, button)) {
Entry::Occupied(mut vec) => {
vec.get_mut().push(callback);
}
Entry::Vacant(entry) => {
entry.insert(SmallVec::from_buf([callback]));
}
}
}
pub fn on_move<V, F>(mut self, handler: F) -> Self
where
V: 'static,
F: Fn(MouseMove, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::move_disc(), None,
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::Move(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Move, found {:?}",
region_event);
}
}));
self
}
pub fn on_move_out<V, F>(mut self, handler: F) -> Self
where
V: 'static,
F: Fn(MouseMoveOut, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::move_out_disc(), None,
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::MoveOut(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::<V>::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::MoveOut, found {:?}",
region_event);
}
}));
self
}
pub fn on_down<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseDown, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::down_disc(), Some(button),
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::Down(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Down, found {:?}",
region_event);
}
}));
self
}
pub fn on_up<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseUp, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::up_disc(), Some(button),
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::Up(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Up, found {:?}",
region_event);
}
}));
self
}
pub fn on_click<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::click_disc(), Some(button),
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::Click(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Click, found {:?}",
region_event);
}
}));
self
}
pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::click_out_disc(), Some(button),
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::ClickOut(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ClickOut, found {:?}",
region_event);
}
}));
self
}
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseDownOut, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::down_out_disc(), Some(button),
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::DownOut(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::DownOut, found {:?}",
region_event);
}
}));
self
}
pub fn on_up_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseUpOut, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::up_out_disc(), Some(button),
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::UpOut(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::UpOut, found {:?}",
region_event);
}
}));
self
}
pub fn on_drag<V, F>(mut self, button: MouseButton, handler: F) -> Self
where
V: 'static,
F: Fn(MouseDrag, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::drag_disc(), Some(button),
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::Drag(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Drag, found {:?}",
region_event);
}
}));
self
}
pub fn on_hover<V, F>(mut self, handler: F) -> Self
where
V: 'static,
F: Fn(MouseHover, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::hover_disc(), None,
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::Hover(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Hover, found {:?}",
region_event);
}
}));
self
}
pub fn on_scroll<V, F>(mut self, handler: F) -> Self
where
V: 'static,
F: Fn(MouseScrollWheel, &mut V, &mut EventContext<V>) + 'static,
{
self.insert(MouseEvent::scroll_wheel_disc(), None,
Rc::new(move |region_event, view, cx, view_id| {
if let MouseEvent::ScrollWheel(e) = region_event {
let view = view.downcast_mut().unwrap();
let mut cx = ViewContext::mutable(cx, view_id);
let mut cx = EventContext::new(&mut cx);
handler(e, view, &mut cx);
cx.handled
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ScrollWheel, found {:?}",
region_event
);
}
}));
self
}
}

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