Implemented support for elliptical radial gradients (new default).
Created overscroll visual.
This commit is contained in:
parent
4bbf094bec
commit
4c82edfe49
|
@ -29,11 +29,13 @@
|
|||
# Scroll
|
||||
|
||||
* Implement over-scroll indicator.
|
||||
- Fade-in.
|
||||
- Animation release.
|
||||
* Implement touch scroll inertia.
|
||||
* Implement `ScrollMode::ZOOM`.
|
||||
- Touch gesture.
|
||||
- Scroll wheel zoom.
|
||||
- Commands.
|
||||
* Implement touch scroll inertia.
|
||||
|
||||
# Touch Events
|
||||
|
||||
|
|
|
@ -87,6 +87,12 @@ impl fmt::Debug for GradientRadiusBase {
|
|||
pub struct GradientRadius {
|
||||
/// How the base length is calculated. The base length is the `100.pct()` length.
|
||||
pub base: GradientRadiusBase,
|
||||
|
||||
/// If the gradient is circular or elliptical.
|
||||
///
|
||||
/// If `true` the radius is the same in both dimensions, if `false` the radius can be different.
|
||||
pub circle: bool,
|
||||
|
||||
/// The length of the rendered gradient stops.
|
||||
pub radii: Size,
|
||||
}
|
||||
|
@ -105,138 +111,136 @@ impl Default for GradientRadius {
|
|||
}
|
||||
}
|
||||
impl GradientRadius {
|
||||
/// Circle radius relative from center to the closest edge.
|
||||
pub fn closest_side(radius: impl Into<Length>) -> Self {
|
||||
Self {
|
||||
base: GradientRadiusBase::ClosestSide,
|
||||
radii: Size::splat(radius),
|
||||
}
|
||||
}
|
||||
|
||||
/// Circle radius relative from center to the closest corner.
|
||||
pub fn closest_corner(radius: impl Into<Length>) -> Self {
|
||||
Self {
|
||||
base: GradientRadiusBase::ClosestCorner,
|
||||
radii: Size::splat(radius),
|
||||
}
|
||||
}
|
||||
|
||||
/// Circle radius relative from center to the farthest edge.
|
||||
pub fn farthest_side(radius: impl Into<Length>) -> Self {
|
||||
Self {
|
||||
base: GradientRadiusBase::FarthestSide,
|
||||
radii: Size::splat(radius),
|
||||
}
|
||||
}
|
||||
|
||||
/// Circle radius relative from center to the farthest corner.
|
||||
pub fn farthest_corner(radius: impl Into<Length>) -> Self {
|
||||
Self {
|
||||
base: GradientRadiusBase::FarthestCorner,
|
||||
radii: Size::splat(radius),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ellipse radii relative from center to the closest edge.
|
||||
pub fn closest_side_ell(radius: impl Into<Size>) -> Self {
|
||||
pub fn closest_side(radius: impl Into<Size>) -> Self {
|
||||
Self {
|
||||
base: GradientRadiusBase::ClosestSide,
|
||||
circle: false,
|
||||
radii: radius.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ellipse radii relative from center to the closest corner.
|
||||
pub fn closest_corner_ell(radius: impl Into<Size>) -> Self {
|
||||
pub fn closest_corner(radius: impl Into<Size>) -> Self {
|
||||
Self {
|
||||
base: GradientRadiusBase::ClosestCorner,
|
||||
circle: false,
|
||||
radii: radius.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ellipse radii relative from center to the farthest edge.
|
||||
pub fn farthest_side_ell(radius: impl Into<Size>) -> Self {
|
||||
pub fn farthest_side(radius: impl Into<Size>) -> Self {
|
||||
Self {
|
||||
base: GradientRadiusBase::FarthestSide,
|
||||
circle: false,
|
||||
radii: radius.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ellipse radii relative from center to the farthest corner.
|
||||
pub fn farthest_corner_ell(radius: impl Into<Size>) -> Self {
|
||||
pub fn farthest_corner(radius: impl Into<Size>) -> Self {
|
||||
Self {
|
||||
base: GradientRadiusBase::FarthestCorner,
|
||||
circle: false,
|
||||
radii: radius.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable circular radius.
|
||||
pub fn circle(mut self) -> Self {
|
||||
self.circle = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the radius in the current [`LAYOUT`] context.
|
||||
pub fn layout(&self, center: PxPoint) -> PxSize {
|
||||
let size = LAYOUT.constraints().fill_size();
|
||||
|
||||
let length = match self.base {
|
||||
GradientRadiusBase::ClosestSide => center
|
||||
.x
|
||||
.min(center.y)
|
||||
.min(size.width - center.x)
|
||||
.min(size.height - center.y)
|
||||
.max(Px(0)),
|
||||
GradientRadiusBase::ClosestCorner => {
|
||||
let center = center.to_vector();
|
||||
let square_len = center
|
||||
.square_length()
|
||||
.min((center - PxVector::new(size.width, Px(0))).square_length())
|
||||
.min((center - size.to_vector()).square_length())
|
||||
.min((center - PxVector::new(Px(0), size.height)).square_length())
|
||||
.max(Px(0));
|
||||
Px((square_len.0 as f32).sqrt().round() as _)
|
||||
let min_sides = || {
|
||||
PxSize::new(
|
||||
center.x.min(size.width - center.x).max(Px(0)),
|
||||
center.y.min(size.height - center.y).max(Px(0)),
|
||||
)
|
||||
};
|
||||
let max_sides = || {
|
||||
PxSize::new(
|
||||
center.x.max(size.width - center.x).max(Px(0)),
|
||||
center.y.max(size.height - center.y).max(Px(0)),
|
||||
)
|
||||
};
|
||||
|
||||
let base_size = match self.base {
|
||||
GradientRadiusBase::ClosestSide => {
|
||||
let min = min_sides();
|
||||
if self.circle {
|
||||
PxSize::splat(min.width.min(min.height))
|
||||
} else {
|
||||
min
|
||||
}
|
||||
}
|
||||
GradientRadiusBase::ClosestCorner => {
|
||||
let min = min_sides();
|
||||
if self.circle {
|
||||
let s = min.cast::<f32>();
|
||||
let l = s.width.hypot(s.height);
|
||||
PxSize::splat(Px(l as _))
|
||||
} else {
|
||||
// preserve aspect-ratio of ClosestSide
|
||||
let s = std::f32::consts::FRAC_1_SQRT_2 * 2.0;
|
||||
PxSize::new(min.width * s, min.height * s)
|
||||
}
|
||||
}
|
||||
GradientRadiusBase::FarthestSide => {
|
||||
let max = max_sides();
|
||||
if self.circle {
|
||||
PxSize::splat(max.width.max(max.height))
|
||||
} else {
|
||||
max
|
||||
}
|
||||
}
|
||||
GradientRadiusBase::FarthestSide => center
|
||||
.x
|
||||
.max(center.y)
|
||||
.max(size.width - center.x)
|
||||
.max(size.height - center.y)
|
||||
.max(Px(0)),
|
||||
GradientRadiusBase::FarthestCorner => {
|
||||
let center = center.to_vector();
|
||||
let square_len = center
|
||||
.square_length()
|
||||
.max((center - PxVector::new(size.width, Px(0))).square_length())
|
||||
.max((center - size.to_vector()).square_length())
|
||||
.max((center - PxVector::new(Px(0), size.height)).square_length())
|
||||
.max(Px(0));
|
||||
Px((square_len.0 as f32).sqrt().round() as _)
|
||||
let max = max_sides();
|
||||
if self.circle {
|
||||
let s = max.cast::<f32>();
|
||||
let l = s.width.hypot(s.height);
|
||||
PxSize::splat(Px(l as _))
|
||||
} else {
|
||||
let s = std::f32::consts::FRAC_1_SQRT_2 * 2.0;
|
||||
PxSize::new(max.width * s, max.height * s)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LAYOUT.with_constraints(PxConstraints2d::new_exact(length, length), || {
|
||||
self.radii.layout_dft(PxSize::splat(length))
|
||||
})
|
||||
LAYOUT.with_constraints(PxConstraints2d::new_exact_size(base_size), || self.radii.layout_dft(base_size))
|
||||
}
|
||||
}
|
||||
impl_from_and_into_var! {
|
||||
/// Circle fill the base radius.
|
||||
/// Ellipse fill the base radius.
|
||||
fn from(base: GradientRadiusBase) -> GradientRadius {
|
||||
GradientRadius {
|
||||
base,
|
||||
circle: false,
|
||||
radii: Size::fill()
|
||||
}
|
||||
}
|
||||
|
||||
/// From [`GradientRadiusBase`] and circle radius.
|
||||
/// Ellipse [`GradientRadiusBase`] and ellipse radius.
|
||||
fn from<B: Into<GradientRadiusBase>, R: Into<Length>>((base, radius): (B, R)) -> GradientRadius {
|
||||
GradientRadius {
|
||||
base: base.into(),
|
||||
circle: false,
|
||||
radii: Size::splat(radius)
|
||||
}
|
||||
}
|
||||
|
||||
/// Circle [`GradientRadius::farthest_corner`].
|
||||
/// Ellipse [`GradientRadius::farthest_corner`].
|
||||
fn from(radius: Length) -> GradientRadius {
|
||||
GradientRadius::farthest_corner(radius)
|
||||
}
|
||||
/// Circle [`GradientRadius::farthest_corner_ell`].
|
||||
/// Ellipse [`GradientRadius::farthest_corner`].
|
||||
fn from(radii: Size) -> GradientRadius {
|
||||
GradientRadius::farthest_corner_ell(radii)
|
||||
GradientRadius::farthest_corner(radii)
|
||||
}
|
||||
|
||||
/// Conversion to [`Length::Relative`] and to radius.
|
||||
|
|
|
@ -29,7 +29,7 @@ pub fn linear_gradient<A: IntoVar<LinearGradientAxis>, S: IntoVar<GradientStops>
|
|||
gradient(stops).linear(axis)
|
||||
}
|
||||
|
||||
/// Starts building a radial gradient with the axis and color stops.
|
||||
/// Starts building a radial gradient with the radius and color stops.
|
||||
///
|
||||
/// Returns a node that is also a builder that can be used to refine the gradient definition.
|
||||
pub fn radial_gradient<C, R, S>(center: C, radius: R, stops: S) -> RadialGradient<S::Var, C::Var, R::Var, LocalVar<ExtendMode>>
|
||||
|
@ -41,7 +41,7 @@ where
|
|||
gradient(stops).radial(center, radius)
|
||||
}
|
||||
|
||||
/// Starts building a conic gradient with the axis and color stops.
|
||||
/// Starts building a conic gradient with the angle and color stops.
|
||||
///
|
||||
/// Returns a node that is also a builder that can be used to refine the gradient definition.
|
||||
pub fn conic_gradient<C, A, S>(center: C, angle: A, stops: S) -> ConicGradient<S::Var, C::Var, A::Var, LocalVar<ExtendMode>>
|
||||
|
|
|
@ -8,7 +8,6 @@ pub mod scrollbar;
|
|||
pub mod thumb;
|
||||
|
||||
mod scroll_properties;
|
||||
pub mod overscroll;
|
||||
pub use scroll_properties::*;
|
||||
|
||||
mod types;
|
||||
|
@ -36,7 +35,6 @@ impl Scroll {
|
|||
focusable = true;
|
||||
focus_scope = true;
|
||||
focus_scope_behavior = crate::core::focus::FocusScopeOnFocus::FirstDescendant;
|
||||
foreground = overscroll::over_scroll_node();
|
||||
}
|
||||
self.widget_builder().push_build_action(on_build);
|
||||
}
|
||||
|
@ -75,7 +73,8 @@ fn on_build(wgt: &mut WidgetBuilding) {
|
|||
let clip_to_viewport = wgt.capture_var_or_default(property_id!(clip_to_viewport));
|
||||
|
||||
wgt.push_intrinsic(NestGroup::CHILD_CONTEXT, "scroll_node", |child| {
|
||||
scroll_node(child, mode, clip_to_viewport)
|
||||
let child = scroll_node(child, mode, clip_to_viewport);
|
||||
nodes::overscroll_node(child)
|
||||
});
|
||||
|
||||
wgt.push_intrinsic(NestGroup::EVENT, "commands", |child| {
|
||||
|
@ -100,7 +99,10 @@ fn on_build(wgt: &mut WidgetBuilding) {
|
|||
let child = SCROLL.config_node(child);
|
||||
|
||||
let child = with_context_var(child, SCROLL_VERTICAL_OFFSET_VAR, var(0.fct()));
|
||||
with_context_var(child, SCROLL_HORIZONTAL_OFFSET_VAR, var(0.fct()))
|
||||
let child = with_context_var(child, SCROLL_HORIZONTAL_OFFSET_VAR, var(0.fct()));
|
||||
|
||||
let child = with_context_var(child, OVERSCROLL_VERTICAL_OFFSET_VAR, var(0.fct()));
|
||||
with_context_var(child, OVERSCROLL_HORIZONTAL_OFFSET_VAR, var(0.fct()))
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ use crate::prelude::new_widget::*;
|
|||
|
||||
use crate::core::{
|
||||
focus::FOCUS_CHANGED_EVENT,
|
||||
gradient::{ExtendMode, RenderGradientStop},
|
||||
mouse::{MouseScrollDelta, MOUSE_WHEEL_EVENT},
|
||||
touch::{TouchPhase, TOUCH_TRANSFORM_EVENT},
|
||||
};
|
||||
|
@ -769,3 +770,120 @@ pub fn scroll_wheel_node(child: impl UiNode) -> impl UiNode {
|
|||
_ => {}
|
||||
})
|
||||
}
|
||||
|
||||
/// Overscroll visual indicator.
|
||||
pub fn overscroll_node(child: impl UiNode) -> impl UiNode {
|
||||
let mut v_rect = PxRect::zero();
|
||||
let mut v_center = PxPoint::zero();
|
||||
let mut v_radius_w = Px(0);
|
||||
|
||||
let mut h_rect = PxRect::zero();
|
||||
let mut h_center = PxPoint::zero();
|
||||
let mut h_radius_h = Px(0);
|
||||
|
||||
match_node(child, move |c, op| match op {
|
||||
UiNodeOp::Init => {
|
||||
WIDGET
|
||||
.sub_var_layout(&OVERSCROLL_VERTICAL_OFFSET_VAR)
|
||||
.sub_var_layout(&OVERSCROLL_HORIZONTAL_OFFSET_VAR);
|
||||
}
|
||||
UiNodeOp::Layout { final_size, wl } => {
|
||||
*final_size = c.layout(wl);
|
||||
|
||||
let mut new_v_rect = PxRect::zero();
|
||||
let v = OVERSCROLL_VERTICAL_OFFSET_VAR.get();
|
||||
if v < 0.fct() {
|
||||
// overscroll top
|
||||
new_v_rect.size = *final_size;
|
||||
new_v_rect.size.height *= v.abs().min(0.1.fct());
|
||||
v_center.y = Px(0);
|
||||
} else if v > 0.fct() {
|
||||
// overscroll bottom
|
||||
new_v_rect.size = *final_size;
|
||||
new_v_rect.size.height *= v.abs().min(0.1.fct());
|
||||
new_v_rect.origin.y = final_size.height - new_v_rect.size.height;
|
||||
v_center.y = new_v_rect.size.height;
|
||||
}
|
||||
|
||||
let mut new_h_rect = PxRect::zero();
|
||||
let h = OVERSCROLL_HORIZONTAL_OFFSET_VAR.get();
|
||||
if h < 0.fct() {
|
||||
// overscroll left
|
||||
new_h_rect.size = *final_size;
|
||||
new_h_rect.size.width *= h.abs().min(0.1.fct());
|
||||
h_center.x = Px(0);
|
||||
} else if h > 0.fct() {
|
||||
// overscroll right
|
||||
new_h_rect.size = *final_size;
|
||||
new_h_rect.size.width *= h.abs().min(0.1.fct());
|
||||
new_h_rect.origin.x = final_size.width - new_h_rect.size.width;
|
||||
h_center.x = new_h_rect.size.width;
|
||||
}
|
||||
|
||||
if new_v_rect != v_rect {
|
||||
v_rect = new_v_rect;
|
||||
// 50%
|
||||
v_center.x = v_rect.size.width / Px(2);
|
||||
// 110%
|
||||
let radius = v_center.x;
|
||||
v_radius_w = radius + radius * 0.1;
|
||||
|
||||
WIDGET.render();
|
||||
}
|
||||
if new_h_rect != h_rect {
|
||||
h_rect = new_h_rect;
|
||||
h_center.y = h_rect.size.height / Px(2);
|
||||
let radius = h_center.y;
|
||||
h_radius_h = radius + radius * 0.1;
|
||||
WIDGET.render();
|
||||
}
|
||||
}
|
||||
UiNodeOp::Render { frame } => {
|
||||
c.render(frame);
|
||||
|
||||
frame.with_auto_hit_test(false, |frame| {
|
||||
let color = OVERSCROLL_COLOR_VAR.get().into();
|
||||
let stops = [
|
||||
RenderGradientStop { offset: 0.0, color },
|
||||
RenderGradientStop { offset: 0.99, color },
|
||||
RenderGradientStop {
|
||||
offset: 1.0,
|
||||
color: {
|
||||
let mut c = color;
|
||||
c.a = 0.0;
|
||||
c
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if !v_rect.size.is_empty() {
|
||||
let mut radius = v_rect.size;
|
||||
radius.width = v_radius_w;
|
||||
frame.push_radial_gradient(
|
||||
v_rect,
|
||||
v_center,
|
||||
radius,
|
||||
&stops,
|
||||
ExtendMode::Clamp.into(),
|
||||
v_rect.size,
|
||||
PxSize::zero(),
|
||||
);
|
||||
}
|
||||
if !h_rect.size.is_empty() {
|
||||
let mut radius = h_rect.size;
|
||||
radius.height = h_radius_h;
|
||||
frame.push_radial_gradient(
|
||||
h_rect,
|
||||
h_center,
|
||||
radius,
|
||||
&stops,
|
||||
ExtendMode::Clamp.into(),
|
||||
h_rect.size,
|
||||
PxSize::zero(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
//! Over-scroll visual indicator.
|
||||
|
||||
use crate::prelude::new_widget::*;
|
||||
|
||||
use super::{OVERSCROLL_HORIZONTAL_OFFSET_VAR, OVERSCROLL_VERTICAL_OFFSET_VAR};
|
||||
|
||||
/// Visual indicator when a touch scroll attempts to scroll past the limit.
|
||||
#[widget($crate::widgets::scroll::overscroll::OverScroll)]
|
||||
pub struct OverScroll(WidgetBase);
|
||||
impl OverScroll {
|
||||
fn widget_intrinsic(&mut self) {
|
||||
self.widget_builder().push_build_action(|wgt| {
|
||||
wgt.set_child(over_scroll_node());
|
||||
});
|
||||
|
||||
widget_set! {
|
||||
self;
|
||||
interactive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn over_scroll_node() -> impl UiNode {
|
||||
let mut v_rect = PxRect::zero();
|
||||
match_node_leaf(move |op| match op {
|
||||
UiNodeOp::Init => {
|
||||
WIDGET
|
||||
.sub_var_layout(&OVERSCROLL_VERTICAL_OFFSET_VAR)
|
||||
.sub_var_layout(&OVERSCROLL_HORIZONTAL_OFFSET_VAR);
|
||||
}
|
||||
UiNodeOp::Layout { final_size, .. } => {
|
||||
*final_size = LAYOUT.constraints().fill_size();
|
||||
|
||||
let mut new_v_rect = PxRect::zero();
|
||||
|
||||
let v = OVERSCROLL_VERTICAL_OFFSET_VAR.get();
|
||||
if dbg!(v) < 0.fct() {
|
||||
new_v_rect.size = *final_size;
|
||||
new_v_rect.size.height *= v.abs().min(0.1.fct());
|
||||
} else if v > 0.fct() {
|
||||
new_v_rect.size = *final_size;
|
||||
new_v_rect.size.height *= v.abs().min(0.1.fct());
|
||||
new_v_rect.origin.y = final_size.height - v_rect.size.height;
|
||||
}
|
||||
|
||||
if new_v_rect != v_rect {
|
||||
v_rect = new_v_rect;
|
||||
WIDGET.render();
|
||||
}
|
||||
}
|
||||
UiNodeOp::Render { frame } => {
|
||||
if !v_rect.size.is_empty() {
|
||||
frame.push_color(v_rect, FrameValue::Value(colors::RED.into()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -79,6 +79,9 @@ context_var! {
|
|||
///
|
||||
/// By default is `500.dip().min(100.pct())`, one full viewport extra capped at 500.
|
||||
pub static AUTO_HIDE_EXTRA_VAR: SideOffsets = 500.dip().min(100.pct());
|
||||
|
||||
/// Color of the overscroll indicator.
|
||||
pub static OVERSCROLL_COLOR_VAR: Rgba = colors::GRAY.with_alpha(50.pct());
|
||||
}
|
||||
|
||||
fn default_scrollbar() -> WidgetFn<ScrollBarArgs> {
|
||||
|
@ -233,6 +236,15 @@ pub fn auto_hide_extra(child: impl UiNode, extra: impl IntoVar<SideOffsets>) ->
|
|||
with_context_var(child, AUTO_HIDE_EXTRA_VAR, extra)
|
||||
}
|
||||
|
||||
/// Color of the overscroll indicator.
|
||||
///
|
||||
/// The overscroll indicator appears when touch scroll tries to scroll past an edge in a dimension
|
||||
/// that can scroll.
|
||||
#[property(CONTEXT, default(OVERSCROLL_COLOR_VAR), widget_impl(Scroll))]
|
||||
pub fn overscroll_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
|
||||
with_context_var(child, OVERSCROLL_COLOR_VAR, color)
|
||||
}
|
||||
|
||||
/// Arguments for scrollbar widget functions.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ScrollBarArgs {
|
||||
|
|
Loading…
Reference in New Issue