Add support for resizing splits and docks, as well as utilities (#3595)
Making this PR to upstream changes to util and GPUI2, resizing exists but isn't working yet. Release Notes: - N/A
This commit is contained in:
commit
927d18b0f3
20 changed files with 976 additions and 512 deletions
|
@ -391,9 +391,9 @@ impl EditorElement {
|
||||||
let mut click_count = event.click_count;
|
let mut click_count = event.click_count;
|
||||||
let modifiers = event.modifiers;
|
let modifiers = event.modifiers;
|
||||||
|
|
||||||
if gutter_bounds.contains_point(&event.position) {
|
if gutter_bounds.contains(&event.position) {
|
||||||
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
|
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
|
||||||
} else if !text_bounds.contains_point(&event.position) {
|
} else if !text_bounds.contains(&event.position) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if !cx.was_top_layer(&event.position, stacking_order) {
|
if !cx.was_top_layer(&event.position, stacking_order) {
|
||||||
|
@ -439,7 +439,7 @@ impl EditorElement {
|
||||||
text_bounds: Bounds<Pixels>,
|
text_bounds: Bounds<Pixels>,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) {
|
) {
|
||||||
if !text_bounds.contains_point(&event.position) {
|
if !text_bounds.contains(&event.position) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let point_for_position = position_map.point_for_position(text_bounds, event.position);
|
let point_for_position = position_map.point_for_position(text_bounds, event.position);
|
||||||
|
@ -469,7 +469,7 @@ impl EditorElement {
|
||||||
|
|
||||||
if !pending_nonempty_selections
|
if !pending_nonempty_selections
|
||||||
&& event.modifiers.command
|
&& event.modifiers.command
|
||||||
&& text_bounds.contains_point(&event.position)
|
&& text_bounds.contains(&event.position)
|
||||||
&& cx.was_top_layer(&event.position, stacking_order)
|
&& cx.was_top_layer(&event.position, stacking_order)
|
||||||
{
|
{
|
||||||
let point = position_map.point_for_position(text_bounds, event.position);
|
let point = position_map.point_for_position(text_bounds, event.position);
|
||||||
|
@ -531,8 +531,8 @@ impl EditorElement {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_hovered = text_bounds.contains_point(&event.position);
|
let text_hovered = text_bounds.contains(&event.position);
|
||||||
let gutter_hovered = gutter_bounds.contains_point(&event.position);
|
let gutter_hovered = gutter_bounds.contains(&event.position);
|
||||||
let was_top = cx.was_top_layer(&event.position, stacking_order);
|
let was_top = cx.was_top_layer(&event.position, stacking_order);
|
||||||
|
|
||||||
editor.set_gutter_hovered(gutter_hovered, cx);
|
editor.set_gutter_hovered(gutter_hovered, cx);
|
||||||
|
@ -900,7 +900,7 @@ impl EditorElement {
|
||||||
bounds: text_bounds,
|
bounds: text_bounds,
|
||||||
}),
|
}),
|
||||||
|cx| {
|
|cx| {
|
||||||
if text_bounds.contains_point(&cx.mouse_position()) {
|
if text_bounds.contains(&cx.mouse_position()) {
|
||||||
if self
|
if self
|
||||||
.editor
|
.editor
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -966,7 +966,7 @@ impl EditorElement {
|
||||||
|fold_element_state, cx| {
|
|fold_element_state, cx| {
|
||||||
if fold_element_state.is_active() {
|
if fold_element_state.is_active() {
|
||||||
gpui::blue()
|
gpui::blue()
|
||||||
} else if fold_bounds.contains_point(&cx.mouse_position()) {
|
} else if fold_bounds.contains(&cx.mouse_position()) {
|
||||||
gpui::black()
|
gpui::black()
|
||||||
} else {
|
} else {
|
||||||
gpui::red()
|
gpui::red()
|
||||||
|
@ -1377,7 +1377,7 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mouse_position = cx.mouse_position();
|
let mouse_position = cx.mouse_position();
|
||||||
if track_bounds.contains_point(&mouse_position) {
|
if track_bounds.contains(&mouse_position) {
|
||||||
cx.set_cursor_style(CursorStyle::Arrow);
|
cx.set_cursor_style(CursorStyle::Arrow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1405,7 +1405,7 @@ impl EditorElement {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
} else {
|
} else {
|
||||||
editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
|
editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
|
||||||
if track_bounds.contains_point(&event.position) {
|
if track_bounds.contains(&event.position) {
|
||||||
editor.scroll_manager.show_scrollbar(cx);
|
editor.scroll_manager.show_scrollbar(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1428,7 +1428,7 @@ impl EditorElement {
|
||||||
let editor = self.editor.clone();
|
let editor = self.editor.clone();
|
||||||
move |event: &MouseDownEvent, phase, cx| {
|
move |event: &MouseDownEvent, phase, cx| {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
if track_bounds.contains_point(&event.position) {
|
if track_bounds.contains(&event.position) {
|
||||||
editor.scroll_manager.set_is_dragging_scrollbar(true, cx);
|
editor.scroll_manager.set_is_dragging_scrollbar(true, cx);
|
||||||
|
|
||||||
let y = event.position.y;
|
let y = event.position.y;
|
||||||
|
@ -2502,10 +2502,10 @@ impl EditorElement {
|
||||||
gutter_bounds,
|
gutter_bounds,
|
||||||
&stacking_order,
|
&stacking_order,
|
||||||
cx,
|
cx,
|
||||||
)
|
);
|
||||||
}),
|
}),
|
||||||
MouseButton::Right => editor.update(cx, |editor, cx| {
|
MouseButton::Right => editor.update(cx, |editor, cx| {
|
||||||
Self::mouse_right_down(editor, event, &position_map, text_bounds, cx)
|
Self::mouse_right_down(editor, event, &position_map, text_bounds, cx);
|
||||||
}),
|
}),
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
|
|
|
@ -513,6 +513,10 @@ impl AppContext {
|
||||||
self.platform.path_for_auxiliary_executable(name)
|
self.platform.path_for_auxiliary_executable(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn double_click_interval(&self) -> Duration {
|
||||||
|
self.platform.double_click_interval()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn prompt_for_paths(
|
pub fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
options: PathPromptOptions,
|
options: PathPromptOptions,
|
||||||
|
|
|
@ -761,7 +761,7 @@ pub struct InteractiveBounds {
|
||||||
|
|
||||||
impl InteractiveBounds {
|
impl InteractiveBounds {
|
||||||
pub fn visibly_contains(&self, point: &Point<Pixels>, cx: &WindowContext) -> bool {
|
pub fn visibly_contains(&self, point: &Point<Pixels>, cx: &WindowContext) -> bool {
|
||||||
self.bounds.contains_point(point) && cx.was_top_layer(&point, &self.stacking_order)
|
self.bounds.contains(point) && cx.was_top_layer(&point, &self.stacking_order)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -860,10 +860,10 @@ impl Interactivity {
|
||||||
.and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
|
.and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
|
||||||
|
|
||||||
if let Some(group_bounds) = hover_group_bounds {
|
if let Some(group_bounds) = hover_group_bounds {
|
||||||
let hovered = group_bounds.contains_point(&cx.mouse_position());
|
let hovered = group_bounds.contains(&cx.mouse_position());
|
||||||
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
||||||
if phase == DispatchPhase::Capture {
|
if phase == DispatchPhase::Capture {
|
||||||
if group_bounds.contains_point(&event.position) != hovered {
|
if group_bounds.contains(&event.position) != hovered {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -875,10 +875,10 @@ impl Interactivity {
|
||||||
|| cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
|
|| cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
|
||||||
{
|
{
|
||||||
let bounds = bounds.intersect(&cx.content_mask().bounds);
|
let bounds = bounds.intersect(&cx.content_mask().bounds);
|
||||||
let hovered = bounds.contains_point(&cx.mouse_position());
|
let hovered = bounds.contains(&cx.mouse_position());
|
||||||
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
||||||
if phase == DispatchPhase::Capture {
|
if phase == DispatchPhase::Capture {
|
||||||
if bounds.contains_point(&event.position) != hovered {
|
if bounds.contains(&event.position) != hovered {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1068,8 +1068,8 @@ impl Interactivity {
|
||||||
let interactive_bounds = interactive_bounds.clone();
|
let interactive_bounds = interactive_bounds.clone();
|
||||||
cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| {
|
cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| {
|
||||||
if phase == DispatchPhase::Bubble {
|
if phase == DispatchPhase::Bubble {
|
||||||
let group = active_group_bounds
|
let group =
|
||||||
.map_or(false, |bounds| bounds.contains_point(&down.position));
|
active_group_bounds.map_or(false, |bounds| bounds.contains(&down.position));
|
||||||
let element = interactive_bounds.visibly_contains(&down.position, cx);
|
let element = interactive_bounds.visibly_contains(&down.position, cx);
|
||||||
if group || element {
|
if group || element {
|
||||||
*active_state.borrow_mut() = ElementClickedState { group, element };
|
*active_state.borrow_mut() = ElementClickedState { group, element };
|
||||||
|
@ -1183,7 +1183,7 @@ impl Interactivity {
|
||||||
let mouse_position = cx.mouse_position();
|
let mouse_position = cx.mouse_position();
|
||||||
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
||||||
if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
|
if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
|
||||||
if group_bounds.contains_point(&mouse_position)
|
if group_bounds.contains(&mouse_position)
|
||||||
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||||
{
|
{
|
||||||
style.refine(&group_hover.style);
|
style.refine(&group_hover.style);
|
||||||
|
@ -1193,7 +1193,7 @@ impl Interactivity {
|
||||||
if self.hover_style.is_some() {
|
if self.hover_style.is_some() {
|
||||||
if bounds
|
if bounds
|
||||||
.intersect(&cx.content_mask().bounds)
|
.intersect(&cx.content_mask().bounds)
|
||||||
.contains_point(&mouse_position)
|
.contains(&mouse_position)
|
||||||
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||||
{
|
{
|
||||||
style.refine(&self.hover_style);
|
style.refine(&self.hover_style);
|
||||||
|
@ -1204,7 +1204,7 @@ impl Interactivity {
|
||||||
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
||||||
if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
|
if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
|
||||||
if *state_type == drag.view.entity_type()
|
if *state_type == drag.view.entity_type()
|
||||||
&& group_bounds.contains_point(&mouse_position)
|
&& group_bounds.contains(&mouse_position)
|
||||||
{
|
{
|
||||||
style.refine(&group_drag_style.style);
|
style.refine(&group_drag_style.style);
|
||||||
}
|
}
|
||||||
|
@ -1215,7 +1215,7 @@ impl Interactivity {
|
||||||
if *state_type == drag.view.entity_type()
|
if *state_type == drag.view.entity_type()
|
||||||
&& bounds
|
&& bounds
|
||||||
.intersect(&cx.content_mask().bounds)
|
.intersect(&cx.content_mask().bounds)
|
||||||
.contains_point(&mouse_position)
|
.contains(&mouse_position)
|
||||||
{
|
{
|
||||||
style.refine(drag_over_style);
|
style.refine(drag_over_style);
|
||||||
}
|
}
|
||||||
|
|
|
@ -253,7 +253,7 @@ impl TextState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
||||||
if !bounds.contains_point(&position) {
|
if !bounds.contains(&position) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,12 @@ where
|
||||||
T: 'static,
|
T: 'static,
|
||||||
E: 'static + Debug,
|
E: 'static + Debug,
|
||||||
{
|
{
|
||||||
|
#[track_caller]
|
||||||
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
||||||
cx.foreground_executor().spawn(self.log_err()).detach();
|
let location = core::panic::Location::caller();
|
||||||
|
cx.foreground_executor()
|
||||||
|
.spawn(self.log_tracked_err(*location))
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,62 @@ use std::{
|
||||||
ops::{Add, Div, Mul, MulAssign, Sub},
|
ops::{Add, Div, Mul, MulAssign, Sub},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub enum Axis {
|
||||||
|
Vertical,
|
||||||
|
Horizontal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Axis {
|
||||||
|
pub fn invert(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Axis::Vertical => Axis::Horizontal,
|
||||||
|
Axis::Horizontal => Axis::Vertical,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Along {
|
||||||
|
type Unit;
|
||||||
|
|
||||||
|
fn along(&self, axis: Axis) -> Self::Unit;
|
||||||
|
|
||||||
|
fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sqlez::bindable::StaticColumnCount for Axis {}
|
||||||
|
impl sqlez::bindable::Bind for Axis {
|
||||||
|
fn bind(
|
||||||
|
&self,
|
||||||
|
statement: &sqlez::statement::Statement,
|
||||||
|
start_index: i32,
|
||||||
|
) -> anyhow::Result<i32> {
|
||||||
|
match self {
|
||||||
|
Axis::Horizontal => "Horizontal",
|
||||||
|
Axis::Vertical => "Vertical",
|
||||||
|
}
|
||||||
|
.bind(statement, start_index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sqlez::bindable::Column for Axis {
|
||||||
|
fn column(
|
||||||
|
statement: &mut sqlez::statement::Statement,
|
||||||
|
start_index: i32,
|
||||||
|
) -> anyhow::Result<(Self, i32)> {
|
||||||
|
String::column(statement, start_index).and_then(|(axis_text, next_index)| {
|
||||||
|
Ok((
|
||||||
|
match axis_text.as_str() {
|
||||||
|
"Horizontal" => Axis::Horizontal,
|
||||||
|
"Vertical" => Axis::Vertical,
|
||||||
|
_ => anyhow::bail!("Stored serialized item kind is incorrect"),
|
||||||
|
},
|
||||||
|
next_index,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Describes a location in a 2D cartesian coordinate space.
|
/// Describes a location in a 2D cartesian coordinate space.
|
||||||
///
|
///
|
||||||
/// It holds two public fields, `x` and `y`, which represent the coordinates in the space.
|
/// It holds two public fields, `x` and `y`, which represent the coordinates in the space.
|
||||||
|
@ -96,6 +152,30 @@ impl<T: Clone + Debug + Default> Point<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Debug + Default> Along for Point<T> {
|
||||||
|
type Unit = T;
|
||||||
|
|
||||||
|
fn along(&self, axis: Axis) -> T {
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => self.x.clone(),
|
||||||
|
Axis::Vertical => self.y.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Point<T> {
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => Point {
|
||||||
|
x: f(self.x.clone()),
|
||||||
|
y: self.y.clone(),
|
||||||
|
},
|
||||||
|
Axis::Vertical => Point {
|
||||||
|
x: self.x.clone(),
|
||||||
|
y: f(self.y.clone()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Point<Pixels> {
|
impl Point<Pixels> {
|
||||||
/// Scales the point by a given factor, which is typically derived from the resolution
|
/// Scales the point by a given factor, which is typically derived from the resolution
|
||||||
/// of a target display to ensure proper sizing of UI elements.
|
/// of a target display to ensure proper sizing of UI elements.
|
||||||
|
@ -373,6 +453,34 @@ impl Size<Pixels> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Along for Size<T>
|
||||||
|
where
|
||||||
|
T: Clone + Default + Debug,
|
||||||
|
{
|
||||||
|
type Unit = T;
|
||||||
|
|
||||||
|
fn along(&self, axis: Axis) -> T {
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => self.width.clone(),
|
||||||
|
Axis::Vertical => self.height.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the value of this size along the given axis.
|
||||||
|
fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Self {
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => Size {
|
||||||
|
width: f(self.width.clone()),
|
||||||
|
height: self.height.clone(),
|
||||||
|
},
|
||||||
|
Axis::Vertical => Size {
|
||||||
|
width: self.width.clone(),
|
||||||
|
height: f(self.height.clone()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Size<T>
|
impl<T> Size<T>
|
||||||
where
|
where
|
||||||
T: PartialOrd + Clone + Default + Debug,
|
T: PartialOrd + Clone + Default + Debug,
|
||||||
|
@ -992,7 +1100,7 @@ where
|
||||||
/// assert!(bounds.contains_point(&inside_point));
|
/// assert!(bounds.contains_point(&inside_point));
|
||||||
/// assert!(!bounds.contains_point(&outside_point));
|
/// assert!(!bounds.contains_point(&outside_point));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn contains_point(&self, point: &Point<T>) -> bool {
|
pub fn contains(&self, point: &Point<T>) -> bool {
|
||||||
point.x >= self.origin.x
|
point.x >= self.origin.x
|
||||||
&& point.x <= self.origin.x.clone() + self.size.width.clone()
|
&& point.x <= self.origin.x.clone() + self.size.width.clone()
|
||||||
&& point.y >= self.origin.y
|
&& point.y >= self.origin.y
|
||||||
|
|
|
@ -131,6 +131,12 @@ pub struct MouseMoveEvent {
|
||||||
pub modifiers: Modifiers,
|
pub modifiers: Modifiers,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MouseMoveEvent {
|
||||||
|
pub fn dragging(&self) -> bool {
|
||||||
|
self.pressed_button == Some(MouseButton::Left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ScrollWheelEvent {
|
pub struct ScrollWheelEvent {
|
||||||
pub position: Point<Pixels>,
|
pub position: Point<Pixels>,
|
||||||
|
|
|
@ -105,6 +105,7 @@ pub(crate) trait Platform: 'static {
|
||||||
fn app_version(&self) -> Result<SemanticVersion>;
|
fn app_version(&self) -> Result<SemanticVersion>;
|
||||||
fn app_path(&self) -> Result<PathBuf>;
|
fn app_path(&self) -> Result<PathBuf>;
|
||||||
fn local_timezone(&self) -> UtcOffset;
|
fn local_timezone(&self) -> UtcOffset;
|
||||||
|
fn double_click_interval(&self) -> Duration;
|
||||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
||||||
|
|
||||||
fn set_cursor_style(&self, style: CursorStyle);
|
fn set_cursor_style(&self, style: CursorStyle);
|
||||||
|
|
|
@ -49,6 +49,7 @@ use std::{
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
slice, str,
|
slice, str,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
use time::UtcOffset;
|
use time::UtcOffset;
|
||||||
|
|
||||||
|
@ -657,6 +658,13 @@ impl Platform for MacPlatform {
|
||||||
"macOS"
|
"macOS"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn double_click_interval(&self) -> Duration {
|
||||||
|
unsafe {
|
||||||
|
let double_click_interval: f64 = msg_send![class!(NSEvent), doubleClickInterval];
|
||||||
|
Duration::from_secs_f64(double_click_interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn os_version(&self) -> Result<SemanticVersion> {
|
fn os_version(&self) -> Result<SemanticVersion> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let process_info = NSProcessInfo::processInfo(nil);
|
let process_info = NSProcessInfo::processInfo(nil);
|
||||||
|
|
|
@ -12,6 +12,7 @@ use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
rc::{Rc, Weak},
|
rc::{Rc, Weak},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct TestPlatform {
|
pub struct TestPlatform {
|
||||||
|
@ -274,4 +275,8 @@ impl Platform for TestPlatform {
|
||||||
fn delete_credentials(&self, _url: &str) -> Result<()> {
|
fn delete_credentials(&self, _url: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn double_click_interval(&self) -> std::time::Duration {
|
||||||
|
Duration::from_millis(500)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -477,3 +477,12 @@ impl From<Pixels> for AvailableSpace {
|
||||||
AvailableSpace::Definite(pixels)
|
AvailableSpace::Definite(pixels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Size<Pixels>> for Size<AvailableSpace> {
|
||||||
|
fn from(size: Size<Pixels>) -> Self {
|
||||||
|
Size {
|
||||||
|
width: AvailableSpace::Definite(size.width),
|
||||||
|
height: AvailableSpace::Definite(size.height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -60,6 +60,16 @@ pub enum DispatchPhase {
|
||||||
Capture,
|
Capture,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DispatchPhase {
|
||||||
|
pub fn bubble(self) -> bool {
|
||||||
|
self == DispatchPhase::Bubble
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capture(self) -> bool {
|
||||||
|
self == DispatchPhase::Capture
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
|
type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
|
||||||
type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
|
type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
|
||||||
type AnyFocusListener = Box<dyn Fn(&FocusEvent, &mut WindowContext) + 'static>;
|
type AnyFocusListener = Box<dyn Fn(&FocusEvent, &mut WindowContext) + 'static>;
|
||||||
|
@ -866,7 +876,7 @@ impl<'a> WindowContext<'a> {
|
||||||
/// same layer as the given stacking order.
|
/// same layer as the given stacking order.
|
||||||
pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool {
|
pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool {
|
||||||
for (stack, bounds) in self.window.rendered_frame.depth_map.iter() {
|
for (stack, bounds) in self.window.rendered_frame.depth_map.iter() {
|
||||||
if bounds.contains_point(point) {
|
if bounds.contains(point) {
|
||||||
return level.starts_with(stack) || stack.starts_with(level);
|
return level.starts_with(stack) || stack.starts_with(level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
|
||||||
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||||
if phase == DispatchPhase::Bubble
|
if phase == DispatchPhase::Bubble
|
||||||
&& event.button == MouseButton::Right
|
&& event.button == MouseButton::Right
|
||||||
&& bounds.contains_point(&event.position)
|
&& bounds.contains(&event.position)
|
||||||
{
|
{
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.prevent_default();
|
cx.prevent_default();
|
||||||
|
|
|
@ -184,6 +184,11 @@ pub trait TryFutureExt {
|
||||||
fn log_err(self) -> LogErrorFuture<Self>
|
fn log_err(self) -> LogErrorFuture<Self>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
|
||||||
|
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
fn warn_on_err(self) -> LogErrorFuture<Self>
|
fn warn_on_err(self) -> LogErrorFuture<Self>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
@ -197,18 +202,29 @@ where
|
||||||
F: Future<Output = Result<T, E>>,
|
F: Future<Output = Result<T, E>>,
|
||||||
E: std::fmt::Debug,
|
E: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
|
#[track_caller]
|
||||||
fn log_err(self) -> LogErrorFuture<Self>
|
fn log_err(self) -> LogErrorFuture<Self>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
LogErrorFuture(self, log::Level::Error)
|
let location = Location::caller();
|
||||||
|
LogErrorFuture(self, log::Level::Error, *location)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
LogErrorFuture(self, log::Level::Error, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
fn warn_on_err(self) -> LogErrorFuture<Self>
|
fn warn_on_err(self) -> LogErrorFuture<Self>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
LogErrorFuture(self, log::Level::Warn)
|
let location = Location::caller();
|
||||||
|
LogErrorFuture(self, log::Level::Warn, *location)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unwrap(self) -> UnwrapFuture<Self>
|
fn unwrap(self) -> UnwrapFuture<Self>
|
||||||
|
@ -219,7 +235,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LogErrorFuture<F>(F, log::Level);
|
pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
|
||||||
|
|
||||||
impl<F, T, E> Future for LogErrorFuture<F>
|
impl<F, T, E> Future for LogErrorFuture<F>
|
||||||
where
|
where
|
||||||
|
@ -230,12 +246,19 @@ where
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||||
let level = self.1;
|
let level = self.1;
|
||||||
|
let location = self.2;
|
||||||
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
|
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
|
||||||
match inner.poll(cx) {
|
match inner.poll(cx) {
|
||||||
Poll::Ready(output) => Poll::Ready(match output {
|
Poll::Ready(output) => Poll::Ready(match output {
|
||||||
Ok(output) => Some(output),
|
Ok(output) => Some(output),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::log!(level, "{:?}", error);
|
log::log!(
|
||||||
|
level,
|
||||||
|
"{}:{}: {:?}",
|
||||||
|
location.file(),
|
||||||
|
location.line(),
|
||||||
|
error
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::{status_bar::StatusItemView, Axis, Workspace};
|
use crate::{status_bar::StatusItemView, Workspace};
|
||||||
|
use crate::{DockClickReset, DockDragState};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, Action, AnchorCorner, AnyView, AppContext, Div, Entity, EntityId, EventEmitter,
|
div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
|
||||||
FocusHandle, FocusableView, IntoElement, ParentElement, Render, SharedString, Styled,
|
EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
|
||||||
Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
|
SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -364,7 +365,7 @@ impl Dock {
|
||||||
this.set_open(false, cx);
|
this.set_open(false, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PanelEvent::Focus => todo!(),
|
PanelEvent::Focus => {}
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -485,6 +486,48 @@ impl Render for Dock {
|
||||||
if let Some(entry) = self.visible_entry() {
|
if let Some(entry) = self.visible_entry() {
|
||||||
let size = entry.panel.size(cx);
|
let size = entry.panel.size(cx);
|
||||||
|
|
||||||
|
let mut pre_resize_handle = None;
|
||||||
|
let mut post_resize_handle = None;
|
||||||
|
let position = self.position;
|
||||||
|
let handler = div()
|
||||||
|
.id("resize-handle")
|
||||||
|
.bg(cx.theme().colors().border)
|
||||||
|
.on_mouse_down(gpui::MouseButton::Left, move |_, cx| {
|
||||||
|
cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(position))
|
||||||
|
})
|
||||||
|
.on_click(cx.listener(|v, e: &ClickEvent, cx| {
|
||||||
|
if e.down.button == MouseButton::Left {
|
||||||
|
cx.update_global(|state: &mut DockClickReset, cx| {
|
||||||
|
if state.0.is_some() {
|
||||||
|
state.0 = None;
|
||||||
|
v.resize_active_panel(None, cx)
|
||||||
|
} else {
|
||||||
|
let double_click = cx.double_click_interval();
|
||||||
|
let timer = cx.background_executor().timer(double_click);
|
||||||
|
state.0 = Some(cx.spawn(|_, mut cx| async move {
|
||||||
|
timer.await;
|
||||||
|
cx.update_global(|state: &mut DockClickReset, cx| {
|
||||||
|
state.0 = None;
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
match self.position() {
|
||||||
|
DockPosition::Left => {
|
||||||
|
post_resize_handle = Some(handler.w_1().h_full().cursor_col_resize())
|
||||||
|
}
|
||||||
|
DockPosition::Bottom => {
|
||||||
|
pre_resize_handle = Some(handler.w_full().h_1().cursor_row_resize())
|
||||||
|
}
|
||||||
|
DockPosition::Right => {
|
||||||
|
pre_resize_handle = Some(handler.w_full().h_1().cursor_col_resize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.map(|this| match self.position().axis() {
|
.map(|this| match self.position().axis() {
|
||||||
|
@ -496,7 +539,9 @@ impl Render for Dock {
|
||||||
DockPosition::Right => this.border_l(),
|
DockPosition::Right => this.border_l(),
|
||||||
DockPosition::Bottom => this.border_t(),
|
DockPosition::Bottom => this.border_t(),
|
||||||
})
|
})
|
||||||
|
.children(pre_resize_handle)
|
||||||
.child(entry.panel.to_any())
|
.child(entry.panel.to_any())
|
||||||
|
.children(post_resize_handle)
|
||||||
} else {
|
} else {
|
||||||
div()
|
div()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1046,10 +1046,11 @@ impl Pane {
|
||||||
{
|
{
|
||||||
pane.remove_item(item_ix, false, cx);
|
pane.remove_item(item_ix, false, cx);
|
||||||
}
|
}
|
||||||
})?;
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pane.update(&mut cx, |_, cx| cx.notify())?;
|
pane.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -6,12 +6,12 @@ use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
||||||
use gpui::WindowBounds;
|
use gpui::{Axis, WindowBounds};
|
||||||
|
|
||||||
use util::{unzip_option, ResultExt};
|
use util::{unzip_option, ResultExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{Axis, WorkspaceId};
|
use crate::WorkspaceId;
|
||||||
|
|
||||||
use model::{
|
use model::{
|
||||||
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
|
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
|
||||||
|
@ -403,7 +403,7 @@ impl WorkspaceDb {
|
||||||
.map(|(group_id, axis, pane_id, active, flexes)| {
|
.map(|(group_id, axis, pane_id, active, flexes)| {
|
||||||
if let Some((group_id, axis)) = group_id.zip(axis) {
|
if let Some((group_id, axis)) = group_id.zip(axis) {
|
||||||
let flexes = flexes
|
let flexes = flexes
|
||||||
.map(|flexes| serde_json::from_str::<Vec<f32>>(&flexes))
|
.map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
Ok(SerializedPaneGroup::Group {
|
Ok(SerializedPaneGroup::Group {
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
use crate::{
|
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
|
||||||
item::ItemHandle, Axis, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId,
|
|
||||||
};
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use db::sqlez::{
|
use db::sqlez::{
|
||||||
bindable::{Bind, Column, StaticColumnCount},
|
bindable::{Bind, Column, StaticColumnCount},
|
||||||
statement::Statement,
|
statement::Statement,
|
||||||
};
|
};
|
||||||
use gpui::{AsyncWindowContext, Model, Task, View, WeakView, WindowBounds};
|
use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView, WindowBounds};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
|
|
@ -29,12 +29,12 @@ use futures::{
|
||||||
Future, FutureExt, StreamExt,
|
Future, FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
|
actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
|
||||||
AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity,
|
AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity,
|
||||||
EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement,
|
EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement,
|
||||||
KeyContext, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Point,
|
KeyContext, ManagedView, Model, ModelContext, MouseMoveEvent, ParentElement, PathPromptOptions,
|
||||||
PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext,
|
Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext,
|
||||||
WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
||||||
};
|
};
|
||||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -227,6 +227,9 @@ pub fn init_settings(cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
cx.default_global::<DockDragState>();
|
||||||
|
cx.default_global::<DockClickReset>();
|
||||||
|
|
||||||
init_settings(cx);
|
init_settings(cx);
|
||||||
notifications::init(cx);
|
notifications::init(cx);
|
||||||
|
|
||||||
|
@ -480,8 +483,6 @@ struct FollowerState {
|
||||||
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
|
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WorkspaceBounds {}
|
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
workspace_id: WorkspaceId,
|
workspace_id: WorkspaceId,
|
||||||
|
@ -2032,7 +2033,7 @@ impl Workspace {
|
||||||
};
|
};
|
||||||
let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
|
let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
|
||||||
let center = match cursor {
|
let center = match cursor {
|
||||||
Some(cursor) if bounding_box.contains_point(&cursor) => cursor,
|
Some(cursor) if bounding_box.contains(&cursor) => cursor,
|
||||||
_ => bounding_box.center(),
|
_ => bounding_box.center(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3571,6 +3572,16 @@ impl FocusableView for Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct WorkspaceBounds(Bounds<Pixels>);
|
||||||
|
|
||||||
|
//todo!("remove this when better drag APIs are in GPUI2")
|
||||||
|
#[derive(Default)]
|
||||||
|
struct DockDragState(Option<DockPosition>);
|
||||||
|
|
||||||
|
//todo!("remove this when better double APIs are in GPUI2")
|
||||||
|
#[derive(Default)]
|
||||||
|
struct DockClickReset(Option<Task<()>>);
|
||||||
|
|
||||||
impl Render for Workspace {
|
impl Render for Workspace {
|
||||||
type Element = Div;
|
type Element = Div;
|
||||||
|
|
||||||
|
@ -3614,6 +3625,37 @@ impl Render for Workspace {
|
||||||
.border_t()
|
.border_t()
|
||||||
.border_b()
|
.border_b()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
|
.on_mouse_up(gpui::MouseButton::Left, |_, cx| {
|
||||||
|
cx.update_global(|drag: &mut DockDragState, cx| {
|
||||||
|
drag.0 = None;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_mouse_move(cx.listener(|workspace, e: &MouseMoveEvent, cx| {
|
||||||
|
if let Some(types) = &cx.global::<DockDragState>().0 {
|
||||||
|
let workspace_bounds = cx.global::<WorkspaceBounds>().0;
|
||||||
|
match types {
|
||||||
|
DockPosition::Left => {
|
||||||
|
let size = e.position.x;
|
||||||
|
workspace.left_dock.update(cx, |left_dock, cx| {
|
||||||
|
left_dock.resize_active_panel(Some(size.0), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DockPosition::Right => {
|
||||||
|
let size = workspace_bounds.size.width - e.position.x;
|
||||||
|
workspace.right_dock.update(cx, |right_dock, cx| {
|
||||||
|
right_dock.resize_active_panel(Some(size.0), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DockPosition::Bottom => {
|
||||||
|
let size = workspace_bounds.size.height - e.position.y;
|
||||||
|
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
|
||||||
|
bottom_dock.resize_active_panel(Some(size.0), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.child(canvas(|bounds, cx| cx.set_global(WorkspaceBounds(bounds))))
|
||||||
.child(self.modal_layer.clone())
|
.child(self.modal_layer.clone())
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue