Implemented support for elliptical radial gradients (new default).

Created overscroll visual.
This commit is contained in:
Samuel Guerra 2023-09-07 22:38:52 -03:00
parent 4bbf094bec
commit 4c82edfe49
7 changed files with 219 additions and 139 deletions

View File

@ -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

View File

@ -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.

View File

@ -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>>

View File

@ -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()))
});
}

View File

@ -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(),
);
}
});
}
_ => {}
})
}

View File

@ -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()));
}
}
_ => {}
})
}

View File

@ -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 {