blob: e57963ad9cbcb8250273b20d311f0a005b5789a8 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/ui/a11y/lib/magnifier/magnifier_2.h"
#include <lib/syslog/cpp/macros.h>
namespace a11y {
Magnifier2::Magnifier2(std::shared_ptr<Delegate> delegate) : delegate_(delegate) {}
void Magnifier2::ZoomOutIfMagnified() {
if (state_.mode != Mode::UNMAGNIFIED) {
state_.mode = Mode::UNMAGNIFIED;
TransitionOutOfZoom();
}
}
bool Magnifier2::State::operator==(const State& o) const {
return transition_rate == o.transition_rate && scale == o.scale && translation == o.translation;
}
bool Magnifier2::State::operator!=(const State& o) const { return !(*this == o); }
void Magnifier2::State::FocusOn(const ::fuchsia::math::PointF& focus) {
glm::vec2 focus_vec;
focus_vec.x = focus.x;
focus_vec.y = focus.y;
translation = -focus_vec * (scale - 1);
}
void Magnifier2::TransitionIntoZoom() {
state_.FocusOn(state_.gesture_context.CurrentCentroid(false /* use_local_coordinates */));
state_.scale = kDefaultScale;
state_.transition_rate = kTransitionRate;
UpdateTransform();
}
void Magnifier2::TransitionOutOfZoom() {
state_.transition_rate = -kTransitionRate;
UpdateTransform();
}
void Magnifier2::ClampTranslation() {
auto& translation = state_.translation;
const auto freedom = state_.scale - 1;
translation.x = std::clamp(translation.x, -freedom, freedom);
translation.y = std::clamp(translation.y, -freedom, freedom);
}
void Magnifier2::HandleTemporaryDrag(const Delta& delta) {
FX_DCHECK(state_.mode == Mode::TEMPORARY);
// If the zoom is temporary, treat the coordinate as a focal point, i.e.
// focus on the area that would be at that position unzoomed.
//
// Instead of using the raw centroid coordinate, which jumps around as
// fingers are added or removed, move the original tap coordinate by the
// delta.
state_.FocusOn(state_.gesture_context.CurrentCentroid(false /* use_local_coordinates */));
// Ensure that translation does not fall outside of sensical values.
ClampTranslation();
UpdateTransform();
}
void Magnifier2::HandlePersistentDrag(const Delta& delta) {
FX_DCHECK(state_.mode == Mode::PERSISTENT);
float& scale = state_.scale;
const float old_scale = scale;
scale *= delta.scale;
scale = std::clamp(scale, kMinScale, kMaxScale);
// Account for clamping for accurate anchor calculation
const float actual_delta_scale = scale / old_scale;
auto& translation = state_.translation;
// For persistent magnification, we want the UX to be a little bit different from temporary
// magnification. In persistent magnification, the user can pan and zoom using a two-finger drag
// gesture. As the distance between the user’s fingers change, the zoom should change
// proportionally to the change in distance (so moving the fingers twice as far apart will cause
// the scale to increase by a factor of 2). So, given the two fingers’ former and current
// locations, we can compute new_scale = old_scale * (new_distance_between_fingers /
// old_distance_between_fingers). To achieve panning, our goal is to keep the same point in
// unscaled space under the centroid of the drag at all times. To do so, we need to consider both
// the new and previous locations of the centroid (midpoint) between the two fingers. To determine
// the point in unscaled space that is under the previous centroid of the two fingers, we can
// simply apply the inverse of the current magnification transform to the previous centroid
// coordinates. Then, we can compute the new magnification transform by determining what
// translation will place the unscaled point at the new centroid location (after we’ve applied the
// new scale factor).
auto current_centroid =
ToVec2(state_.gesture_context.CurrentCentroid(false /* use_local_coordinates */));
translation =
current_centroid + delta.translation + actual_delta_scale * (translation - current_centroid);
// Ensure that translation does not fall outside of sensical values.
ClampTranslation();
UpdateTransform();
}
void Magnifier2::UpdateTransform() {
float& transition_rate = state_.transition_rate;
float& transition_progress = state_.transition_progress;
bool& update_pending = state_.update_pending;
bool& update_in_progress = state_.update_in_progress;
if (!delegate_) {
FX_LOGS(WARNING) << "No magnification delegate registered.";
// If there's no handler, don't bother animating.
if (transition_rate > 0) {
transition_progress = 1;
transition_rate = 0;
} else if (transition_rate < 0) {
transition_progress = 0;
transition_rate = 0;
}
return;
}
if (update_in_progress) {
update_pending = true; // We'll |UpdateTransform| on the next callback instead.
} else {
update_in_progress = true;
if (transition_rate != 0) {
transition_progress = std::clamp(transition_progress + transition_rate, 0.f, 1.f);
// If the transition rate is positive, then transition progress of 0
// indicates that the transition is just beginning, and transition
// progress of 1 indicatest that the transition is complete.
// For negative transition rate, transition progress decreases from 1 ->
// 0.
//
// Therefore, this if statement just checks if the transition is not yet
// complete.
if ((transition_rate > 0 && transition_progress < 1) ||
(transition_rate < 0 && transition_progress > 0)) {
update_pending = true;
} else {
transition_rate = 0;
}
}
auto translation_x = transition_progress * state_.translation.x;
auto translation_y = transition_progress * state_.translation.y;
auto scale = 1 + transition_progress * (state_.scale - 1);
delegate_->SetMagnificationTransform(scale, translation_x, translation_y, [this] {
state_.update_in_progress = false;
if (state_.update_pending) {
state_.update_pending = false;
UpdateTransform();
}
});
}
}
void Magnifier2::TogglePersistentMagnification() {
if (state_.mode == Mode::UNMAGNIFIED) {
state_.mode = Mode::PERSISTENT;
TransitionIntoZoom();
} else if (state_.mode == Mode::PERSISTENT) {
state_.mode = Mode::UNMAGNIFIED;
TransitionOutOfZoom();
}
}
void Magnifier2::BindGestures(a11y::GestureHandlerV2* gesture_handler) {
FX_DCHECK(gesture_handler);
// Add gestures with higher priority earlier than gestures with lower priority.
bool gesture_bind_status =
gesture_handler->BindMFingerNTapAction(1 /* number of fingers */, 3 /* number of taps */,
[this](a11y::gesture_util_v2::GestureContext context) {
state_.gesture_context = context;
// One-finger-triple-tap should be a NOOP in
// temporary magnification mode.
if (state_.mode == Mode::TEMPORARY) {
return;
}
TogglePersistentMagnification();
});
FX_DCHECK(gesture_bind_status);
gesture_bind_status =
gesture_handler->BindMFingerNTapAction(3 /* number of fingers */, 2 /* number of taps */,
[this](a11y::gesture_util_v2::GestureContext context) {
state_.gesture_context = context;
// Three-finger-double-tap should be a NOOP in
// temporary magnification mode.
if (state_.mode == Mode::TEMPORARY) {
return;
}
TogglePersistentMagnification();
});
FX_DCHECK(gesture_bind_status);
gesture_bind_status = gesture_handler->BindMFingerNTapDragAction(
[this](a11y::gesture_util_v2::GestureContext context) {
state_.gesture_context = context;
// Tap-drag gestures should only work to enable temporary magnification
// from an unmagnified state.
if (state_.mode != Mode::UNMAGNIFIED) {
return;
}
state_.mode = Mode::TEMPORARY;
TransitionIntoZoom();
}, /* on recognize */
[this](a11y::gesture_util_v2::GestureContext context) {
// We should be in TEMPORARY magnification mode if we hit this callback.
if (state_.mode != Mode::TEMPORARY) {
return;
}
auto delta = GetDelta(context /* current */, state_.gesture_context /* previous */,
0 /* scale_min_finger_radius */);
state_.gesture_context = context;
HandleTemporaryDrag(delta);
}, /* on update */
[this](a11y::gesture_util_v2::GestureContext context) {
state_.mode = Mode::UNMAGNIFIED;
TransitionOutOfZoom();
}, /* on complete */
1 /* number of fingers */, 3 /* number of taps */);
FX_DCHECK(gesture_bind_status);
gesture_bind_status = gesture_handler->BindMFingerNTapDragAction(
[this](a11y::gesture_util_v2::GestureContext context) {
state_.gesture_context = context;
// Tap-drag gestures should only work to enable temporary magnification
// from an unmagnified state.
if (state_.mode != Mode::UNMAGNIFIED) {
return;
}
state_.mode = Mode::TEMPORARY;
TransitionIntoZoom();
}, /* on recognize */
[this](a11y::gesture_util_v2::GestureContext context) {
// We should be in TEMPORARY magnification mode if we hit this callback.
if (state_.mode != Mode::TEMPORARY) {
return;
}
auto delta = GetDelta(context /* current */, state_.gesture_context /* previous */,
0 /* scale_min_finger_radius */);
state_.gesture_context = context;
HandleTemporaryDrag(delta);
}, /* on update */
[this](a11y::gesture_util_v2::GestureContext context) {
state_.mode = Mode::UNMAGNIFIED;
TransitionOutOfZoom();
}, /* on complete */
3 /* number of fingers */, 2 /* number of taps */);
FX_DCHECK(gesture_bind_status);
gesture_bind_status = gesture_handler->BindTwoFingerDragAction(
[this](a11y::gesture_util_v2::GestureContext context) {
// The magnifier should only respond to two-finger drags when in
// PERSISTENT magnification mode.
if (state_.mode != Mode::PERSISTENT) {
return;
}
// Since this is the first event received for the gesture, we don't have
// any previous context to compare the current one against, so we can't
// adjust the magnification transform meaningfully yet.
state_.gesture_context = context;
}, /* on recognize */
[this](a11y::gesture_util_v2::GestureContext context) {
// The magnifier should only respond to two-finger drags when in
// PERSISTENT magnification mode.
if (state_.mode != Mode::PERSISTENT) {
return;
}
auto delta = GetDelta(context /* current */, state_.gesture_context /* previous */,
kZoomMinFingerDistance);
HandlePersistentDrag(delta);
// We need to wait until after we've called HandlePersistentDrag() to
// update state_.gesture_context, becuase HandlePersistentDrag() needs
// to use the old gesture_context.
state_.gesture_context = context;
}, /* on update */
[](a11y::gesture_util_v2::GestureContext context) {
/* NOOP */
} /* on complete */);
FX_DCHECK(gesture_bind_status);
}
} // namespace a11y