Remove dispatch_event from Element trait
This commit is contained in:
parent
d25c6b15a6
commit
12eab6551f
26 changed files with 288 additions and 736 deletions
|
@ -526,18 +526,6 @@ impl Element for AvatarRibbon {
|
||||||
cx.scene.push_path(path.build(self.color, None));
|
cx.scene.push_path(path.build(self.color, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &gpui::Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
_: &mut gpui::EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: Range<usize>,
|
_: Range<usize>,
|
||||||
|
|
|
@ -29,9 +29,9 @@ use gpui::{
|
||||||
json::{self, ToJson},
|
json::{self, ToJson},
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
text_layout::{self, Line, RunStyle, TextLayoutCache},
|
||||||
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
|
AppContext, Axis, Border, CursorRegion, Element, ElementBox, EventContext, LayoutContext,
|
||||||
LayoutContext, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MutableAppContext,
|
MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MutableAppContext, PaintContext,
|
||||||
PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
|
Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use json::json;
|
use json::json;
|
||||||
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
||||||
|
@ -1878,18 +1878,6 @@ impl Element for EditorElement {
|
||||||
cx.scene.pop_layer();
|
cx.scene.pop_layer();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut LayoutState,
|
|
||||||
_: &mut (),
|
|
||||||
_: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -101,18 +101,6 @@ impl gpui::Element for TextElement {
|
||||||
line.paint(bounds.origin(), visible_bounds, bounds.height(), cx);
|
line.paint(bounds.origin(), visible_bounds, bounds.height(), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &gpui::Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
_: &mut gpui::EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: Range<usize>,
|
_: Range<usize>,
|
||||||
|
|
|
@ -33,8 +33,8 @@ use crate::{
|
||||||
},
|
},
|
||||||
json,
|
json,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext,
|
Action, DebugContext, EventContext, LayoutContext, PaintContext, RenderContext, SizeConstraint,
|
||||||
SizeConstraint, View,
|
View,
|
||||||
};
|
};
|
||||||
use core::panic;
|
use core::panic;
|
||||||
use json::ToJson;
|
use json::ToJson;
|
||||||
|
@ -50,7 +50,6 @@ use std::{
|
||||||
trait AnyElement {
|
trait AnyElement {
|
||||||
fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F;
|
fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F;
|
||||||
fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext);
|
fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext);
|
||||||
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool;
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
@ -80,16 +79,6 @@ pub trait Element {
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState;
|
) -> Self::PaintState;
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
bounds: RectF,
|
|
||||||
visible_bounds: RectF,
|
|
||||||
layout: &mut Self::LayoutState,
|
|
||||||
paint: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool;
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
@ -303,22 +292,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
|
|
||||||
if let Lifecycle::PostPaint {
|
|
||||||
element,
|
|
||||||
bounds,
|
|
||||||
visible_bounds,
|
|
||||||
layout,
|
|
||||||
paint,
|
|
||||||
..
|
|
||||||
} = self
|
|
||||||
{
|
|
||||||
element.dispatch_event(event, *bounds, *visible_bounds, layout, paint, cx)
|
|
||||||
} else {
|
|
||||||
panic!("invalid element lifecycle state");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
@ -433,10 +406,6 @@ impl ElementRc {
|
||||||
self.element.borrow_mut().paint(origin, visible_bounds, cx);
|
self.element.borrow_mut().paint(origin, visible_bounds, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
|
|
||||||
self.element.borrow_mut().dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rect_for_text_range(
|
pub fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -2,8 +2,7 @@ use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json,
|
json,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||||
SizeConstraint,
|
|
||||||
};
|
};
|
||||||
use json::ToJson;
|
use json::ToJson;
|
||||||
|
|
||||||
|
@ -84,18 +83,6 @@ impl Element for Align {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: std::ops::Range<usize>,
|
range_utf16: std::ops::Range<usize>,
|
||||||
|
|
|
@ -56,18 +56,6 @@ where
|
||||||
self.0(bounds, visible_bounds, cx)
|
self.0(bounds, visible_bounds, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &crate::Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
_: &mut crate::EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: std::ops::Range<usize>,
|
_: std::ops::Range<usize>,
|
||||||
|
|
|
@ -7,8 +7,7 @@ use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json,
|
json,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||||
SizeConstraint,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ConstrainedBox {
|
pub struct ConstrainedBox {
|
||||||
|
@ -157,18 +156,6 @@ impl Element for ConstrainedBox {
|
||||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
scene::{self, Border, CursorRegion, Quad},
|
scene::{self, Border, CursorRegion, Quad},
|
||||||
Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -285,18 +285,6 @@ impl Element for Container {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
DebugContext,
|
DebugContext,
|
||||||
};
|
};
|
||||||
use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
|
use crate::{Element, LayoutContext, PaintContext, SizeConstraint};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Empty {
|
pub struct Empty {
|
||||||
|
@ -59,18 +59,6 @@ impl Element for Empty {
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
_: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: Range<usize>,
|
_: Range<usize>,
|
||||||
|
|
|
@ -4,8 +4,7 @@ use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json,
|
json,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||||
SizeConstraint,
|
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
@ -66,18 +65,6 @@ impl Element for Expanded {
|
||||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -3,8 +3,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
|
||||||
use crate::{
|
use crate::{
|
||||||
json::{self, ToJson, Value},
|
json::{self, ToJson, Value},
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
|
Axis, DebugContext, Element, ElementBox, ElementStateHandle, LayoutContext, PaintContext,
|
||||||
LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
|
RenderContext, SizeConstraint, Vector2FExt, View,
|
||||||
};
|
};
|
||||||
use pathfinder_geometry::{
|
use pathfinder_geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
|
@ -318,23 +318,6 @@ impl Element for Flex {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
let mut handled = false;
|
|
||||||
for child in &mut self.children {
|
|
||||||
handled = child.dispatch_event(event, cx) || handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
handled
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
@ -420,18 +403,6 @@ impl Element for FlexItem {
|
||||||
self.child.paint(bounds.origin(), visible_bounds, cx)
|
self.child.paint(bounds.origin(), visible_bounds, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -4,8 +4,7 @@ use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json::json,
|
json::json,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||||
SizeConstraint,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Hook {
|
pub struct Hook {
|
||||||
|
@ -56,18 +55,6 @@ impl Element for Hook {
|
||||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -6,8 +6,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
json::{json, ToJson},
|
json::{json, ToJson},
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext,
|
scene, Border, DebugContext, Element, ImageData, LayoutContext, PaintContext, SizeConstraint,
|
||||||
PaintContext, SizeConstraint,
|
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
|
@ -81,18 +80,6 @@ impl Element for Image {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
_: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: Range<usize>,
|
_: Range<usize>,
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
||||||
elements::*,
|
elements::*,
|
||||||
fonts::TextStyle,
|
fonts::TextStyle,
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
Action, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
Action, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
@ -64,18 +64,6 @@ impl Element for KeystrokeLabel {
|
||||||
element.paint(bounds.origin(), visible_bounds, cx);
|
element.paint(bounds.origin(), visible_bounds, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
element: &mut ElementBox,
|
|
||||||
_: &mut (),
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
element.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: Range<usize>,
|
_: Range<usize>,
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
json::{ToJson, Value},
|
json::{ToJson, Value},
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
text_layout::{Line, RunStyle},
|
text_layout::{Line, RunStyle},
|
||||||
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
DebugContext, Element, LayoutContext, PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -165,18 +165,6 @@ impl Element for Label {
|
||||||
line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
|
line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
_: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: Range<usize>,
|
_: Range<usize>,
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
json::json,
|
json::json,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, MouseRegion,
|
DebugContext, Element, ElementBox, ElementRc, EventContext, LayoutContext, MouseRegion,
|
||||||
PaintContext, RenderContext, SizeConstraint, View, ViewContext,
|
PaintContext, RenderContext, SizeConstraint, View, ViewContext,
|
||||||
};
|
};
|
||||||
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
|
||||||
|
@ -13,7 +13,6 @@ use sum_tree::{Bias, SumTree};
|
||||||
|
|
||||||
pub struct List {
|
pub struct List {
|
||||||
state: ListState,
|
state: ListState,
|
||||||
invalidated_elements: Vec<ElementRc>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -82,10 +81,7 @@ struct Height(f32);
|
||||||
|
|
||||||
impl List {
|
impl List {
|
||||||
pub fn new(state: ListState) -> Self {
|
pub fn new(state: ListState) -> Self {
|
||||||
Self {
|
Self { state }
|
||||||
state,
|
|
||||||
invalidated_elements: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,50 +285,6 @@ impl Element for List {
|
||||||
cx.scene.pop_layer();
|
cx.scene.pop_layer();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
bounds: RectF,
|
|
||||||
_: RectF,
|
|
||||||
scroll_top: &mut ListOffset,
|
|
||||||
_: &mut (),
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
let mut handled = false;
|
|
||||||
|
|
||||||
let mut state = self.state.0.borrow_mut();
|
|
||||||
let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
|
|
||||||
let mut cursor = state.items.cursor::<Count>();
|
|
||||||
let mut new_items = cursor.slice(&Count(scroll_top.item_ix), Bias::Right, &());
|
|
||||||
while let Some(item) = cursor.item() {
|
|
||||||
if item_origin.y() > bounds.max_y() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ListItem::Rendered(element) = item {
|
|
||||||
let prev_notify_count = cx.notify_count();
|
|
||||||
let mut element = element.clone();
|
|
||||||
handled = element.dispatch_event(event, cx) || handled;
|
|
||||||
item_origin.set_y(item_origin.y() + element.size().y());
|
|
||||||
if cx.notify_count() > prev_notify_count {
|
|
||||||
new_items.push(ListItem::Unrendered, &());
|
|
||||||
self.invalidated_elements.push(element);
|
|
||||||
} else {
|
|
||||||
new_items.push(item.clone(), &());
|
|
||||||
}
|
|
||||||
cursor.next(&());
|
|
||||||
} else {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new_items.push_tree(cursor.suffix(&()), &());
|
|
||||||
drop(cursor);
|
|
||||||
state.items = new_items;
|
|
||||||
|
|
||||||
handled
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
@ -964,18 +916,6 @@ mod tests {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut (),
|
|
||||||
_: &mut (),
|
|
||||||
_: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: Range<usize>,
|
_: Range<usize>,
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
|
CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
|
||||||
MouseMove, MouseScrollWheel, MouseUp, MouseUpOut,
|
MouseMove, MouseScrollWheel, MouseUp, MouseUpOut,
|
||||||
},
|
},
|
||||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
|
DebugContext, Element, ElementBox, EventContext, LayoutContext, MeasurementContext,
|
||||||
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
|
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -194,18 +194,6 @@ impl<Tag> Element for MouseEventHandler<Tag> {
|
||||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -4,8 +4,8 @@ use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json::ToJson,
|
json::ToJson,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
|
Axis, DebugContext, Element, ElementBox, LayoutContext, MouseRegion, PaintContext,
|
||||||
PaintContext, SizeConstraint,
|
SizeConstraint,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
@ -225,18 +225,6 @@ impl Element for Overlay {
|
||||||
cx.scene.pop_stacking_context();
|
cx.scene.pop_stacking_context();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -187,18 +187,6 @@ impl Element for Resizable {
|
||||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &crate::Event,
|
|
||||||
_bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
_visible_bounds: pathfinder_geometry::rect::RectF,
|
|
||||||
_layout: &mut Self::LayoutState,
|
|
||||||
_paint: &mut Self::PaintState,
|
|
||||||
cx: &mut crate::EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: std::ops::Range<usize>,
|
range_utf16: std::ops::Range<usize>,
|
||||||
|
|
|
@ -4,8 +4,7 @@ use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json::{self, json, ToJson},
|
json::{self, json, ToJson},
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
|
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
|
||||||
SizeConstraint,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -49,23 +48,6 @@ impl Element for Stack {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
for child in self.children.iter_mut().rev() {
|
|
||||||
if child.dispatch_event(event, cx) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
scene, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
scene, DebugContext, Element, LayoutContext, PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Svg {
|
pub struct Svg {
|
||||||
|
@ -73,18 +73,6 @@ impl Element for Svg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
_: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: Range<usize>,
|
_: Range<usize>,
|
||||||
|
|
|
@ -8,8 +8,7 @@ use crate::{
|
||||||
json::{ToJson, Value},
|
json::{ToJson, Value},
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
text_layout::{Line, RunStyle, ShapedBoundary},
|
text_layout::{Line, RunStyle, ShapedBoundary},
|
||||||
DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
|
DebugContext, Element, FontCache, LayoutContext, PaintContext, SizeConstraint, TextLayoutCache,
|
||||||
SizeConstraint, TextLayoutCache,
|
|
||||||
};
|
};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -178,18 +177,6 @@ impl Element for Text {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
_: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
_: Range<usize>,
|
_: Range<usize>,
|
||||||
|
|
|
@ -186,18 +186,6 @@ impl Element for Tooltip {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &crate::Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
_: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut crate::EventContext,
|
|
||||||
) -> bool {
|
|
||||||
self.child.dispatch_event(event, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
|
use super::{Element, EventContext, LayoutContext, PaintContext, SizeConstraint};
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::{
|
geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
|
@ -324,23 +324,6 @@ impl Element for UniformList {
|
||||||
cx.scene.pop_layer();
|
cx.scene.pop_layer();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
layout: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
let mut handled = false;
|
|
||||||
for item in &mut layout.items {
|
|
||||||
handled = item.dispatch_event(event, cx) || handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
handled
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
|
|
|
@ -228,296 +228,283 @@ impl Presenter {
|
||||||
event_reused: bool,
|
event_reused: bool,
|
||||||
cx: &mut MutableAppContext,
|
cx: &mut MutableAppContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
let mut mouse_events = SmallVec::<[_; 2]>::new();
|
||||||
let mut mouse_events = SmallVec::<[_; 2]>::new();
|
let mut notified_views: HashSet<usize> = Default::default();
|
||||||
let mut notified_views: HashSet<usize> = Default::default();
|
|
||||||
|
|
||||||
// 1. Handle platform event. Keyboard events get dispatched immediately, while mouse events
|
// 1. Handle platform event. Keyboard events get dispatched immediately, while mouse events
|
||||||
// get mapped into the mouse-specific MouseEvent type.
|
// get mapped into the mouse-specific MouseEvent type.
|
||||||
// -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
|
// -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
|
||||||
// -> Also updates mouse-related state
|
// -> Also updates mouse-related state
|
||||||
match &event {
|
match &event {
|
||||||
Event::KeyDown(e) => return cx.dispatch_key_down(self.window_id, e),
|
Event::KeyDown(e) => return cx.dispatch_key_down(self.window_id, e),
|
||||||
Event::KeyUp(e) => return cx.dispatch_key_up(self.window_id, e),
|
Event::KeyUp(e) => return cx.dispatch_key_up(self.window_id, e),
|
||||||
Event::ModifiersChanged(e) => {
|
Event::ModifiersChanged(e) => return cx.dispatch_modifiers_changed(self.window_id, e),
|
||||||
return cx.dispatch_modifiers_changed(self.window_id, e)
|
Event::MouseDown(e) => {
|
||||||
}
|
// Click events are weird because they can be fired after a drag event.
|
||||||
Event::MouseDown(e) => {
|
// MDN says that browsers handle this by starting from 'the most
|
||||||
// Click events are weird because they can be fired after a drag event.
|
// specific ancestor element that contained both [positions]'
|
||||||
// MDN says that browsers handle this by starting from 'the most
|
// So we need to store the overlapping regions on mouse down.
|
||||||
// specific ancestor element that contained both [positions]'
|
|
||||||
// So we need to store the overlapping regions on mouse down.
|
|
||||||
|
|
||||||
// If there is already clicked_button stored, don't replace it.
|
// If there is already clicked_button stored, don't replace it.
|
||||||
if self.clicked_button.is_none() {
|
if self.clicked_button.is_none() {
|
||||||
self.clicked_region_ids = self
|
self.clicked_region_ids = self
|
||||||
.mouse_regions
|
.mouse_regions
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(region, _)| {
|
.filter_map(|(region, _)| {
|
||||||
if region.bounds.contains_point(e.position) {
|
if region.bounds.contains_point(e.position) {
|
||||||
Some(region.id())
|
Some(region.id())
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
self.clicked_button = Some(e.button);
|
|
||||||
}
|
|
||||||
|
|
||||||
mouse_events.push(MouseEvent::Down(MouseDown {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: e.clone(),
|
|
||||||
}));
|
|
||||||
mouse_events.push(MouseEvent::DownOut(MouseDownOut {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: e.clone(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
Event::MouseUp(e) => {
|
|
||||||
// NOTE: The order of event pushes is important! MouseUp events MUST be fired
|
|
||||||
// before click events, and so the MouseUp events need to be pushed before
|
|
||||||
// MouseClick events.
|
|
||||||
mouse_events.push(MouseEvent::Up(MouseUp {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: e.clone(),
|
|
||||||
}));
|
|
||||||
mouse_events.push(MouseEvent::UpOut(MouseUpOut {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: e.clone(),
|
|
||||||
}));
|
|
||||||
mouse_events.push(MouseEvent::Click(MouseClick {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: e.clone(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
Event::MouseMoved(
|
|
||||||
e @ MouseMovedEvent {
|
|
||||||
position,
|
|
||||||
pressed_button,
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
let mut style_to_assign = CursorStyle::Arrow;
|
|
||||||
for region in self.cursor_regions.iter().rev() {
|
|
||||||
if region.bounds.contains_point(*position) {
|
|
||||||
style_to_assign = region.style;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cx.platform().set_cursor_style(style_to_assign);
|
|
||||||
|
|
||||||
if !event_reused {
|
|
||||||
if pressed_button.is_some() {
|
|
||||||
mouse_events.push(MouseEvent::Drag(MouseDrag {
|
|
||||||
region: Default::default(),
|
|
||||||
prev_mouse_position: self.mouse_position,
|
|
||||||
platform_event: e.clone(),
|
|
||||||
}));
|
|
||||||
} else if let Some(clicked_button) = self.clicked_button {
|
|
||||||
// Mouse up event happened outside the current window. Simulate mouse up button event
|
|
||||||
let button_event = e.to_button_event(clicked_button);
|
|
||||||
mouse_events.push(MouseEvent::Up(MouseUp {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: button_event.clone(),
|
|
||||||
}));
|
|
||||||
mouse_events.push(MouseEvent::UpOut(MouseUpOut {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: button_event.clone(),
|
|
||||||
}));
|
|
||||||
mouse_events.push(MouseEvent::Click(MouseClick {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: button_event.clone(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
mouse_events.push(MouseEvent::Move(MouseMove {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: e.clone(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
mouse_events.push(MouseEvent::Hover(MouseHover {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: e.clone(),
|
|
||||||
started: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.last_mouse_moved_event = Some(event.clone());
|
|
||||||
}
|
|
||||||
Event::ScrollWheel(e) => {
|
|
||||||
mouse_events.push(MouseEvent::ScrollWheel(MouseScrollWheel {
|
|
||||||
region: Default::default(),
|
|
||||||
platform_event: e.clone(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(position) = event.position() {
|
|
||||||
self.mouse_position = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Dispatch mouse events on regions
|
|
||||||
let mut any_event_handled = false;
|
|
||||||
for mut mouse_event in mouse_events {
|
|
||||||
let mut valid_regions = Vec::new();
|
|
||||||
|
|
||||||
// GPUI elements are arranged by depth but sibling elements can register overlapping
|
|
||||||
// mouse regions. As such, hover events are only fired on overlapping elements which
|
|
||||||
// are at the same depth as the topmost element which overlaps with the mouse.
|
|
||||||
match &mouse_event {
|
|
||||||
MouseEvent::Hover(_) => {
|
|
||||||
let mut top_most_depth = None;
|
|
||||||
let mouse_position = self.mouse_position.clone();
|
|
||||||
for (region, depth) in self.mouse_regions.iter().rev() {
|
|
||||||
// Allow mouse regions to appear transparent to hovers
|
|
||||||
if !region.hoverable {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contains_mouse = region.bounds.contains_point(mouse_position);
|
|
||||||
|
|
||||||
if contains_mouse && top_most_depth.is_none() {
|
|
||||||
top_most_depth = Some(depth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This unwrap relies on short circuiting boolean expressions
|
|
||||||
// The right side of the && is only executed when contains_mouse
|
|
||||||
// is true, and we know above that when contains_mouse is true
|
|
||||||
// top_most_depth is set
|
|
||||||
if contains_mouse && depth == top_most_depth.unwrap() {
|
|
||||||
//Ensure that hover entrance events aren't sent twice
|
|
||||||
if self.hovered_region_ids.insert(region.id()) {
|
|
||||||
valid_regions.push(region.clone());
|
|
||||||
if region.notify_on_hover {
|
|
||||||
notified_views.insert(region.id().view_id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Ensure that hover exit events aren't sent twice
|
None
|
||||||
if self.hovered_region_ids.remove(®ion.id()) {
|
|
||||||
valid_regions.push(region.clone());
|
|
||||||
if region.notify_on_hover {
|
|
||||||
notified_views.insert(region.id().view_id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
.collect();
|
||||||
MouseEvent::Down(_) | MouseEvent::Up(_) => {
|
|
||||||
for (region, _) in self.mouse_regions.iter().rev() {
|
|
||||||
if region.bounds.contains_point(self.mouse_position) {
|
|
||||||
if region.notify_on_click {
|
|
||||||
notified_views.insert(region.id().view_id());
|
|
||||||
}
|
|
||||||
valid_regions.push(region.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MouseEvent::Click(e) => {
|
|
||||||
// Only raise click events if the released button is the same as the one stored
|
|
||||||
if self
|
|
||||||
.clicked_button
|
|
||||||
.map(|clicked_button| clicked_button == e.button)
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
// Clear clicked regions and clicked button
|
|
||||||
let clicked_region_ids =
|
|
||||||
std::mem::replace(&mut self.clicked_region_ids, Default::default());
|
|
||||||
self.clicked_button = None;
|
|
||||||
|
|
||||||
// Find regions which still overlap with the mouse since the last MouseDown happened
|
self.clicked_button = Some(e.button);
|
||||||
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
|
||||||
if clicked_region_ids.contains(&mouse_region.id()) {
|
|
||||||
if mouse_region.bounds.contains_point(self.mouse_position) {
|
|
||||||
valid_regions.push(mouse_region.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MouseEvent::Drag(_) => {
|
|
||||||
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
|
||||||
if self.clicked_region_ids.contains(&mouse_region.id()) {
|
|
||||||
valid_regions.push(mouse_region.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
|
|
||||||
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
|
||||||
// NOT contains
|
|
||||||
if !mouse_region.bounds.contains_point(self.mouse_position) {
|
|
||||||
valid_regions.push(mouse_region.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
|
||||||
// Contains
|
|
||||||
if mouse_region.bounds.contains_point(self.mouse_position) {
|
|
||||||
valid_regions.push(mouse_region.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//3. Fire region events
|
mouse_events.push(MouseEvent::Down(MouseDown {
|
||||||
let hovered_region_ids = self.hovered_region_ids.clone();
|
region: Default::default(),
|
||||||
for valid_region in valid_regions.into_iter() {
|
platform_event: e.clone(),
|
||||||
let mut event_cx = self.build_event_context(&mut notified_views, cx);
|
}));
|
||||||
|
mouse_events.push(MouseEvent::DownOut(MouseDownOut {
|
||||||
mouse_event.set_region(valid_region.bounds);
|
region: Default::default(),
|
||||||
if let MouseEvent::Hover(e) = &mut mouse_event {
|
platform_event: e.clone(),
|
||||||
e.started = hovered_region_ids.contains(&valid_region.id())
|
}));
|
||||||
}
|
}
|
||||||
// Handle Down events if the MouseRegion has a Click or Drag handler. This makes the api more intuitive as you would
|
Event::MouseUp(e) => {
|
||||||
// not expect a MouseRegion to be transparent to Down events if it also has a Click handler.
|
// NOTE: The order of event pushes is important! MouseUp events MUST be fired
|
||||||
// This behavior can be overridden by adding a Down handler that calls cx.propogate_event
|
// before click events, and so the MouseUp events need to be pushed before
|
||||||
if let MouseEvent::Down(e) = &mouse_event {
|
// MouseClick events.
|
||||||
if valid_region
|
mouse_events.push(MouseEvent::Up(MouseUp {
|
||||||
.handlers
|
region: Default::default(),
|
||||||
.contains_handler(MouseEvent::click_disc(), Some(e.button))
|
platform_event: e.clone(),
|
||||||
|| valid_region
|
}));
|
||||||
.handlers
|
mouse_events.push(MouseEvent::UpOut(MouseUpOut {
|
||||||
.contains_handler(MouseEvent::drag_disc(), Some(e.button))
|
region: Default::default(),
|
||||||
{
|
platform_event: e.clone(),
|
||||||
event_cx.handled = true;
|
}));
|
||||||
}
|
mouse_events.push(MouseEvent::Click(MouseClick {
|
||||||
}
|
region: Default::default(),
|
||||||
|
platform_event: e.clone(),
|
||||||
if let Some(callback) = valid_region.handlers.get(&mouse_event.handler_key()) {
|
}));
|
||||||
event_cx.handled = true;
|
}
|
||||||
event_cx.with_current_view(valid_region.id().view_id(), {
|
Event::MouseMoved(
|
||||||
let region_event = mouse_event.clone();
|
e @ MouseMovedEvent {
|
||||||
|cx| {
|
position,
|
||||||
callback(region_event, cx);
|
pressed_button,
|
||||||
}
|
..
|
||||||
});
|
},
|
||||||
}
|
) => {
|
||||||
|
let mut style_to_assign = CursorStyle::Arrow;
|
||||||
any_event_handled = any_event_handled || event_cx.handled;
|
for region in self.cursor_regions.iter().rev() {
|
||||||
// For bubbling events, if the event was handled, don't continue dispatching
|
if region.bounds.contains_point(*position) {
|
||||||
// This only makes sense for local events.
|
style_to_assign = region.style;
|
||||||
if event_cx.handled && mouse_event.is_capturable() {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
cx.platform().set_cursor_style(style_to_assign);
|
||||||
|
|
||||||
if !any_event_handled && !event_reused {
|
if !event_reused {
|
||||||
let mut event_cx = self.build_event_context(&mut notified_views, cx);
|
if pressed_button.is_some() {
|
||||||
any_event_handled = event_cx.dispatch_event(root_view_id, &event);
|
mouse_events.push(MouseEvent::Drag(MouseDrag {
|
||||||
}
|
region: Default::default(),
|
||||||
|
prev_mouse_position: self.mouse_position,
|
||||||
|
platform_event: e.clone(),
|
||||||
|
}));
|
||||||
|
} else if let Some(clicked_button) = self.clicked_button {
|
||||||
|
// Mouse up event happened outside the current window. Simulate mouse up button event
|
||||||
|
let button_event = e.to_button_event(clicked_button);
|
||||||
|
mouse_events.push(MouseEvent::Up(MouseUp {
|
||||||
|
region: Default::default(),
|
||||||
|
platform_event: button_event.clone(),
|
||||||
|
}));
|
||||||
|
mouse_events.push(MouseEvent::UpOut(MouseUpOut {
|
||||||
|
region: Default::default(),
|
||||||
|
platform_event: button_event.clone(),
|
||||||
|
}));
|
||||||
|
mouse_events.push(MouseEvent::Click(MouseClick {
|
||||||
|
region: Default::default(),
|
||||||
|
platform_event: button_event.clone(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
for view_id in notified_views {
|
mouse_events.push(MouseEvent::Move(MouseMove {
|
||||||
cx.notify_view(self.window_id, view_id);
|
region: Default::default(),
|
||||||
}
|
platform_event: e.clone(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
any_event_handled
|
mouse_events.push(MouseEvent::Hover(MouseHover {
|
||||||
} else {
|
region: Default::default(),
|
||||||
false
|
platform_event: e.clone(),
|
||||||
|
started: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.last_mouse_moved_event = Some(event.clone());
|
||||||
|
}
|
||||||
|
Event::ScrollWheel(e) => mouse_events.push(MouseEvent::ScrollWheel(MouseScrollWheel {
|
||||||
|
region: Default::default(),
|
||||||
|
platform_event: e.clone(),
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(position) = event.position() {
|
||||||
|
self.mouse_position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Dispatch mouse events on regions
|
||||||
|
let mut any_event_handled = false;
|
||||||
|
for mut mouse_event in mouse_events {
|
||||||
|
let mut valid_regions = Vec::new();
|
||||||
|
|
||||||
|
// GPUI elements are arranged by depth but sibling elements can register overlapping
|
||||||
|
// mouse regions. As such, hover events are only fired on overlapping elements which
|
||||||
|
// are at the same depth as the topmost element which overlaps with the mouse.
|
||||||
|
match &mouse_event {
|
||||||
|
MouseEvent::Hover(_) => {
|
||||||
|
let mut top_most_depth = None;
|
||||||
|
let mouse_position = self.mouse_position.clone();
|
||||||
|
for (region, depth) in self.mouse_regions.iter().rev() {
|
||||||
|
// Allow mouse regions to appear transparent to hovers
|
||||||
|
if !region.hoverable {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contains_mouse = region.bounds.contains_point(mouse_position);
|
||||||
|
|
||||||
|
if contains_mouse && top_most_depth.is_none() {
|
||||||
|
top_most_depth = Some(depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This unwrap relies on short circuiting boolean expressions
|
||||||
|
// The right side of the && is only executed when contains_mouse
|
||||||
|
// is true, and we know above that when contains_mouse is true
|
||||||
|
// top_most_depth is set
|
||||||
|
if contains_mouse && depth == top_most_depth.unwrap() {
|
||||||
|
//Ensure that hover entrance events aren't sent twice
|
||||||
|
if self.hovered_region_ids.insert(region.id()) {
|
||||||
|
valid_regions.push(region.clone());
|
||||||
|
if region.notify_on_hover {
|
||||||
|
notified_views.insert(region.id().view_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ensure that hover exit events aren't sent twice
|
||||||
|
if self.hovered_region_ids.remove(®ion.id()) {
|
||||||
|
valid_regions.push(region.clone());
|
||||||
|
if region.notify_on_hover {
|
||||||
|
notified_views.insert(region.id().view_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseEvent::Down(_) | MouseEvent::Up(_) => {
|
||||||
|
for (region, _) in self.mouse_regions.iter().rev() {
|
||||||
|
if region.bounds.contains_point(self.mouse_position) {
|
||||||
|
if region.notify_on_click {
|
||||||
|
notified_views.insert(region.id().view_id());
|
||||||
|
}
|
||||||
|
valid_regions.push(region.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseEvent::Click(e) => {
|
||||||
|
// Only raise click events if the released button is the same as the one stored
|
||||||
|
if self
|
||||||
|
.clicked_button
|
||||||
|
.map(|clicked_button| clicked_button == e.button)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
// Clear clicked regions and clicked button
|
||||||
|
let clicked_region_ids =
|
||||||
|
std::mem::replace(&mut self.clicked_region_ids, Default::default());
|
||||||
|
self.clicked_button = None;
|
||||||
|
|
||||||
|
// Find regions which still overlap with the mouse since the last MouseDown happened
|
||||||
|
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
||||||
|
if clicked_region_ids.contains(&mouse_region.id()) {
|
||||||
|
if mouse_region.bounds.contains_point(self.mouse_position) {
|
||||||
|
valid_regions.push(mouse_region.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseEvent::Drag(_) => {
|
||||||
|
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
||||||
|
if self.clicked_region_ids.contains(&mouse_region.id()) {
|
||||||
|
valid_regions.push(mouse_region.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
|
||||||
|
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
||||||
|
// NOT contains
|
||||||
|
if !mouse_region.bounds.contains_point(self.mouse_position) {
|
||||||
|
valid_regions.push(mouse_region.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
||||||
|
// Contains
|
||||||
|
if mouse_region.bounds.contains_point(self.mouse_position) {
|
||||||
|
valid_regions.push(mouse_region.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//3. Fire region events
|
||||||
|
let hovered_region_ids = self.hovered_region_ids.clone();
|
||||||
|
for valid_region in valid_regions.into_iter() {
|
||||||
|
let mut event_cx = self.build_event_context(&mut notified_views, cx);
|
||||||
|
|
||||||
|
mouse_event.set_region(valid_region.bounds);
|
||||||
|
if let MouseEvent::Hover(e) = &mut mouse_event {
|
||||||
|
e.started = hovered_region_ids.contains(&valid_region.id())
|
||||||
|
}
|
||||||
|
// Handle Down events if the MouseRegion has a Click or Drag handler. This makes the api more intuitive as you would
|
||||||
|
// not expect a MouseRegion to be transparent to Down events if it also has a Click handler.
|
||||||
|
// This behavior can be overridden by adding a Down handler that calls cx.propogate_event
|
||||||
|
if let MouseEvent::Down(e) = &mouse_event {
|
||||||
|
if valid_region
|
||||||
|
.handlers
|
||||||
|
.contains_handler(MouseEvent::click_disc(), Some(e.button))
|
||||||
|
|| valid_region
|
||||||
|
.handlers
|
||||||
|
.contains_handler(MouseEvent::drag_disc(), Some(e.button))
|
||||||
|
{
|
||||||
|
event_cx.handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(callback) = valid_region.handlers.get(&mouse_event.handler_key()) {
|
||||||
|
event_cx.handled = true;
|
||||||
|
event_cx.with_current_view(valid_region.id().view_id(), {
|
||||||
|
let region_event = mouse_event.clone();
|
||||||
|
|cx| {
|
||||||
|
callback(region_event, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
any_event_handled = any_event_handled || event_cx.handled;
|
||||||
|
// For bubbling events, if the event was handled, don't continue dispatching
|
||||||
|
// This only makes sense for local events.
|
||||||
|
if event_cx.handled && mouse_event.is_capturable() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for view_id in notified_views {
|
||||||
|
cx.notify_view(self.window_id, view_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
any_event_handled
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_event_context<'a>(
|
pub fn build_event_context<'a>(
|
||||||
|
@ -526,7 +513,6 @@ impl Presenter {
|
||||||
cx: &'a mut MutableAppContext,
|
cx: &'a mut MutableAppContext,
|
||||||
) -> EventContext<'a> {
|
) -> EventContext<'a> {
|
||||||
EventContext {
|
EventContext {
|
||||||
rendered_views: &mut self.rendered_views,
|
|
||||||
font_cache: &self.font_cache,
|
font_cache: &self.font_cache,
|
||||||
text_layout_cache: &self.text_layout_cache,
|
text_layout_cache: &self.text_layout_cache,
|
||||||
view_stack: Default::default(),
|
view_stack: Default::default(),
|
||||||
|
@ -745,7 +731,6 @@ impl<'a> Deref for PaintContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EventContext<'a> {
|
pub struct EventContext<'a> {
|
||||||
rendered_views: &'a mut HashMap<usize, ElementBox>,
|
|
||||||
pub font_cache: &'a FontCache,
|
pub font_cache: &'a FontCache,
|
||||||
pub text_layout_cache: &'a TextLayoutCache,
|
pub text_layout_cache: &'a TextLayoutCache,
|
||||||
pub app: &'a mut MutableAppContext,
|
pub app: &'a mut MutableAppContext,
|
||||||
|
@ -757,17 +742,6 @@ pub struct EventContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> EventContext<'a> {
|
impl<'a> EventContext<'a> {
|
||||||
fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool {
|
|
||||||
if let Some(mut element) = self.rendered_views.remove(&view_id) {
|
|
||||||
let result =
|
|
||||||
self.with_current_view(view_id, |this| element.dispatch_event(event, this));
|
|
||||||
self.rendered_views.insert(view_id, element);
|
|
||||||
result
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_current_view<F, T>(&mut self, view_id: usize, f: F) -> T
|
fn with_current_view<F, T>(&mut self, view_id: usize, f: F) -> T
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Self) -> T,
|
F: FnOnce(&mut Self) -> T,
|
||||||
|
@ -1030,27 +1004,6 @@ impl Element for ChildView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
event: &Event,
|
|
||||||
_: RectF,
|
|
||||||
_: RectF,
|
|
||||||
view_is_valid: &mut Self::LayoutState,
|
|
||||||
_: &mut Self::PaintState,
|
|
||||||
cx: &mut EventContext,
|
|
||||||
) -> bool {
|
|
||||||
if *view_is_valid {
|
|
||||||
cx.dispatch_event(self.view.id(), event)
|
|
||||||
} else {
|
|
||||||
log::error!(
|
|
||||||
"dispatch_event called on a ChildView element whose underlying view was dropped (view_id: {}, name: {:?})",
|
|
||||||
self.view.id(),
|
|
||||||
self.view_name
|
|
||||||
);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
&self,
|
&self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
@ -798,18 +798,6 @@ impl Element for TerminalElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_event(
|
|
||||||
&mut self,
|
|
||||||
_: &gpui::Event,
|
|
||||||
_bounds: gpui::geometry::rect::RectF,
|
|
||||||
_visible_bounds: gpui::geometry::rect::RectF,
|
|
||||||
_layout: &mut Self::LayoutState,
|
|
||||||
_paint: &mut Self::PaintState,
|
|
||||||
_: &mut gpui::EventContext,
|
|
||||||
) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn metadata(&self) -> Option<&dyn std::any::Any> {
|
fn metadata(&self) -> Option<&dyn std::any::Any> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue