Merge branch 'gpui2' into gpui2-theme-to-color
This commit is contained in:
commit
3932c1064e
38 changed files with 2036 additions and 340 deletions
328
crates/gpui3/src/action.rs
Normal file
328
crates/gpui3/src/action.rs
Normal file
|
@ -0,0 +1,328 @@
|
|||
use crate::SharedString;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use std::any::Any;
|
||||
|
||||
pub trait Action: Any + Send + Sync {
|
||||
fn eq(&self, action: &dyn Action) -> bool;
|
||||
fn boxed_clone(&self) -> Box<dyn Action>;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct ActionContext {
|
||||
set: HashSet<SharedString>,
|
||||
map: HashMap<SharedString, SharedString>,
|
||||
}
|
||||
|
||||
impl ActionContext {
|
||||
pub fn new() -> Self {
|
||||
ActionContext {
|
||||
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<SharedString>>(&mut self, identifier: I) {
|
||||
self.set.insert(identifier.into());
|
||||
}
|
||||
|
||||
pub fn add_key<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
|
||||
self.map.insert(key.into(), value.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ActionContextPredicate {
|
||||
Identifier(SharedString),
|
||||
Equal(SharedString, SharedString),
|
||||
NotEqual(SharedString, SharedString),
|
||||
Child(Box<ActionContextPredicate>, Box<ActionContextPredicate>),
|
||||
Not(Box<ActionContextPredicate>),
|
||||
And(Box<ActionContextPredicate>, Box<ActionContextPredicate>),
|
||||
Or(Box<ActionContextPredicate>, Box<ActionContextPredicate>),
|
||||
}
|
||||
|
||||
impl ActionContextPredicate {
|
||||
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: &[ActionContext]) -> bool {
|
||||
let Some(context) = contexts.first() else {
|
||||
return false;
|
||||
};
|
||||
match self {
|
||||
Self::Identifier(name) => context.set.contains(&name),
|
||||
Self::Equal(left, right) => context
|
||||
.map
|
||||
.get(&left)
|
||||
.map(|value| value == right)
|
||||
.unwrap_or(false),
|
||||
Self::NotEqual(left, right) => context
|
||||
.map
|
||||
.get(&left)
|
||||
.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(ActionContextPredicate, ActionContextPredicate) -> Result<ActionContextPredicate>;
|
||||
|
||||
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((ActionContextPredicate::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((
|
||||
ActionContextPredicate::Identifier(identifier.to_string().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::ActionContextPredicate::{self, *};
|
||||
|
||||
#[test]
|
||||
fn test_parse_identifiers() {
|
||||
// Identifiers
|
||||
assert_eq!(
|
||||
ActionContextPredicate::parse("abc12").unwrap(),
|
||||
Identifier("abc12".into())
|
||||
);
|
||||
assert_eq!(
|
||||
ActionContextPredicate::parse("_1a").unwrap(),
|
||||
Identifier("_1a".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_negations() {
|
||||
assert_eq!(
|
||||
ActionContextPredicate::parse("!abc").unwrap(),
|
||||
Not(Box::new(Identifier("abc".into())))
|
||||
);
|
||||
assert_eq!(
|
||||
ActionContextPredicate::parse(" ! ! abc").unwrap(),
|
||||
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_equality_operators() {
|
||||
assert_eq!(
|
||||
ActionContextPredicate::parse("a == b").unwrap(),
|
||||
Equal("a".into(), "b".into())
|
||||
);
|
||||
assert_eq!(
|
||||
ActionContextPredicate::parse("c!=d").unwrap(),
|
||||
NotEqual("c".into(), "d".into())
|
||||
);
|
||||
assert_eq!(
|
||||
ActionContextPredicate::parse("c == !d")
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"operands must be identifiers"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_boolean_operators() {
|
||||
assert_eq!(
|
||||
ActionContextPredicate::parse("a || b").unwrap(),
|
||||
Or(
|
||||
Box::new(Identifier("a".into())),
|
||||
Box::new(Identifier("b".into()))
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
ActionContextPredicate::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!(
|
||||
ActionContextPredicate::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!(
|
||||
ActionContextPredicate::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!(
|
||||
ActionContextPredicate::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!(
|
||||
ActionContextPredicate::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!(
|
||||
ActionContextPredicate::parse(" ( a || b ) ").unwrap(),
|
||||
Or(
|
||||
Box::new(Identifier("a".into())),
|
||||
Box::new(Identifier("b".into())),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,14 +10,14 @@ use smallvec::SmallVec;
|
|||
|
||||
use crate::{
|
||||
current_platform, image_cache::ImageCache, AssetSource, Context, DisplayId, Executor,
|
||||
FocusEvent, FocusHandle, FocusId, LayoutId, MainThread, MainThreadOnly, Platform,
|
||||
SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, Window,
|
||||
WindowContext, WindowHandle, WindowId,
|
||||
FocusEvent, FocusHandle, FocusId, KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly,
|
||||
Platform, SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View,
|
||||
Window, WindowContext, WindowHandle, WindowId,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use futures::Future;
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use slotmap::SlotMap;
|
||||
use std::{
|
||||
any::{type_name, Any, TypeId},
|
||||
|
@ -67,6 +67,7 @@ impl App {
|
|||
unit_entity,
|
||||
entities,
|
||||
windows: SlotMap::with_key(),
|
||||
keymap: Arc::new(RwLock::new(Keymap::default())),
|
||||
pending_notifications: Default::default(),
|
||||
pending_effects: Default::default(),
|
||||
observers: SubscriberSet::new(),
|
||||
|
@ -111,6 +112,7 @@ pub struct AppContext {
|
|||
pub(crate) unit_entity: Handle<()>,
|
||||
pub(crate) entities: EntityMap,
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
pub(crate) keymap: Arc<RwLock<Keymap>>,
|
||||
pub(crate) pending_notifications: HashSet<EntityId>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
pub(crate) observers: SubscriberSet<EntityId, Handler>,
|
||||
|
@ -165,6 +167,7 @@ impl AppContext {
|
|||
}
|
||||
Effect::Emit { .. } => self.pending_effects.push_back(effect),
|
||||
Effect::FocusChanged { .. } => self.pending_effects.push_back(effect),
|
||||
Effect::Refresh => self.pending_effects.push_back(effect),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,6 +182,9 @@ impl AppContext {
|
|||
Effect::FocusChanged { window_id, focused } => {
|
||||
self.apply_focus_changed(window_id, focused)
|
||||
}
|
||||
Effect::Refresh => {
|
||||
self.apply_refresh();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
|
@ -284,6 +290,14 @@ impl AppContext {
|
|||
.ok();
|
||||
}
|
||||
|
||||
pub fn apply_refresh(&mut self) {
|
||||
for window in self.windows.values_mut() {
|
||||
if let Some(window) = window.as_mut() {
|
||||
window.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_async(&self) -> AsyncAppContext {
|
||||
AsyncAppContext(unsafe { mem::transmute(self.this.clone()) })
|
||||
}
|
||||
|
@ -403,6 +417,11 @@ impl AppContext {
|
|||
pub(crate) fn pop_text_style(&mut self) {
|
||||
self.text_style_stack.pop();
|
||||
}
|
||||
|
||||
pub fn bind_keys(&mut self, bindings: impl IntoIterator<Item = KeyBinding>) {
|
||||
self.keymap.write().add_bindings(bindings);
|
||||
self.push_effect(Effect::Refresh);
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AppContext {
|
||||
|
@ -492,6 +511,7 @@ pub(crate) enum Effect {
|
|||
window_id: WindowId,
|
||||
focused: Option<FocusId>,
|
||||
},
|
||||
Refresh,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -33,7 +33,7 @@ pub trait Element: 'static + Send + Sync + IntoAnyElement<Self::ViewState> {
|
|||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct GlobalElementId(SmallVec<[ElementId; 8]>);
|
||||
pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
|
||||
|
||||
pub trait ElementIdentity: 'static + Send + Sync {
|
||||
fn id(&self) -> Option<ElementId>;
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
use crate::{
|
||||
Active, Anonymous, AnyElement, AppContext, BorrowWindow, Bounds, Click, DispatchPhase, Element,
|
||||
ElementFocusability, ElementId, ElementIdentity, EventListeners, Focus, FocusHandle, Focusable,
|
||||
Hover, Identified, Interactive, IntoAnyElement, LayoutId, MouseClickEvent, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, NonFocusable, Overflow, ParentElement, Pixels, Point,
|
||||
ScrollWheelEvent, SharedString, Style, StyleRefinement, Styled, ViewContext,
|
||||
GlobalElementId, Hover, Identified, Interactive, IntoAnyElement, KeyDownEvent, KeyMatch,
|
||||
LayoutId, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, NonFocusable,
|
||||
Overflow, ParentElement, Pixels, Point, ScrollWheelEvent, SharedString, Style, StyleRefinement,
|
||||
Styled, ViewContext,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use refineable::Refineable;
|
||||
use smallvec::SmallVec;
|
||||
use std::{mem, sync::Arc};
|
||||
use std::{any::TypeId, mem, sync::Arc};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DivState {
|
||||
|
@ -187,12 +188,12 @@ where
|
|||
fn with_element_id<R>(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<V>,
|
||||
f: impl FnOnce(&mut Self, &mut ViewContext<V>) -> R,
|
||||
f: impl FnOnce(&mut Self, Option<GlobalElementId>, &mut ViewContext<V>) -> R,
|
||||
) -> R {
|
||||
if let Some(id) = self.id() {
|
||||
cx.with_element_id(id, |cx| f(self, cx))
|
||||
cx.with_element_id(id, |global_id, cx| f(self, Some(global_id), cx))
|
||||
} else {
|
||||
f(self, cx)
|
||||
f(self, None, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -423,27 +424,49 @@ where
|
|||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> Self::ElementState {
|
||||
for listener in self.listeners.focus.iter().cloned() {
|
||||
self.with_element_id(cx, |this, global_id, cx| {
|
||||
let element_state = element_state.unwrap_or_default();
|
||||
for listener in this.listeners.focus.iter().cloned() {
|
||||
cx.on_focus_changed(move |view, event, cx| listener(view, event, cx));
|
||||
}
|
||||
|
||||
let key_listeners = mem::take(&mut self.listeners.key);
|
||||
let mut key_listeners = mem::take(&mut this.listeners.key);
|
||||
|
||||
if let Some(global_id) = global_id {
|
||||
key_listeners.push((
|
||||
TypeId::of::<KeyDownEvent>(),
|
||||
Arc::new(move |_, key_down, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
let key_down = key_down.downcast_ref::<KeyDownEvent>().unwrap();
|
||||
if let KeyMatch::Some(action) =
|
||||
cx.match_keystroke(&global_id, &key_down.keystroke)
|
||||
{
|
||||
return Some(action);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
cx.with_key_listeners(&key_listeners, |cx| {
|
||||
if let Some(focus_handle) = self.focusability.focus_handle().cloned() {
|
||||
if let Some(focus_handle) = this.focusability.focus_handle().cloned() {
|
||||
cx.with_focus(focus_handle, |cx| {
|
||||
for child in &mut self.children {
|
||||
for child in &mut this.children {
|
||||
child.initialize(view_state, cx);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
for child in &mut self.children {
|
||||
for child in &mut this.children {
|
||||
child.initialize(view_state, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.listeners.key = key_listeners;
|
||||
this.listeners.key = key_listeners;
|
||||
|
||||
element_state.unwrap_or_default()
|
||||
element_state
|
||||
})
|
||||
}
|
||||
|
||||
fn layout(
|
||||
|
@ -454,7 +477,7 @@ where
|
|||
) -> LayoutId {
|
||||
let style = self.compute_style(Bounds::default(), element_state, cx);
|
||||
style.apply_text_style(cx, |cx| {
|
||||
self.with_element_id(cx, |this, cx| {
|
||||
self.with_element_id(cx, |this, _global_id, cx| {
|
||||
let layout_ids = this
|
||||
.children
|
||||
.iter_mut()
|
||||
|
@ -472,7 +495,7 @@ where
|
|||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) {
|
||||
self.with_element_id(cx, |this, cx| {
|
||||
self.with_element_id(cx, |this, _global_id, cx| {
|
||||
if let Some(group) = this.group.clone() {
|
||||
cx.default_global::<GroupBounds>()
|
||||
.0
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
point, Bounds, DispatchPhase, FocusHandle, Keystroke, Modifiers, Pixels, Point, ViewContext,
|
||||
point, Action, Bounds, DispatchPhase, FocusHandle, Keystroke, Modifiers, Pixels, Point,
|
||||
ViewContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
|
@ -254,8 +255,12 @@ pub type ScrollWheelListener<V> = Arc<
|
|||
+ 'static,
|
||||
>;
|
||||
|
||||
pub type KeyListener<V> =
|
||||
Arc<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + Send + Sync + 'static>;
|
||||
pub type KeyListener<V> = Arc<
|
||||
dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) -> Option<Box<dyn Action>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>;
|
||||
|
||||
pub type FocusListener<V> =
|
||||
Arc<dyn Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + Sync + 'static>;
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
use std::{any::TypeId, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
DispatchPhase, FocusEvent, FocusHandle, Interactive, KeyDownEvent, KeyUpEvent, StyleRefinement,
|
||||
ViewContext,
|
||||
};
|
||||
use crate::{FocusEvent, FocusHandle, Interactive, StyleRefinement, ViewContext};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait Focus: Interactive {
|
||||
fn set_focus_style(&mut self, style: StyleRefinement);
|
||||
|
@ -135,48 +131,4 @@ pub trait Focus: Interactive {
|
|||
}));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_key_down(
|
||||
mut self,
|
||||
listener: impl Fn(
|
||||
&mut Self::ViewState,
|
||||
&KeyDownEvent,
|
||||
DispatchPhase,
|
||||
&mut ViewContext<Self::ViewState>,
|
||||
) + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.listeners().key.push((
|
||||
TypeId::of::<KeyDownEvent>(),
|
||||
Arc::new(move |view, event, phase, cx| {
|
||||
let event = event.downcast_ref().unwrap();
|
||||
listener(view, event, phase, cx)
|
||||
}),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_key_up(
|
||||
mut self,
|
||||
listener: impl Fn(&mut Self::ViewState, &KeyUpEvent, DispatchPhase, &mut ViewContext<Self::ViewState>)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.listeners().key.push((
|
||||
TypeId::of::<KeyUpEvent>(),
|
||||
Arc::new(move |view, event, phase, cx| {
|
||||
let event = event.downcast_ref().unwrap();
|
||||
listener(view, event, phase, cx)
|
||||
}),
|
||||
));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
mod action;
|
||||
mod active;
|
||||
mod app;
|
||||
mod assets;
|
||||
|
@ -11,6 +12,7 @@ mod geometry;
|
|||
mod hover;
|
||||
mod image_cache;
|
||||
mod interactive;
|
||||
mod keymap;
|
||||
mod platform;
|
||||
mod scene;
|
||||
mod style;
|
||||
|
@ -23,6 +25,7 @@ mod util;
|
|||
mod view;
|
||||
mod window;
|
||||
|
||||
pub use action::*;
|
||||
pub use active::*;
|
||||
pub use anyhow::Result;
|
||||
pub use app::*;
|
||||
|
@ -38,6 +41,7 @@ pub use gpui3_macros::*;
|
|||
pub use hover::*;
|
||||
pub use image_cache::*;
|
||||
pub use interactive::*;
|
||||
pub use keymap::*;
|
||||
pub use platform::*;
|
||||
pub use refineable::*;
|
||||
pub use scene::*;
|
||||
|
@ -64,7 +68,7 @@ use std::{
|
|||
};
|
||||
use taffy::TaffyLayoutEngine;
|
||||
|
||||
type AnyBox = Box<dyn Any + Send + Sync + 'static>;
|
||||
type AnyBox = Box<dyn Any + Send + Sync>;
|
||||
|
||||
pub trait Context {
|
||||
type EntityContext<'a, 'w, T: 'static + Send + Sync>;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use std::sync::Arc;
|
||||
use std::{any::TypeId, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
DispatchPhase, Element, EventListeners, MouseButton, MouseClickEvent, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, ScrollWheelEvent, ViewContext,
|
||||
DispatchPhase, Element, EventListeners, KeyDownEvent, KeyUpEvent, MouseButton, MouseClickEvent,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ScrollWheelEvent, ViewContext,
|
||||
};
|
||||
|
||||
pub trait Interactive: Element {
|
||||
|
@ -143,6 +143,73 @@ pub trait Interactive: Element {
|
|||
}));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_key_down(
|
||||
mut self,
|
||||
listener: impl Fn(
|
||||
&mut Self::ViewState,
|
||||
&KeyDownEvent,
|
||||
DispatchPhase,
|
||||
&mut ViewContext<Self::ViewState>,
|
||||
) + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.listeners().key.push((
|
||||
TypeId::of::<KeyDownEvent>(),
|
||||
Arc::new(move |view, event, phase, cx| {
|
||||
let event = event.downcast_ref().unwrap();
|
||||
listener(view, event, phase, cx);
|
||||
None
|
||||
}),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_key_up(
|
||||
mut self,
|
||||
listener: impl Fn(&mut Self::ViewState, &KeyUpEvent, DispatchPhase, &mut ViewContext<Self::ViewState>)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.listeners().key.push((
|
||||
TypeId::of::<KeyUpEvent>(),
|
||||
Arc::new(move |view, event, phase, cx| {
|
||||
let event = event.downcast_ref().unwrap();
|
||||
listener(view, event, phase, cx);
|
||||
None
|
||||
}),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_action<A: 'static>(
|
||||
mut self,
|
||||
listener: impl Fn(&mut Self::ViewState, &A, DispatchPhase, &mut ViewContext<Self::ViewState>)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.listeners().key.push((
|
||||
TypeId::of::<A>(),
|
||||
Arc::new(move |view, event, phase, cx| {
|
||||
let event = event.downcast_ref().unwrap();
|
||||
listener(view, event, phase, cx);
|
||||
None
|
||||
}),
|
||||
));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Click: Interactive {
|
||||
|
|
80
crates/gpui3/src/keymap/binding.rs
Normal file
80
crates/gpui3/src/keymap/binding.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use crate::{Action, ActionContext, ActionContextPredicate, KeyMatch, Keystroke};
|
||||
use anyhow::Result;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub struct KeyBinding {
|
||||
action: Box<dyn Action>,
|
||||
pub(super) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
pub(super) context_predicate: Option<ActionContextPredicate>,
|
||||
}
|
||||
|
||||
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(ActionContextPredicate::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: &[ActionContext]) -> bool {
|
||||
self.context_predicate
|
||||
.as_ref()
|
||||
.map(|predicate| predicate.eval(contexts))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn match_keystrokes(
|
||||
&self,
|
||||
pending_keystrokes: &[Keystroke],
|
||||
contexts: &[ActionContext],
|
||||
) -> 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(self.action.boxed_clone())
|
||||
} else {
|
||||
KeyMatch::Pending
|
||||
}
|
||||
} else {
|
||||
KeyMatch::None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keystrokes_for_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
contexts: &[ActionContext],
|
||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||
if self.action.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()
|
||||
}
|
||||
}
|
397
crates/gpui3/src/keymap/keymap.rs
Normal file
397
crates/gpui3/src/keymap/keymap.rs
Normal file
|
@ -0,0 +1,397 @@
|
|||
use crate::{ActionContextPredicate, KeyBinding, Keystroke};
|
||||
use collections::HashSet;
|
||||
use smallvec::SmallVec;
|
||||
use std::{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<ActionContextPredicate>>>,
|
||||
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) {
|
||||
// todo!("no action")
|
||||
// let no_action_id = (NoAction {}).id();
|
||||
let mut new_bindings = Vec::new();
|
||||
let 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().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"
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
473
crates/gpui3/src/keymap/matcher.rs
Normal file
473
crates/gpui3/src/keymap/matcher.rs
Normal file
|
@ -0,0 +1,473 @@
|
|||
use crate::{Action, ActionContext, Keymap, KeymapVersion, Keystroke};
|
||||
use parking_lot::RwLock;
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct KeyMatcher {
|
||||
pending_keystrokes: Vec<Keystroke>,
|
||||
keymap: Arc<RwLock<Keymap>>,
|
||||
keymap_version: KeymapVersion,
|
||||
}
|
||||
|
||||
impl KeyMatcher {
|
||||
pub fn new(keymap: Arc<RwLock<Keymap>>) -> Self {
|
||||
let keymap_version = keymap.read().version();
|
||||
Self {
|
||||
pending_keystrokes: Vec::new(),
|
||||
keymap_version,
|
||||
keymap,
|
||||
}
|
||||
}
|
||||
|
||||
// todo!("replace with a function that calls an FnMut for every binding matching the action")
|
||||
// pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator<Item = &Binding> {
|
||||
// self.keymap.read().bindings_for_action(action_id)
|
||||
// }
|
||||
|
||||
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: &[ActionContext],
|
||||
) -> KeyMatch {
|
||||
let keymap = self.keymap.read();
|
||||
// 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;
|
||||
|
||||
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(action) => {
|
||||
self.pending_keystrokes.clear();
|
||||
return KeyMatch::Some(action);
|
||||
}
|
||||
KeyMatch::Pending => {
|
||||
pending_key.get_or_insert(candidate);
|
||||
}
|
||||
KeyMatch::None => {}
|
||||
}
|
||||
self.pending_keystrokes.pop();
|
||||
}
|
||||
}
|
||||
|
||||
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: &[ActionContext],
|
||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||
self.keymap
|
||||
.read()
|
||||
.bindings()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|binding| binding.keystrokes_for_action(action, contexts))
|
||||
}
|
||||
}
|
||||
|
||||
pub enum KeyMatch {
|
||||
None,
|
||||
Pending,
|
||||
Some(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(())
|
||||
// }
|
||||
// }
|
7
crates/gpui3/src/keymap/mod.rs
Normal file
7
crates/gpui3/src/keymap/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod binding;
|
||||
mod keymap;
|
||||
mod matcher;
|
||||
|
||||
pub use binding::*;
|
||||
pub use keymap::*;
|
||||
pub use matcher::*;
|
|
@ -1,29 +1,54 @@
|
|||
use anyhow::anyhow;
|
||||
use serde::Deserialize;
|
||||
use smallvec::SmallVec;
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Clone, Copy, 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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Hash)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
pub struct Keystroke {
|
||||
pub key: String,
|
||||
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,
|
||||
/// ime_key is the character inserted by the IME engine when that key was pressed.
|
||||
/// e.g. for option-s, ime_key is "ß"
|
||||
pub ime_key: Option<String>,
|
||||
}
|
||||
|
||||
impl Keystroke {
|
||||
// When matching a key we cannot know whether the user intended to type
|
||||
// the ime_key or the key. On some non-US keyboards keys we use in our
|
||||
// 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_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 {
|
||||
modifiers: Modifiers {
|
||||
control: self.modifiers.control,
|
||||
alt: false,
|
||||
shift: false,
|
||||
command: false,
|
||||
function: false,
|
||||
},
|
||||
key: ime_key.to_string(),
|
||||
ime_key: None,
|
||||
});
|
||||
possibilities.push(Keystroke {
|
||||
ime_key: None,
|
||||
..self.clone()
|
||||
});
|
||||
}
|
||||
}
|
||||
possibilities
|
||||
}
|
||||
|
||||
/// key syntax is:
|
||||
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
|
||||
/// 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 control = false;
|
||||
let mut alt = false;
|
||||
|
@ -31,6 +56,7 @@ impl Keystroke {
|
|||
let mut command = false;
|
||||
let mut function = false;
|
||||
let mut key = None;
|
||||
let mut ime_key = None;
|
||||
|
||||
let mut components = source.split('-').peekable();
|
||||
while let Some(component) = components.next() {
|
||||
|
@ -41,10 +67,14 @@ impl Keystroke {
|
|||
"cmd" => command = true,
|
||||
"fn" => function = true,
|
||||
_ => {
|
||||
if let Some(component) = components.peek() {
|
||||
if component.is_empty() && source.ends_with('-') {
|
||||
if let Some(next) = components.peek() {
|
||||
if next.is_empty() && source.ends_with('-') {
|
||||
key = Some(String::from("-"));
|
||||
break;
|
||||
} else if next.len() > 1 && next.starts_with('>') {
|
||||
key = Some(String::from(component));
|
||||
ime_key = Some(String::from(&next[1..]));
|
||||
components.next();
|
||||
} else {
|
||||
return Err(anyhow!("Invalid keystroke `{}`", source));
|
||||
}
|
||||
|
@ -66,40 +96,25 @@ impl Keystroke {
|
|||
function,
|
||||
},
|
||||
key,
|
||||
ime_key,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn modified(&self) -> bool {
|
||||
self.modifiers.modified()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Keystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let Modifiers {
|
||||
control,
|
||||
alt,
|
||||
shift,
|
||||
command,
|
||||
function,
|
||||
} = self.modifiers;
|
||||
|
||||
if control {
|
||||
if self.modifiers.control {
|
||||
f.write_char('^')?;
|
||||
}
|
||||
if alt {
|
||||
f.write_char('⎇')?;
|
||||
if self.modifiers.alt {
|
||||
f.write_char('⌥')?;
|
||||
}
|
||||
if command {
|
||||
if self.modifiers.command {
|
||||
f.write_char('⌘')?;
|
||||
}
|
||||
if shift {
|
||||
if self.modifiers.shift {
|
||||
f.write_char('⇧')?;
|
||||
}
|
||||
if function {
|
||||
f.write_char('𝙛')?;
|
||||
}
|
||||
|
||||
let key = match self.key.as_str() {
|
||||
"backspace" => '⌫',
|
||||
"up" => '↑',
|
||||
|
@ -119,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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -325,6 +325,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
|||
function,
|
||||
},
|
||||
key,
|
||||
ime_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1043,6 +1043,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
|||
// we don't match cmd/fn because they don't seem to use IME
|
||||
modifiers: Default::default(),
|
||||
key: ime_text.clone().unwrap(),
|
||||
ime_key: None, // todo!("handle IME key")
|
||||
},
|
||||
};
|
||||
handled = callback(InputEvent::KeyDown(event_with_ime_text));
|
||||
|
@ -1203,6 +1204,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
|
|||
let keystroke = Keystroke {
|
||||
modifiers: Default::default(),
|
||||
key: ".".into(),
|
||||
ime_key: None,
|
||||
};
|
||||
let event = InputEvent::KeyDown(KeyDownEvent {
|
||||
keystroke: keystroke.clone(),
|
||||
|
|
|
@ -219,8 +219,8 @@ impl TextSystem {
|
|||
Ok(lines)
|
||||
}
|
||||
|
||||
pub fn end_frame(&self) {
|
||||
self.line_layout_cache.end_frame()
|
||||
pub fn start_frame(&self) {
|
||||
self.line_layout_cache.start_frame()
|
||||
}
|
||||
|
||||
pub fn line_wrapper(
|
||||
|
|
|
@ -167,7 +167,7 @@ impl LineLayoutCache {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn end_frame(&self) {
|
||||
pub fn start_frame(&self) {
|
||||
let mut prev_frame = self.prev_frame.lock();
|
||||
let mut curr_frame = self.curr_frame.write();
|
||||
std::mem::swap(&mut *prev_frame, &mut *curr_frame);
|
||||
|
|
|
@ -164,7 +164,7 @@ impl<V: Send + Sync + 'static> ViewObject for View<V> {
|
|||
}
|
||||
|
||||
fn initialize(&mut self, cx: &mut WindowContext) -> AnyBox {
|
||||
cx.with_element_id(self.entity_id(), |cx| {
|
||||
cx.with_element_id(self.entity_id(), |_global_id, cx| {
|
||||
self.state.update(cx, |state, cx| {
|
||||
let mut any_element = Box::new((self.render)(state, cx));
|
||||
any_element.initialize(state, cx);
|
||||
|
@ -174,7 +174,7 @@ impl<V: Send + Sync + 'static> ViewObject for View<V> {
|
|||
}
|
||||
|
||||
fn layout(&mut self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId {
|
||||
cx.with_element_id(self.entity_id(), |cx| {
|
||||
cx.with_element_id(self.entity_id(), |_global_id, cx| {
|
||||
self.state.update(cx, |state, cx| {
|
||||
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
|
||||
element.layout(state, cx)
|
||||
|
@ -183,7 +183,7 @@ impl<V: Send + Sync + 'static> ViewObject for View<V> {
|
|||
}
|
||||
|
||||
fn paint(&mut self, _: Bounds<Pixels>, element: &mut AnyBox, cx: &mut WindowContext) {
|
||||
cx.with_element_id(self.entity_id(), |cx| {
|
||||
cx.with_element_id(self.entity_id(), |_global_id, cx| {
|
||||
self.state.update(cx, |state, cx| {
|
||||
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
|
||||
element.paint(state, None, cx);
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use crate::{
|
||||
px, size, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, BorrowAppContext,
|
||||
Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId, Edges, Effect, Element, EntityId,
|
||||
EventEmitter, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla, ImageData,
|
||||
InputEvent, IsZero, KeyListener, LayoutId, MainThread, MainThreadOnly, MonochromeSprite,
|
||||
MouseMoveEvent, Path, Pixels, Platform, PlatformAtlas, PlatformWindow, Point, PolychromeSprite,
|
||||
Quad, Reference, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
|
||||
SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task,
|
||||
Underline, UnderlineStyle, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
|
||||
px, size, Action, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
|
||||
BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId, Edges, Effect,
|
||||
Element, EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla,
|
||||
ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId,
|
||||
MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent, Path, Pixels, Platform,
|
||||
PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams,
|
||||
RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
|
||||
Style, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, WeakHandle,
|
||||
WindowOptions, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
|
@ -45,6 +46,12 @@ pub enum DispatchPhase {
|
|||
}
|
||||
|
||||
type AnyListener = Arc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + Send + Sync + 'static>;
|
||||
type AnyKeyListener = Arc<
|
||||
dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) -> Option<Box<dyn Action>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>;
|
||||
type AnyFocusListener = Arc<dyn Fn(&FocusEvent, &mut WindowContext) + Send + Sync + 'static>;
|
||||
|
||||
slotmap::new_key_type! { pub struct FocusId; }
|
||||
|
@ -141,18 +148,20 @@ pub struct Window {
|
|||
layout_engine: TaffyLayoutEngine,
|
||||
pub(crate) root_view: Option<AnyView>,
|
||||
pub(crate) element_id_stack: GlobalElementId,
|
||||
prev_element_states: HashMap<GlobalElementId, AnyBox>,
|
||||
prev_frame_element_states: HashMap<GlobalElementId, AnyBox>,
|
||||
element_states: HashMap<GlobalElementId, AnyBox>,
|
||||
prev_frame_key_matchers: HashMap<GlobalElementId, KeyMatcher>,
|
||||
key_matchers: HashMap<GlobalElementId, KeyMatcher>,
|
||||
z_index_stack: StackingOrder,
|
||||
content_mask_stack: Vec<ContentMask<Pixels>>,
|
||||
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>,
|
||||
key_listeners: HashMap<TypeId, Vec<AnyListener>>,
|
||||
key_listeners: Vec<(TypeId, AnyKeyListener)>,
|
||||
key_events_enabled: bool,
|
||||
focus_stack: Vec<FocusId>,
|
||||
focus_parents_by_child: HashMap<FocusId, FocusId>,
|
||||
pub(crate) focus_listeners: Vec<AnyFocusListener>,
|
||||
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||
propagate_event: bool,
|
||||
propagate: bool,
|
||||
default_prevented: bool,
|
||||
mouse_position: Point<Pixels>,
|
||||
scale_factor: f32,
|
||||
|
@ -214,17 +223,19 @@ impl Window {
|
|||
layout_engine: TaffyLayoutEngine::new(),
|
||||
root_view: None,
|
||||
element_id_stack: GlobalElementId::default(),
|
||||
prev_element_states: HashMap::default(),
|
||||
prev_frame_element_states: HashMap::default(),
|
||||
element_states: HashMap::default(),
|
||||
prev_frame_key_matchers: HashMap::default(),
|
||||
key_matchers: HashMap::default(),
|
||||
z_index_stack: StackingOrder(SmallVec::new()),
|
||||
content_mask_stack: Vec::new(),
|
||||
mouse_listeners: HashMap::default(),
|
||||
key_listeners: HashMap::default(),
|
||||
key_listeners: Vec::new(),
|
||||
key_events_enabled: true,
|
||||
focus_stack: Vec::new(),
|
||||
focus_parents_by_child: HashMap::default(),
|
||||
focus_listeners: Vec::new(),
|
||||
propagate_event: true,
|
||||
propagate: true,
|
||||
default_prevented: true,
|
||||
mouse_position,
|
||||
scale_factor,
|
||||
|
@ -434,7 +445,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
|||
}
|
||||
|
||||
pub fn stop_propagation(&mut self) {
|
||||
self.window.propagate_event = false;
|
||||
self.window.propagate = false;
|
||||
}
|
||||
|
||||
pub fn prevent_default(&mut self) {
|
||||
|
@ -462,19 +473,6 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
|||
))
|
||||
}
|
||||
|
||||
pub fn on_keyboard_event<Event: 'static>(
|
||||
&mut self,
|
||||
handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + Send + Sync + 'static,
|
||||
) {
|
||||
self.window
|
||||
.key_listeners
|
||||
.entry(TypeId::of::<Event>())
|
||||
.or_default()
|
||||
.push(Arc::new(move |event: &dyn Any, phase, cx| {
|
||||
handler(event.downcast_ref().unwrap(), phase, cx)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn mouse_position(&self) -> Point<Pixels> {
|
||||
self.window.mouse_position
|
||||
}
|
||||
|
@ -777,7 +775,6 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
|||
|
||||
cx.window.root_view = Some(root_view);
|
||||
let scene = cx.window.scene_builder.build();
|
||||
cx.end_frame();
|
||||
|
||||
cx.run_on_main(view, |_, cx| {
|
||||
cx.window
|
||||
|
@ -807,13 +804,28 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
|||
}
|
||||
|
||||
fn start_frame(&mut self) {
|
||||
// Make the current element states the previous, and then clear the current.
|
||||
// The empty element states map will be populated for any element states we
|
||||
// reference during the upcoming frame.
|
||||
self.text_system().start_frame();
|
||||
|
||||
let window = &mut *self.window;
|
||||
mem::swap(&mut window.element_states, &mut window.prev_element_states);
|
||||
|
||||
// Move the current frame element states to the previous frame.
|
||||
// The new empty element states map will be populated for any element states we
|
||||
// reference during the upcoming frame.
|
||||
mem::swap(
|
||||
&mut window.element_states,
|
||||
&mut window.prev_frame_element_states,
|
||||
);
|
||||
window.element_states.clear();
|
||||
|
||||
// Make the current key matchers the previous, and then clear the current.
|
||||
// An empty key matcher map will be created for every identified element in the
|
||||
// upcoming frame.
|
||||
mem::swap(
|
||||
&mut window.key_matchers,
|
||||
&mut window.prev_frame_key_matchers,
|
||||
);
|
||||
window.key_matchers.clear();
|
||||
|
||||
// Clear mouse event listeners, because elements add new element listeners
|
||||
// when the upcoming frame is painted.
|
||||
window.mouse_listeners.values_mut().for_each(Vec::clear);
|
||||
|
@ -821,15 +833,11 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
|||
// Clear focus state, because we determine what is focused when the new elements
|
||||
// in the upcoming frame are initialized.
|
||||
window.focus_listeners.clear();
|
||||
window.key_listeners.values_mut().for_each(Vec::clear);
|
||||
window.key_listeners.clear();
|
||||
window.focus_parents_by_child.clear();
|
||||
window.key_events_enabled = true;
|
||||
}
|
||||
|
||||
fn end_frame(&mut self) {
|
||||
self.text_system().end_frame();
|
||||
}
|
||||
|
||||
fn dispatch_event(&mut self, event: InputEvent) -> bool {
|
||||
if let Some(any_mouse_event) = event.mouse_event() {
|
||||
if let Some(MouseMoveEvent { position, .. }) = any_mouse_event.downcast_ref() {
|
||||
|
@ -837,7 +845,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
|||
}
|
||||
|
||||
// Handlers may set this to false by calling `stop_propagation`
|
||||
self.window.propagate_event = true;
|
||||
self.window.propagate = true;
|
||||
self.window.default_prevented = false;
|
||||
|
||||
if let Some(mut handlers) = self
|
||||
|
@ -852,16 +860,16 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
|||
// special purposes, such as detecting events outside of a given Bounds.
|
||||
for (_, handler) in &handlers {
|
||||
handler(any_mouse_event, DispatchPhase::Capture, self);
|
||||
if !self.window.propagate_event {
|
||||
if !self.window.propagate {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble phase, where most normal handlers do their work.
|
||||
if self.window.propagate_event {
|
||||
if self.window.propagate {
|
||||
for (_, handler) in handlers.iter().rev() {
|
||||
handler(any_mouse_event, DispatchPhase::Bubble, self);
|
||||
if !self.window.propagate_event {
|
||||
if !self.window.propagate {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -879,43 +887,85 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
|||
.mouse_listeners
|
||||
.insert(any_mouse_event.type_id(), handlers);
|
||||
}
|
||||
} else if let Some(any_keyboard_event) = event.keyboard_event() {
|
||||
if let Some(mut handlers) = self
|
||||
.window
|
||||
.key_listeners
|
||||
.remove(&any_keyboard_event.type_id())
|
||||
} else if let Some(any_key_event) = event.keyboard_event() {
|
||||
let key_listeners = mem::take(&mut self.window.key_listeners);
|
||||
let key_event_type = any_key_event.type_id();
|
||||
|
||||
for (ix, (listener_event_type, listener)) in key_listeners.iter().enumerate() {
|
||||
if key_event_type == *listener_event_type {
|
||||
if let Some(action) = listener(any_key_event, DispatchPhase::Capture, self) {
|
||||
self.dispatch_action(action, &key_listeners[..ix]);
|
||||
}
|
||||
if !self.window.propagate {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.window.propagate {
|
||||
for (ix, (listener_event_type, listener)) in key_listeners.iter().enumerate().rev()
|
||||
{
|
||||
for handler in &handlers {
|
||||
handler(any_keyboard_event, DispatchPhase::Capture, self);
|
||||
if !self.window.propagate_event {
|
||||
break;
|
||||
}
|
||||
if key_event_type == *listener_event_type {
|
||||
if let Some(action) = listener(any_key_event, DispatchPhase::Bubble, self) {
|
||||
self.dispatch_action(action, &key_listeners[..ix]);
|
||||
}
|
||||
|
||||
if self.window.propagate_event {
|
||||
for handler in handlers.iter().rev() {
|
||||
handler(any_keyboard_event, DispatchPhase::Bubble, self);
|
||||
if !self.window.propagate_event {
|
||||
if !self.window.propagate {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlers.extend(
|
||||
self.window
|
||||
.key_listeners
|
||||
.get_mut(&any_keyboard_event.type_id())
|
||||
.into_iter()
|
||||
.flat_map(|handlers| handlers.drain(..)),
|
||||
);
|
||||
self.window
|
||||
.key_listeners
|
||||
.insert(any_keyboard_event.type_id(), handlers);
|
||||
}
|
||||
|
||||
self.window.key_listeners = key_listeners;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn match_keystroke(
|
||||
&mut self,
|
||||
element_id: &GlobalElementId,
|
||||
keystroke: &Keystroke,
|
||||
) -> KeyMatch {
|
||||
let key_match = self
|
||||
.window
|
||||
.key_matchers
|
||||
.get_mut(element_id)
|
||||
.unwrap()
|
||||
.match_keystroke(keystroke, &[]);
|
||||
|
||||
if key_match.is_some() {
|
||||
for matcher in self.window.key_matchers.values_mut() {
|
||||
matcher.clear_pending();
|
||||
}
|
||||
}
|
||||
|
||||
key_match
|
||||
}
|
||||
|
||||
fn dispatch_action(&mut self, action: Box<dyn Action>, listeners: &[(TypeId, AnyKeyListener)]) {
|
||||
let action_type = action.as_any().type_id();
|
||||
for (event_type, listener) in listeners {
|
||||
if action_type == *event_type {
|
||||
listener(action.as_any(), DispatchPhase::Capture, self);
|
||||
if !self.window.propagate {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.window.propagate {
|
||||
for (event_type, listener) in listeners.iter().rev() {
|
||||
if action_type == *event_type {
|
||||
listener(action.as_any(), DispatchPhase::Bubble, self);
|
||||
if !self.window.propagate {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'w> MainThread<WindowContext<'a, 'w>> {
|
||||
|
@ -983,10 +1033,24 @@ pub trait BorrowWindow: BorrowAppContext {
|
|||
fn with_element_id<R>(
|
||||
&mut self,
|
||||
id: impl Into<ElementId>,
|
||||
f: impl FnOnce(&mut Self) -> R,
|
||||
f: impl FnOnce(GlobalElementId, &mut Self) -> R,
|
||||
) -> R {
|
||||
self.window_mut().element_id_stack.push(id.into());
|
||||
let result = f(self);
|
||||
let keymap = self.app_mut().keymap.clone();
|
||||
let window = self.window_mut();
|
||||
window.element_id_stack.push(id.into());
|
||||
let global_id = window.element_id_stack.clone();
|
||||
|
||||
if window.key_matchers.get(&global_id).is_none() {
|
||||
window.key_matchers.insert(
|
||||
global_id.clone(),
|
||||
window
|
||||
.prev_frame_key_matchers
|
||||
.remove(&global_id)
|
||||
.unwrap_or_else(|| KeyMatcher::new(keymap)),
|
||||
);
|
||||
}
|
||||
|
||||
let result = f(global_id, self);
|
||||
self.window_mut().element_id_stack.pop();
|
||||
result
|
||||
}
|
||||
|
@ -1008,13 +1072,12 @@ pub trait BorrowWindow: BorrowAppContext {
|
|||
id: ElementId,
|
||||
f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
|
||||
) -> R {
|
||||
self.with_element_id(id, |cx| {
|
||||
let global_id = cx.window_mut().element_id_stack.clone();
|
||||
self.with_element_id(id, |global_id, cx| {
|
||||
if let Some(any) = cx
|
||||
.window_mut()
|
||||
.element_states
|
||||
.remove(&global_id)
|
||||
.or_else(|| cx.window_mut().prev_element_states.remove(&global_id))
|
||||
.or_else(|| cx.window_mut().prev_frame_element_states.remove(&global_id))
|
||||
{
|
||||
// Using the extra inner option to avoid needing to reallocate a new box.
|
||||
let mut state_box = any
|
||||
|
@ -1225,28 +1288,25 @@ impl<'a, 'w, V: Send + Sync + 'static> ViewContext<'a, 'w, V> {
|
|||
f: impl FnOnce(&mut Self) -> R,
|
||||
) -> R {
|
||||
if self.window.key_events_enabled {
|
||||
for (event_type, listener) in key_listeners.iter().cloned() {
|
||||
let handle = self.handle();
|
||||
for (type_id, listener) in key_listeners {
|
||||
let handle = handle.clone();
|
||||
let listener = listener.clone();
|
||||
self.window
|
||||
.key_listeners
|
||||
.entry(*type_id)
|
||||
.or_default()
|
||||
.push(Arc::new(move |event, phase, cx| {
|
||||
let listener = Arc::new(
|
||||
move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_, '_>| {
|
||||
handle
|
||||
.update(cx, |view, cx| listener(view, event, phase, cx))
|
||||
.log_err();
|
||||
}));
|
||||
.log_err()
|
||||
.flatten()
|
||||
},
|
||||
);
|
||||
self.window.key_listeners.push((event_type, listener));
|
||||
}
|
||||
}
|
||||
|
||||
let result = f(self);
|
||||
|
||||
if self.window.key_events_enabled {
|
||||
for (type_id, _) in key_listeners {
|
||||
self.window.key_listeners.get_mut(type_id).unwrap().pop();
|
||||
}
|
||||
let prev_len = self.window.key_listeners.len() - key_listeners.len();
|
||||
self.window.key_listeners.truncate(prev_len);
|
||||
}
|
||||
|
||||
result
|
||||
|
@ -1317,18 +1377,6 @@ impl<'a, 'w, V: Send + Sync + 'static> ViewContext<'a, 'w, V> {
|
|||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn on_keyboard_event<Event: 'static>(
|
||||
&mut self,
|
||||
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + Send + Sync + 'static,
|
||||
) {
|
||||
let handle = self.handle().upgrade(self).unwrap();
|
||||
self.window_cx.on_keyboard_event(move |event, phase, cx| {
|
||||
handle.update(cx, |view, cx| {
|
||||
handler(view, event, phase, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'w, S: EventEmitter + Send + Sync + 'static> ViewContext<'a, 'w, S> {
|
||||
|
|
|
@ -1,13 +1,56 @@
|
|||
use gpui3::{div, view, Context, Focus, ParentElement, Styled, View, WindowContext};
|
||||
use std::any::Any;
|
||||
|
||||
use gpui3::{
|
||||
div, view, Action, Context, Focus, Interactive, KeyBinding, ParentElement, Styled, View,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
use crate::themes::rose_pine;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ActionA;
|
||||
|
||||
impl Action for ActionA {
|
||||
fn eq(&self, action: &dyn Action) -> bool {
|
||||
action.as_any().downcast_ref::<Self>().is_some()
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> Box<dyn Action> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ActionB;
|
||||
|
||||
impl Action for ActionB {
|
||||
fn eq(&self, action: &dyn Action) -> bool {
|
||||
action.as_any().downcast_ref::<Self>().is_some()
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> Box<dyn Action> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FocusStory {
|
||||
text: View<()>,
|
||||
}
|
||||
|
||||
impl FocusStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<()> {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("cmd-a", ActionA, None),
|
||||
KeyBinding::new("cmd-b", ActionB, None),
|
||||
]);
|
||||
let theme = rose_pine();
|
||||
|
||||
let color_1 = theme.lowest.negative.default.foreground;
|
||||
|
@ -22,6 +65,12 @@ impl FocusStory {
|
|||
let child_2 = cx.focus_handle();
|
||||
view(cx.entity(|cx| ()), move |_, cx| {
|
||||
div()
|
||||
.on_action(|_, action: &ActionA, phase, cx| {
|
||||
println!("Action A dispatched on parent during {:?}", phase);
|
||||
})
|
||||
.on_action(|_, action: &ActionB, phase, cx| {
|
||||
println!("Action B dispatched on parent during {:?}", phase);
|
||||
})
|
||||
.focusable(&parent)
|
||||
.on_focus(|_, _, _| println!("Parent focused"))
|
||||
.on_blur(|_, _, _| println!("Parent blurred"))
|
||||
|
@ -39,6 +88,10 @@ impl FocusStory {
|
|||
.focus_in(|style| style.bg(color_3))
|
||||
.child(
|
||||
div()
|
||||
.id("child 1")
|
||||
.on_action(|_, action: &ActionA, phase, cx| {
|
||||
println!("Action A dispatched on child 1 during {:?}", phase);
|
||||
})
|
||||
.focusable(&child_1)
|
||||
.w_full()
|
||||
.h_6()
|
||||
|
@ -59,6 +112,10 @@ impl FocusStory {
|
|||
)
|
||||
.child(
|
||||
div()
|
||||
.id("child 2")
|
||||
.on_action(|_, action: &ActionB, phase, cx| {
|
||||
println!("Action B dispatched on child 2 during {:?}", phase);
|
||||
})
|
||||
.focusable(&child_2)
|
||||
.w_full()
|
||||
.h_6()
|
||||
|
|
|
@ -17,7 +17,7 @@ use log::LevelFilter;
|
|||
use simplelog::SimpleLogger;
|
||||
use story_selector::ComponentStory;
|
||||
use ui::prelude::*;
|
||||
use ui::themed;
|
||||
use ui::{themed, with_settings, FakeSettings};
|
||||
|
||||
use crate::assets::Assets;
|
||||
use crate::story_selector::StorySelector;
|
||||
|
@ -68,9 +68,11 @@ fn main() {
|
|||
move |cx| {
|
||||
view(
|
||||
cx.entity(|cx| {
|
||||
cx.with_global(FakeSettings::default(), |cx| {
|
||||
cx.with_global(theme.clone(), |cx| {
|
||||
StoryWrapper::new(selector.story(cx), theme)
|
||||
})
|
||||
})
|
||||
}),
|
||||
StoryWrapper::render,
|
||||
)
|
||||
|
@ -85,14 +87,20 @@ fn main() {
|
|||
pub struct StoryWrapper {
|
||||
story: AnyView,
|
||||
theme: Theme,
|
||||
settings: FakeSettings,
|
||||
}
|
||||
|
||||
impl StoryWrapper {
|
||||
pub(crate) fn new(story: AnyView, theme: Theme) -> Self {
|
||||
Self { story, theme }
|
||||
Self {
|
||||
story,
|
||||
theme,
|
||||
settings: FakeSettings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> {
|
||||
with_settings(self.settings.clone(), cx, |cx| {
|
||||
themed(self.theme.clone(), cx, |cx| {
|
||||
div()
|
||||
.flex()
|
||||
|
@ -100,6 +108,7 @@ impl StoryWrapper {
|
|||
.size_full()
|
||||
.child(self.story.clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ impl<S: 'static + Send + Sync + Clone> AssistantPanel<S> {
|
|||
fn render(&mut self, view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
|
||||
let color = ThemeColor::new(cx);
|
||||
|
||||
Panel::new(self.scroll_state.clone())
|
||||
Panel::new(cx)
|
||||
.children(vec![div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
|
|
|
@ -135,13 +135,10 @@ mod stories {
|
|||
Story::container(cx)
|
||||
.child(Story::title_for::<_, ChatPanel<S>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
Panel::new(ScrollState::default())
|
||||
.child(ChatPanel::new(ScrollState::default())),
|
||||
)
|
||||
.child(Panel::new(cx).child(ChatPanel::new(ScrollState::default())))
|
||||
.child(Story::label(cx, "With Mesages"))
|
||||
.child(Panel::new(ScrollState::default()).child(
|
||||
ChatPanel::new(ScrollState::default()).messages(vec![
|
||||
.child(
|
||||
Panel::new(cx).child(ChatPanel::new(ScrollState::default()).messages(vec![
|
||||
ChatMessage::new(
|
||||
"osiewicz".to_string(),
|
||||
"is this thing on?".to_string(),
|
||||
|
@ -156,8 +153,8 @@ mod stories {
|
|||
.unwrap()
|
||||
.naive_local(),
|
||||
),
|
||||
]),
|
||||
))
|
||||
])),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,12 +130,13 @@ impl<S: 'static + Send + Sync + Clone> CollabPanel<S> {
|
|||
let color = ThemeColor::new(cx);
|
||||
|
||||
div()
|
||||
.id("list_item")
|
||||
.h_7()
|
||||
.px_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.hover(|style| style.bg(color.ghost_element_hover))
|
||||
// .active(|style| style.fill(theme.lowest.variant.pressed.background))
|
||||
.active(|style| style.bg(color.ghost_element_active))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
|
|
|
@ -90,11 +90,11 @@ impl<S: 'static + Send + Sync> IconButton<S> {
|
|||
let mut button = h_stack()
|
||||
.justify_center()
|
||||
.rounded_md()
|
||||
.py(ui_size(0.25))
|
||||
.px(ui_size(6. / 14.))
|
||||
.py(ui_size(cx, 0.25))
|
||||
.px(ui_size(cx, 6. / 14.))
|
||||
.bg(bg_color)
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
// .active(|style| style.bg(bg_active_color))
|
||||
.active(|style| style.bg(bg_active_color))
|
||||
.child(IconElement::new(self.icon).color(icon_color));
|
||||
|
||||
if let Some(click_handler) = self.handlers.click.clone() {
|
||||
|
|
|
@ -362,7 +362,7 @@ impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
|
|||
let color = ThemeColor::new(cx);
|
||||
let system_color = SystemColor::new();
|
||||
let color = ThemeColor::new(cx);
|
||||
let setting = user_settings();
|
||||
let settings = user_settings(cx);
|
||||
|
||||
let left_content = match self.left_content.clone() {
|
||||
Some(LeftContent::Icon(i)) => Some(
|
||||
|
@ -394,7 +394,7 @@ impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
|
|||
// .ml(rems(0.75 * self.indent_level as f32))
|
||||
.children((0..self.indent_level).map(|_| {
|
||||
div()
|
||||
.w(*setting.list_indent_depth)
|
||||
.w(*settings.list_indent_depth)
|
||||
.h_full()
|
||||
.flex()
|
||||
.justify_center()
|
||||
|
|
|
@ -83,14 +83,15 @@ impl<S: 'static + Send + Sync + Clone> Palette<S> {
|
|||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.children(self.items.iter().map(|item| {
|
||||
.children(self.items.iter().enumerate().map(|(index, item)| {
|
||||
h_stack()
|
||||
.id(index)
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded_lg()
|
||||
.hover(|style| style.bg(color.ghost_element_hover))
|
||||
// .active(|style| style.bg(color.ghost_element_active))
|
||||
.active(|style| style.bg(color.ghost_element_active))
|
||||
.child(item.clone())
|
||||
})),
|
||||
),
|
||||
|
|
|
@ -53,15 +53,15 @@ pub struct Panel<S: 'static + Send + Sync> {
|
|||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> Panel<S> {
|
||||
pub fn new(scroll_state: ScrollState) -> Self {
|
||||
let setting = user_settings();
|
||||
pub fn new(cx: &mut WindowContext) -> Self {
|
||||
let settings = user_settings(cx);
|
||||
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
scroll_state,
|
||||
scroll_state: ScrollState::default(),
|
||||
current_side: PanelSide::default(),
|
||||
allowed_sides: PanelAllowedSides::default(),
|
||||
initial_width: *setting.default_panel_size,
|
||||
initial_width: *settings.default_panel_size,
|
||||
width: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ mod stories {
|
|||
.child(Story::title_for::<_, Panel<S>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
Panel::new(ScrollState::default()).child(
|
||||
Panel::new(cx).child(
|
||||
div()
|
||||
.overflow_y_scroll(ScrollState::default())
|
||||
.children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))),
|
||||
|
|
|
@ -86,7 +86,7 @@ mod stories {
|
|||
.child(Story::title_for::<_, ProjectPanel<S>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(
|
||||
Panel::new(ScrollState::default())
|
||||
Panel::new(cx)
|
||||
.child(ProjectPanel::new(ScrollState::default())),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ use gpui3::{view, Context, View};
|
|||
use crate::prelude::*;
|
||||
use crate::settings::user_settings;
|
||||
use crate::{
|
||||
random_players_with_call_status, Avatar, Button, Icon, IconButton, IconColor, MicStatus,
|
||||
PlayerWithCallStatus, ScreenShareStatus, ToolDivider, TrafficLights,
|
||||
Avatar, Button, Icon, IconButton, IconColor, MicStatus, PlayerStack, PlayerWithCallStatus,
|
||||
ScreenShareStatus, ToolDivider, TrafficLights,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -80,14 +80,9 @@ impl TitleBar {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
pub fn view(cx: &mut WindowContext, livestream: Option<Livestream>) -> View<Self> {
|
||||
view(
|
||||
cx.entity(|cx| {
|
||||
Self::new(cx).set_livestream(Some(Livestream {
|
||||
players: random_players_with_call_status(7),
|
||||
channel: Some("gpui2-ui".to_string()),
|
||||
}))
|
||||
}),
|
||||
cx.entity(|cx| Self::new(cx).set_livestream(livestream)),
|
||||
Self::render,
|
||||
)
|
||||
}
|
||||
|
@ -95,7 +90,7 @@ impl TitleBar {
|
|||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> {
|
||||
let color = ThemeColor::new(cx);
|
||||
let color = ThemeColor::new(cx);
|
||||
let setting = user_settings();
|
||||
let settings = user_settings(cx);
|
||||
|
||||
// let has_focus = cx.window_is_active();
|
||||
let has_focus = true;
|
||||
|
@ -127,13 +122,13 @@ impl TitleBar {
|
|||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.when(*setting.titlebar.show_project_owner, |this| {
|
||||
.when(*settings.titlebar.show_project_owner, |this| {
|
||||
this.child(Button::new("iamnbutler"))
|
||||
})
|
||||
.child(Button::new("zed"))
|
||||
.child(Button::new("nate/gpui2-ui-components")),
|
||||
)
|
||||
// .children(player_list.map(|p| PlayerStack::new(p)))
|
||||
.children(player_list.map(|p| PlayerStack::new(p)))
|
||||
.child(IconButton::new(Icon::Plus)),
|
||||
)
|
||||
.child(
|
||||
|
@ -204,7 +199,7 @@ mod stories {
|
|||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
view(
|
||||
cx.entity(|cx| Self {
|
||||
title_bar: TitleBar::view(cx),
|
||||
title_bar: TitleBar::view(cx, None),
|
||||
}),
|
||||
Self::render,
|
||||
)
|
||||
|
|
|
@ -1,13 +1,33 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use chrono::DateTime;
|
||||
use gpui3::{px, relative, view, Context, Size, View};
|
||||
use gpui3::{px, relative, rems, view, Context, Size, View};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
theme, v_stack, AssistantPanel, ChatMessage, ChatPanel, CollabPanel, EditorPane, Label,
|
||||
LanguageSelector, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel,
|
||||
SplitDirection, StatusBar, Terminal, TitleBar, Toast, ToastOrigin,
|
||||
static_livestream, theme, user_settings_mut, v_stack, AssistantPanel, Button, ChatMessage,
|
||||
ChatPanel, CollabPanel, EditorPane, FakeSettings, Label, LanguageSelector, Pane, PaneGroup,
|
||||
Panel, PanelAllowedSides, PanelSide, ProjectPanel, SettingValue, SplitDirection, StatusBar,
|
||||
Terminal, TitleBar, Toast, ToastOrigin,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Gpui2UiDebug {
|
||||
pub in_livestream: bool,
|
||||
pub enable_user_settings: bool,
|
||||
pub show_toast: bool,
|
||||
}
|
||||
|
||||
impl Default for Gpui2UiDebug {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
in_livestream: false,
|
||||
enable_user_settings: false,
|
||||
show_toast: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Workspace {
|
||||
title_bar: View<TitleBar>,
|
||||
|
@ -18,17 +38,19 @@ pub struct Workspace {
|
|||
show_assistant_panel: bool,
|
||||
show_notifications_panel: bool,
|
||||
show_terminal: bool,
|
||||
show_debug: bool,
|
||||
show_language_selector: bool,
|
||||
left_panel_scroll_state: ScrollState,
|
||||
right_panel_scroll_state: ScrollState,
|
||||
tab_bar_scroll_state: ScrollState,
|
||||
bottom_panel_scroll_state: ScrollState,
|
||||
debug: Gpui2UiDebug,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
title_bar: TitleBar::view(cx),
|
||||
title_bar: TitleBar::view(cx, None),
|
||||
editor_1: EditorPane::view(cx),
|
||||
show_project_panel: true,
|
||||
show_collab_panel: false,
|
||||
|
@ -36,11 +58,13 @@ impl Workspace {
|
|||
show_assistant_panel: false,
|
||||
show_terminal: true,
|
||||
show_language_selector: false,
|
||||
show_debug: false,
|
||||
show_notifications_panel: true,
|
||||
left_panel_scroll_state: ScrollState::default(),
|
||||
right_panel_scroll_state: ScrollState::default(),
|
||||
tab_bar_scroll_state: ScrollState::default(),
|
||||
bottom_panel_scroll_state: ScrollState::default(),
|
||||
debug: Gpui2UiDebug::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,6 +108,7 @@ impl Workspace {
|
|||
self.show_chat_panel = !self.show_chat_panel;
|
||||
|
||||
self.show_assistant_panel = false;
|
||||
self.show_notifications_panel = false;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -95,7 +120,8 @@ impl Workspace {
|
|||
pub fn toggle_notifications_panel(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_notifications_panel = !self.show_notifications_panel;
|
||||
|
||||
self.show_notifications_panel = false;
|
||||
self.show_chat_panel = false;
|
||||
self.show_assistant_panel = false;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -108,6 +134,7 @@ impl Workspace {
|
|||
self.show_assistant_panel = !self.show_assistant_panel;
|
||||
|
||||
self.show_chat_panel = false;
|
||||
self.show_notifications_panel = false;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -122,6 +149,35 @@ impl Workspace {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_debug(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_debug = !self.show_debug;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn debug_toggle_user_settings(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.debug.enable_user_settings = !self.debug.enable_user_settings;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn debug_toggle_livestream(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.debug.in_livestream = !self.debug.in_livestream;
|
||||
|
||||
self.title_bar = TitleBar::view(
|
||||
cx,
|
||||
Some(static_livestream()).filter(|_| self.debug.in_livestream),
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn debug_toggle_toast(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.debug.show_toast = !self.debug.show_toast;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
view(cx.entity(|cx| Self::new(cx)), Self::render)
|
||||
}
|
||||
|
@ -129,6 +185,20 @@ impl Workspace {
|
|||
pub fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> {
|
||||
let theme = theme(cx).clone();
|
||||
|
||||
// HACK: This should happen inside of `debug_toggle_user_settings`, but
|
||||
// we don't have `cx.global::<FakeSettings>()` in event handlers at the moment.
|
||||
// Need to talk with Nathan/Antonio about this.
|
||||
{
|
||||
let settings = user_settings_mut(cx);
|
||||
|
||||
if self.debug.enable_user_settings {
|
||||
settings.list_indent_depth = SettingValue::UserDefined(rems(0.5).into());
|
||||
settings.ui_scale = SettingValue::UserDefined(1.25);
|
||||
} else {
|
||||
*settings = FakeSettings::default();
|
||||
}
|
||||
}
|
||||
|
||||
let root_group = PaneGroup::new_panes(
|
||||
vec![Pane::new(
|
||||
ScrollState::default(),
|
||||
|
@ -165,7 +235,7 @@ impl Workspace {
|
|||
.border_color(theme.lowest.base.default.border)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new(self.left_panel_scroll_state.clone())
|
||||
Panel::new(cx)
|
||||
.side(PanelSide::Left)
|
||||
.child(ProjectPanel::new(ScrollState::default())),
|
||||
)
|
||||
|
@ -173,7 +243,7 @@ impl Workspace {
|
|||
)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new(self.left_panel_scroll_state.clone())
|
||||
Panel::new(cx)
|
||||
.child(CollabPanel::new(ScrollState::default()))
|
||||
.side(PanelSide::Left),
|
||||
)
|
||||
|
@ -183,20 +253,10 @@ impl Workspace {
|
|||
v_stack()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
// CSS Hack: Flex 1 has to have a set height to properly fill the space
|
||||
// Or it will give you a height of 0
|
||||
// Marshall: We may not need this anymore with `gpui3`. It seems to render
|
||||
// fine without it.
|
||||
.h_px()
|
||||
.child(root_group),
|
||||
)
|
||||
.child(div().flex().flex_1().child(root_group))
|
||||
.children(
|
||||
Some(
|
||||
Panel::new(self.bottom_panel_scroll_state.clone())
|
||||
Panel::new(cx)
|
||||
.child(Terminal::new())
|
||||
.allowed_sides(PanelAllowedSides::BottomOnly)
|
||||
.side(PanelSide::Bottom),
|
||||
|
@ -205,10 +265,8 @@ impl Workspace {
|
|||
),
|
||||
)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new(self.right_panel_scroll_state.clone())
|
||||
.side(PanelSide::Right)
|
||||
.child(ChatPanel::new(ScrollState::default()).messages(vec![
|
||||
Some(Panel::new(cx).side(PanelSide::Right).child(
|
||||
ChatPanel::new(ScrollState::default()).messages(vec![
|
||||
ChatMessage::new(
|
||||
"osiewicz".to_string(),
|
||||
"is this thing on?".to_string(),
|
||||
|
@ -223,45 +281,68 @@ impl Workspace {
|
|||
.unwrap()
|
||||
.naive_local(),
|
||||
),
|
||||
])),
|
||||
)
|
||||
]),
|
||||
))
|
||||
.filter(|_| self.is_chat_panel_open()),
|
||||
)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new(self.right_panel_scroll_state.clone())
|
||||
Panel::new(cx)
|
||||
.side(PanelSide::Right)
|
||||
.child(div().w_96().h_full().child("Notifications")),
|
||||
)
|
||||
.filter(|_| self.is_notifications_panel_open()),
|
||||
)
|
||||
.children(
|
||||
Some(
|
||||
Panel::new(self.right_panel_scroll_state.clone())
|
||||
.child(AssistantPanel::new()),
|
||||
)
|
||||
Some(Panel::new(cx).child(AssistantPanel::new()))
|
||||
.filter(|_| self.is_assistant_panel_open()),
|
||||
),
|
||||
)
|
||||
.child(StatusBar::new())
|
||||
.when(self.debug.show_toast, |this| {
|
||||
this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
|
||||
})
|
||||
.children(
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(50.))
|
||||
.left(px(640.))
|
||||
.z_index(999)
|
||||
.z_index(8)
|
||||
.child(LanguageSelector::new()),
|
||||
)
|
||||
.filter(|_| self.is_language_selector_open()),
|
||||
)
|
||||
.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
|
||||
// .child(Toast::new(ToastOrigin::BottomRight).child(Label::new("Another toast")))
|
||||
// .child(NotificationToast::new(
|
||||
// "Can't pull changes from origin",
|
||||
// "Your local branch is behind the remote branch. Please pull the latest changes before pushing.",
|
||||
// Button::new("Stash & Switch").variant(ButtonVariant::Filled),
|
||||
// ).secondary_action(Button::new("Cancel")))
|
||||
.z_index(8)
|
||||
// Debug
|
||||
.child(
|
||||
v_stack()
|
||||
.z_index(9)
|
||||
.absolute()
|
||||
.bottom_10()
|
||||
.left_1_4()
|
||||
.w_40()
|
||||
.gap_2()
|
||||
.when(self.show_debug, |this| {
|
||||
this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
|
||||
Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
|
||||
))
|
||||
.child(
|
||||
Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
|
||||
|workspace, cx| workspace.debug_toggle_toast(cx),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
|
||||
|workspace, cx| workspace.debug_toggle_livestream(cx),
|
||||
)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::<Workspace>::new("Toggle Debug")
|
||||
.on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -149,11 +149,11 @@ impl<S: 'static + Send + Sync + Clone> Button<S> {
|
|||
fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
|
||||
let icon_color = self.icon_color();
|
||||
let border_color = self.border_color(cx);
|
||||
let setting = user_settings();
|
||||
let settings = user_settings(cx);
|
||||
|
||||
let mut el = h_stack()
|
||||
.p_1()
|
||||
.text_size(ui_size(1.))
|
||||
.text_size(ui_size(cx, 1.))
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_color(border_color)
|
||||
|
|
|
@ -180,8 +180,8 @@ impl<S: 'static + Send + Sync> IconElement<S> {
|
|||
let color = ThemeColor::new(cx);
|
||||
let fill = self.color.color(cx);
|
||||
let svg_size = match self.size {
|
||||
IconSize::Small => ui_size(12. / 14.),
|
||||
IconSize::Medium => ui_size(15. / 14.),
|
||||
IconSize::Small => ui_size(cx, 12. / 14.),
|
||||
IconSize::Medium => ui_size(cx, 15. / 14.),
|
||||
};
|
||||
|
||||
svg()
|
||||
|
|
|
@ -90,6 +90,7 @@ impl<S: 'static + Send + Sync + Clone> Input<S> {
|
|||
});
|
||||
|
||||
div()
|
||||
.id("input")
|
||||
.h_7()
|
||||
.w_full()
|
||||
.px_2()
|
||||
|
@ -97,7 +98,7 @@ impl<S: 'static + Send + Sync + Clone> Input<S> {
|
|||
.border_color(system_color.transparent)
|
||||
.bg(input_bg)
|
||||
.hover(|style| style.bg(input_hover_bg))
|
||||
// .active(|style| style.bg(input_active_bg))
|
||||
.active(|style| style.bg(input_active_bg))
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(
|
||||
|
|
|
@ -98,7 +98,7 @@ impl<S: 'static + Send + Sync + Clone> Label<S> {
|
|||
.bg(LabelColor::Hidden.hsla(cx)),
|
||||
)
|
||||
})
|
||||
.text_size(ui_size(1.))
|
||||
.text_size(ui_size(cx, 1.))
|
||||
.when(self.line_height_style == LineHeightStyle::UILabel, |this| {
|
||||
this.line_height(relative(1.))
|
||||
})
|
||||
|
|
|
@ -14,6 +14,14 @@ pub use elements::*;
|
|||
pub use prelude::*;
|
||||
pub use static_data::*;
|
||||
|
||||
// This needs to be fully qualified with `crate::` otherwise we get a panic
|
||||
// at:
|
||||
// thread '<unnamed>' panicked at crates/gpui3/src/platform/mac/platform.rs:66:81:
|
||||
// called `Option::unwrap()` on a `None` value
|
||||
//
|
||||
// AFAICT this is something to do with conflicting names between crates and modules that
|
||||
// interfaces with declaring the `ClassDecl`.
|
||||
pub use crate::settings::*;
|
||||
pub use crate::theme::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
pub use gpui3::{
|
||||
div, Click, Element, Hover, IntoAnyElement, ParentElement, ScrollState, SharedString, Styled,
|
||||
ViewContext, WindowContext,
|
||||
div, Active, Click, Element, Hover, IntoAnyElement, ParentElement, ScrollState, SharedString,
|
||||
Styled, ViewContext, WindowContext,
|
||||
};
|
||||
|
||||
use crate::settings::user_settings;
|
||||
|
@ -278,12 +278,12 @@ impl HighlightColor {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn ui_size(size: f32) -> Rems {
|
||||
pub fn ui_size(cx: &mut WindowContext, size: f32) -> Rems {
|
||||
const UI_SCALE_RATIO: f32 = 0.875;
|
||||
|
||||
let setting = user_settings();
|
||||
let settings = user_settings(cx);
|
||||
|
||||
rems(*setting.ui_scale * UI_SCALE_RATIO * size)
|
||||
rems(*settings.ui_scale * UI_SCALE_RATIO * size)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui3::{rems, AbsoluteLength};
|
||||
use gpui3::{
|
||||
rems, AbsoluteLength, AnyElement, BorrowAppContext, Bounds, Interactive, LayoutId, Pixels,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
use crate::DisclosureControlStyle;
|
||||
use crate::prelude::*;
|
||||
|
||||
// This is a fake static example of user settings overriding the default settings
|
||||
pub fn user_settings() -> Settings {
|
||||
let mut settings = Settings::default();
|
||||
settings.list_indent_depth = SettingValue::UserDefined(rems(0.5).into());
|
||||
// settings.ui_scale = SettingValue::UserDefined(2.);
|
||||
settings
|
||||
/// Returns the user settings.
|
||||
pub fn user_settings(cx: &WindowContext) -> FakeSettings {
|
||||
cx.global::<FakeSettings>().clone()
|
||||
}
|
||||
|
||||
pub fn user_settings_mut<'cx>(cx: &'cx mut WindowContext) -> &'cx mut FakeSettings {
|
||||
cx.global_mut::<FakeSettings>()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -48,7 +53,7 @@ impl Default for TitlebarSettings {
|
|||
|
||||
// These should be merged into settings
|
||||
#[derive(Clone)]
|
||||
pub struct Settings {
|
||||
pub struct FakeSettings {
|
||||
pub default_panel_size: SettingValue<AbsoluteLength>,
|
||||
pub list_disclosure_style: SettingValue<DisclosureControlStyle>,
|
||||
pub list_indent_depth: SettingValue<AbsoluteLength>,
|
||||
|
@ -56,7 +61,7 @@ pub struct Settings {
|
|||
pub ui_scale: SettingValue<f32>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
impl Default for FakeSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
titlebar: TitlebarSettings::default(),
|
||||
|
@ -68,4 +73,108 @@ impl Default for Settings {
|
|||
}
|
||||
}
|
||||
|
||||
impl Settings {}
|
||||
impl FakeSettings {}
|
||||
|
||||
pub fn with_settings<E, F>(
|
||||
settings: FakeSettings,
|
||||
cx: &mut ViewContext<E::ViewState>,
|
||||
build_child: F,
|
||||
) -> WithSettings<E>
|
||||
where
|
||||
E: Element,
|
||||
F: FnOnce(&mut ViewContext<E::ViewState>) -> E,
|
||||
{
|
||||
let child = cx.with_global(settings.clone(), |cx| build_child(cx));
|
||||
WithSettings { settings, child }
|
||||
}
|
||||
|
||||
pub struct WithSettings<E> {
|
||||
pub(crate) settings: FakeSettings,
|
||||
pub(crate) child: E,
|
||||
}
|
||||
|
||||
impl<E> IntoAnyElement<E::ViewState> for WithSettings<E>
|
||||
where
|
||||
E: Element,
|
||||
{
|
||||
fn into_any(self) -> AnyElement<E::ViewState> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Element> Element for WithSettings<E> {
|
||||
type ViewState = E::ViewState;
|
||||
type ElementState = E::ElementState;
|
||||
|
||||
fn id(&self) -> Option<gpui3::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
view_state: &mut Self::ViewState,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) -> Self::ElementState {
|
||||
cx.with_global(self.settings.clone(), |cx| {
|
||||
self.child.initialize(view_state, element_state, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut E::ViewState,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<E::ViewState>,
|
||||
) -> LayoutId
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
cx.with_global(self.settings.clone(), |cx| {
|
||||
self.child.layout(view_state, element_state, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
view_state: &mut Self::ViewState,
|
||||
frame_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<Self::ViewState>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
cx.with_global(self.settings.clone(), |cx| {
|
||||
self.child.paint(bounds, view_state, frame_state, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Element + Interactive> Interactive for WithSettings<E> {
|
||||
fn listeners(&mut self) -> &mut gpui3::EventListeners<Self::ViewState> {
|
||||
self.child.listeners()
|
||||
}
|
||||
|
||||
fn on_mouse_down(
|
||||
mut self,
|
||||
button: gpui3::MouseButton,
|
||||
handler: impl Fn(&mut Self::ViewState, &gpui3::MouseDownEvent, &mut ViewContext<Self::ViewState>)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
println!("WithSettings on_mouse_down");
|
||||
|
||||
let settings = self.settings.clone();
|
||||
|
||||
self.listeners()
|
||||
.mouse_down
|
||||
.push(Arc::new(move |view, event, bounds, phase, cx| {
|
||||
cx.with_global(settings.clone(), |cx| handler(view, event, cx))
|
||||
}));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue