Skip to content

Commit

Permalink
Prepare <input type=checkbox switch> for multiple animation types
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=265937

Reviewed by Aditya Keerthi.

On iOS the switch control will perform two separate types of
animations. Make it so that the infrastructure supports that in
principle.

Also prepare for supporting non-mouse events.

* Source/WebCore/html/CheckboxInputType.cpp:
(WebCore::CheckboxInputType::handleMouseDownEvent):
(WebCore::CheckboxInputType::handleMouseMoveEvent):
(WebCore::CheckboxInputType::willDispatchClick):
(WebCore::CheckboxInputType::startSwitchPointerTracking):
(WebCore::CheckboxInputType::disabledStateChanged):
(WebCore::CheckboxInputType::willUpdateCheckedness):
(WebCore::switchAnimationUpdateInterval):
(WebCore::switchAnimationDuration):
(WebCore::CheckboxInputType::switchAnimationStartTime const):
(WebCore::CheckboxInputType::setSwitchAnimationStartTime):
(WebCore::CheckboxInputType::isSwitchAnimating const):
(WebCore::CheckboxInputType::performSwitchAnimation):
(WebCore::CheckboxInputType::stopSwitchAnimation):
(WebCore::CheckboxInputType::switchAnimationProgress const):
(WebCore::CheckboxInputType::switchAnimationVisuallyOnProgress const):
(WebCore::CheckboxInputType::updateIsSwitchVisuallyOnFromAbsoluteLocation):
(WebCore::CheckboxInputType::switchAnimationTimerFired):
(WebCore::switchCheckedChangeAnimationUpdateInterval): Deleted.
(WebCore::CheckboxInputType::performSwitchCheckedChangeAnimation): Deleted.
(WebCore::CheckboxInputType::stopSwitchCheckedChangeAnimation): Deleted.
(WebCore::CheckboxInputType::switchCheckedChangeAnimationProgress const): Deleted.
(WebCore::CheckboxInputType::switchCheckedChangeAnimationTimerFired): Deleted.
* Source/WebCore/html/CheckboxInputType.h:
* Source/WebCore/html/HTMLInputElement.cpp:
(WebCore::HTMLInputElement::switchAnimationVisuallyOnProgress const):
(WebCore::HTMLInputElement::switchCheckedChangeAnimationProgress const): Deleted.
* Source/WebCore/html/HTMLInputElement.h:
* Source/WebCore/rendering/RenderTheme.cpp:
(WebCore::updateSwitchThumbPartForRenderer):
(WebCore::updateSwitchTrackPartForRenderer):
* Source/WebCore/rendering/RenderTheme.h:
(WebCore::RenderTheme::switchAnimationVisuallyOnDuration const):
(WebCore::RenderTheme::switchCheckedChangeAnimationDuration const): Deleted.
* Source/WebCore/rendering/RenderThemeMac.h:

Canonical link: https://commits.webkit.org/271607@main
  • Loading branch information
annevk committed Dec 6, 2023
1 parent 5f4ea7c commit 942d831
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 71 deletions.
145 changes: 88 additions & 57 deletions Source/WebCore/html/CheckboxInputType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ void CheckboxInputType::handleMouseDownEvent(MouseEvent& event)
if (!event.isTrusted() || !isSwitch() || element()->isDisabledFormControl() || !element()->renderer())
return;
m_isSwitchVisuallyOn = element()->checked();
startSwitchPointerTracking(element()->renderer()->absoluteToLocal(event.absoluteLocation(), UseTransforms).x());
startSwitchPointerTracking(event.absoluteLocation());
}

void CheckboxInputType::handleMouseMoveEvent(MouseEvent& event)
Expand All @@ -114,30 +114,7 @@ void CheckboxInputType::handleMouseMoveEvent(MouseEvent& event)
return;
}

auto isSwitchVisuallyOn = m_isSwitchVisuallyOn;
auto isRTL = element()->computedStyle()->direction() == TextDirection::RTL;
auto switchThumbIsLeft = (!isRTL && !isSwitchVisuallyOn) || (isRTL && isSwitchVisuallyOn);
auto xPosition = element()->renderer()->absoluteToLocal(event.absoluteLocation(), UseTransforms).x();
auto switchTrackRect = element()->renderer()->absoluteBoundingBoxRect();
auto switchThumbLength = switchTrackRect.height();
auto switchTrackWidth = switchTrackRect.width();

auto changePosition = switchTrackWidth / 2;
if (!m_hasSwitchVisuallyOnChanged) {
auto switchTrackNoThumbWidth = switchTrackWidth - switchThumbLength;
auto changeOffset = switchTrackWidth * RenderTheme::singleton().switchPointerTrackingMagnitudeProportion();
if (switchThumbIsLeft && *m_switchPointerTrackingXPositionStart > switchTrackNoThumbWidth)
changePosition = *m_switchPointerTrackingXPositionStart + changeOffset;
else if (!switchThumbIsLeft && *m_switchPointerTrackingXPositionStart < switchTrackNoThumbWidth)
changePosition = *m_switchPointerTrackingXPositionStart - changeOffset;
}

auto switchThumbIsLeftNow = xPosition < changePosition;
if (switchThumbIsLeftNow != switchThumbIsLeft) {
m_hasSwitchVisuallyOnChanged = true;
m_isSwitchVisuallyOn = !m_isSwitchVisuallyOn;
performSwitchCheckedChangeAnimation();
}
updateIsSwitchVisuallyOnFromAbsoluteLocation(event.absoluteLocation());
}

void CheckboxInputType::willDispatchClick(InputElementClickState& state)
Expand All @@ -161,7 +138,7 @@ void CheckboxInputType::willDispatchClick(InputElementClickState& state)
element()->setChecked(!state.checked, state.trusted ? WasSetByJavaScript::No : WasSetByJavaScript::Yes);

if (isSwitch() && state.trusted && !(isSwitchPointerTracking() && m_hasSwitchVisuallyOnChanged && m_isSwitchVisuallyOn == !state.checked))
performSwitchCheckedChangeAnimation();
performSwitchAnimation(SwitchAnimationType::VisuallyOn);

stopSwitchPointerTracking();
}
Expand All @@ -179,12 +156,14 @@ void CheckboxInputType::didDispatchClick(Event& event, const InputElementClickSt
event.setDefaultHandled();
}

void CheckboxInputType::startSwitchPointerTracking(int xPositionStart)
void CheckboxInputType::startSwitchPointerTracking(LayoutPoint absoluteLocation)
{
ASSERT(element());
ASSERT(element()->renderer());
if (RefPtr frame = element()->document().frame()) {
frame->eventHandler().setCapturingMouseEventsElement(element());
m_switchPointerTrackingXPositionStart = xPositionStart;
m_isSwitchVisuallyOn = element()->checked();
m_switchPointerTrackingXPositionStart = element()->renderer()->absoluteToLocal(absoluteLocation, UseTransforms).x();
}
}

Expand Down Expand Up @@ -215,7 +194,7 @@ void CheckboxInputType::disabledStateChanged()
{
ASSERT(element());
if (isSwitch() && element()->isDisabledFormControl()) {
stopSwitchCheckedChangeAnimation();
stopSwitchAnimation(SwitchAnimationType::VisuallyOn);
stopSwitchPointerTracking();
}
}
Expand All @@ -224,21 +203,41 @@ void CheckboxInputType::willUpdateCheckedness(bool, WasSetByJavaScript wasChecke
{
ASSERT(element());
if (isSwitch() && wasCheckedByJavaScript == WasSetByJavaScript::Yes) {
stopSwitchCheckedChangeAnimation();
stopSwitchAnimation(SwitchAnimationType::VisuallyOn);
stopSwitchPointerTracking();
}
}

// FIXME: ideally CheckboxInputType would not be responsible for the timer specifics and instead
// ask a more knowledgable system for a refresh callback (perhaps passing a desired FPS).
static Seconds switchCheckedChangeAnimationUpdateInterval(HTMLInputElement* element)
static Seconds switchAnimationUpdateInterval(HTMLInputElement* element)
{
if (auto* page = element->document().page())
return page->preferredRenderingUpdateInterval();
return 0_s;
}

void CheckboxInputType::performSwitchCheckedChangeAnimation()
static Seconds switchAnimationDuration(SwitchAnimationType)
{
return RenderTheme::singleton().switchAnimationVisuallyOnDuration();
}

Seconds CheckboxInputType::switchAnimationStartTime(SwitchAnimationType) const
{
return m_switchAnimationVisuallyOnStartTime;
}

void CheckboxInputType::setSwitchAnimationStartTime(SwitchAnimationType, Seconds time)
{
m_switchAnimationVisuallyOnStartTime = time;
}

bool CheckboxInputType::isSwitchAnimating(SwitchAnimationType type) const
{
return switchAnimationStartTime(type) != 0_s;
}

void CheckboxInputType::performSwitchAnimation(SwitchAnimationType type)
{
ASSERT(isSwitch());
ASSERT(element());
Expand All @@ -247,43 +246,47 @@ void CheckboxInputType::performSwitchCheckedChangeAnimation()
if (!element()->renderer()->style().hasEffectiveAppearance())
return;

auto updateInterval = switchCheckedChangeAnimationUpdateInterval(element());
auto duration = RenderTheme::singleton().switchCheckedChangeAnimationDuration();
auto updateInterval = switchAnimationUpdateInterval(element());
auto duration = switchAnimationDuration(type);

if (!m_switchCheckedChangeAnimationTimer) {
if (!m_switchAnimationTimer) {
if (!(duration > 0_s && updateInterval > 0_s))
return;
m_switchCheckedChangeAnimationTimer = makeUnique<Timer>(*this, &CheckboxInputType::switchCheckedChangeAnimationTimerFired);
m_switchAnimationTimer = makeUnique<Timer>(*this, &CheckboxInputType::switchAnimationTimerFired);
}
ASSERT(duration > 0_s);
ASSERT(updateInterval > 0_s);
ASSERT(m_switchCheckedChangeAnimationTimer);
ASSERT(m_switchAnimationTimer);

auto isAnimating = m_switchCheckedChangeAnimationStartTime != 0_s;
auto currentTime = MonotonicTime::now().secondsSinceEpoch();
auto remainingTime = currentTime - m_switchCheckedChangeAnimationStartTime;
auto remainingTime = currentTime - switchAnimationStartTime(type);
auto startTimeOffset = 0_s;
if (isAnimating && remainingTime < duration)
if (isSwitchAnimating(type) && remainingTime < duration)
startTimeOffset = duration - remainingTime;

m_switchCheckedChangeAnimationStartTime = MonotonicTime::now().secondsSinceEpoch() - startTimeOffset;
m_switchCheckedChangeAnimationTimer->startOneShot(updateInterval);
setSwitchAnimationStartTime(type, MonotonicTime::now().secondsSinceEpoch() - startTimeOffset);
m_switchAnimationTimer->startOneShot(updateInterval);
}

void CheckboxInputType::stopSwitchAnimation(SwitchAnimationType type)
{
setSwitchAnimationStartTime(type, 0_s);
}

void CheckboxInputType::stopSwitchCheckedChangeAnimation()
float CheckboxInputType::switchAnimationProgress(SwitchAnimationType type) const
{
m_switchCheckedChangeAnimationStartTime = 0_s;
if (!isSwitchAnimating(type))
return 1.0f;
auto duration = switchAnimationDuration(type);
return std::min((float)((MonotonicTime::now().secondsSinceEpoch() - switchAnimationStartTime(type)) / duration), 1.0f);
}

float CheckboxInputType::switchCheckedChangeAnimationProgress() const
float CheckboxInputType::switchAnimationVisuallyOnProgress() const
{
ASSERT(isSwitch());
ASSERT(switchAnimationDuration(SwitchAnimationType::VisuallyOn) > 0_s);

auto isAnimating = m_switchCheckedChangeAnimationStartTime != 0_s;
if (!isAnimating)
return 1.0f;
auto duration = RenderTheme::singleton().switchCheckedChangeAnimationDuration();
return std::min((float)((MonotonicTime::now().secondsSinceEpoch() - m_switchCheckedChangeAnimationStartTime) / duration), 1.0f);
return switchAnimationProgress(SwitchAnimationType::VisuallyOn);
}

bool CheckboxInputType::isSwitchVisuallyOn() const
Expand All @@ -293,22 +296,50 @@ bool CheckboxInputType::isSwitchVisuallyOn() const
return isSwitchPointerTracking() ? m_isSwitchVisuallyOn : element()->checked();
}

void CheckboxInputType::switchCheckedChangeAnimationTimerFired()
void CheckboxInputType::updateIsSwitchVisuallyOnFromAbsoluteLocation(LayoutPoint absoluteLocation)
{
auto xPosition = element()->renderer()->absoluteToLocal(absoluteLocation, UseTransforms).x();
auto isSwitchVisuallyOn = m_isSwitchVisuallyOn;
auto isRTL = element()->computedStyle()->direction() == TextDirection::RTL;
auto switchThumbIsLeft = (!isRTL && !isSwitchVisuallyOn) || (isRTL && isSwitchVisuallyOn);
auto switchTrackRect = element()->renderer()->absoluteBoundingBoxRect();
auto switchThumbLength = switchTrackRect.height();
auto switchTrackWidth = switchTrackRect.width();

auto changePosition = switchTrackWidth / 2;
if (!m_hasSwitchVisuallyOnChanged) {
auto switchTrackNoThumbWidth = switchTrackWidth - switchThumbLength;
auto changeOffset = switchTrackWidth * RenderTheme::singleton().switchPointerTrackingMagnitudeProportion();
if (switchThumbIsLeft && *m_switchPointerTrackingXPositionStart > switchTrackNoThumbWidth)
changePosition = *m_switchPointerTrackingXPositionStart + changeOffset;
else if (!switchThumbIsLeft && *m_switchPointerTrackingXPositionStart < switchTrackNoThumbWidth)
changePosition = *m_switchPointerTrackingXPositionStart - changeOffset;
}

auto switchThumbIsLeftNow = xPosition < changePosition;
if (switchThumbIsLeftNow != switchThumbIsLeft) {
m_hasSwitchVisuallyOnChanged = true;
m_isSwitchVisuallyOn = !m_isSwitchVisuallyOn;
performSwitchAnimation(SwitchAnimationType::VisuallyOn);
}
}

void CheckboxInputType::switchAnimationTimerFired()
{
ASSERT(m_switchCheckedChangeAnimationTimer);
ASSERT(m_switchAnimationTimer);
if (!isSwitch() || !element() || !element()->renderer())
return;

auto updateInterval = switchCheckedChangeAnimationUpdateInterval(element());
auto updateInterval = switchAnimationUpdateInterval(element());
if (!(updateInterval > 0_s))
return;

auto currentTime = MonotonicTime::now().secondsSinceEpoch();
auto duration = RenderTheme::singleton().switchCheckedChangeAnimationDuration();
if (currentTime - m_switchCheckedChangeAnimationStartTime < duration)
m_switchCheckedChangeAnimationTimer->startOneShot(updateInterval);
auto isVisuallyOnOngoing = currentTime - switchAnimationStartTime(SwitchAnimationType::VisuallyOn) < switchAnimationDuration(SwitchAnimationType::VisuallyOn);
if (isVisuallyOnOngoing)
m_switchAnimationTimer->startOneShot(updateInterval);
else
stopSwitchCheckedChangeAnimation();
stopSwitchAnimation(SwitchAnimationType::VisuallyOn);

element()->renderer()->repaint();
}
Expand Down
21 changes: 14 additions & 7 deletions Source/WebCore/html/CheckboxInputType.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ namespace WebCore {

enum class WasSetByJavaScript : bool;

enum class SwitchAnimationType { VisuallyOn };

class CheckboxInputType final : public BaseCheckableInputType {
public:
static Ref<CheckboxInputType> create(HTMLInputElement& element)
Expand All @@ -44,7 +46,7 @@ class CheckboxInputType final : public BaseCheckableInputType {
}

bool valueMissing(const String&) const final;
float switchCheckedChangeAnimationProgress() const;
float switchAnimationVisuallyOnProgress() const;
bool isSwitchVisuallyOn() const;

private:
Expand All @@ -59,25 +61,30 @@ class CheckboxInputType final : public BaseCheckableInputType {
void handleKeyupEvent(KeyboardEvent&) final;
void handleMouseDownEvent(MouseEvent&) final;
void handleMouseMoveEvent(MouseEvent&) final;
void startSwitchPointerTracking(int);
void startSwitchPointerTracking(LayoutPoint);
void stopSwitchPointerTracking();
bool isSwitchPointerTracking() const;
void willDispatchClick(InputElementClickState&) final;
void didDispatchClick(Event&, const InputElementClickState&) final;
bool matchesIndeterminatePseudoClass() const final;
void willUpdateCheckedness(bool /* nowChecked */, WasSetByJavaScript);
void disabledStateChanged() final;
void performSwitchCheckedChangeAnimation();
void stopSwitchCheckedChangeAnimation();
void switchCheckedChangeAnimationTimerFired();
Seconds switchAnimationStartTime(SwitchAnimationType) const;
void setSwitchAnimationStartTime(SwitchAnimationType, Seconds);
bool isSwitchAnimating(SwitchAnimationType) const;
void performSwitchAnimation(SwitchAnimationType);
void stopSwitchAnimation(SwitchAnimationType);
float switchAnimationProgress(SwitchAnimationType) const;
void updateIsSwitchVisuallyOnFromAbsoluteLocation(LayoutPoint);
void switchAnimationTimerFired();

// FIXME: Consider moving all switch-related state (and methods?) to their own object so
// CheckboxInputType can stay somewhat small.
std::optional<int> m_switchPointerTrackingXPositionStart { std::nullopt };
bool m_hasSwitchVisuallyOnChanged { false };
bool m_isSwitchVisuallyOn { false };
Seconds m_switchCheckedChangeAnimationStartTime { 0_s };
std::unique_ptr<Timer> m_switchCheckedChangeAnimationTimer;
Seconds m_switchAnimationVisuallyOnStartTime { 0_s };
std::unique_ptr<Timer> m_switchAnimationTimer;
};

} // namespace WebCore
Expand Down
4 changes: 2 additions & 2 deletions Source/WebCore/html/HTMLInputElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2304,10 +2304,10 @@ bool HTMLInputElement::dirAutoUsesValue() const
return m_inputType->dirAutoUsesValue();
}

float HTMLInputElement::switchCheckedChangeAnimationProgress() const
float HTMLInputElement::switchAnimationVisuallyOnProgress() const
{
ASSERT(isSwitch());
return checkedDowncast<CheckboxInputType>(*m_inputType).switchCheckedChangeAnimationProgress();
return checkedDowncast<CheckboxInputType>(*m_inputType).switchAnimationVisuallyOnProgress();
}

bool HTMLInputElement::isSwitchVisuallyOn() const
Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/html/HTMLInputElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ class HTMLInputElement : public HTMLTextFormControlElement {

bool hasEverBeenPasswordField() const { return m_hasEverBeenPasswordField; }

float switchCheckedChangeAnimationProgress() const;
float switchAnimationVisuallyOnProgress() const;
bool isSwitchVisuallyOn() const;

protected:
Expand Down
4 changes: 2 additions & 2 deletions Source/WebCore/rendering/RenderTheme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ static void updateSwitchThumbPartForRenderer(SwitchThumbPart& switchThumbPart, c
ASSERT(input.isSwitch());

switchThumbPart.setIsOn(input.isSwitchVisuallyOn());
switchThumbPart.setProgress(input.switchCheckedChangeAnimationProgress());
switchThumbPart.setProgress(input.switchAnimationVisuallyOnProgress());
}

static void updateSwitchTrackPartForRenderer(SwitchTrackPart& switchTrackPart, const RenderObject& renderer)
Expand All @@ -610,7 +610,7 @@ static void updateSwitchTrackPartForRenderer(SwitchTrackPart& switchTrackPart, c
ASSERT(input.isSwitch());

switchTrackPart.setIsOn(input.isSwitchVisuallyOn());
switchTrackPart.setProgress(input.switchCheckedChangeAnimationProgress());
switchTrackPart.setProgress(input.switchAnimationVisuallyOnProgress());
}

RefPtr<ControlPart> RenderTheme::createControlPart(const RenderObject& renderer) const
Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/rendering/RenderTheme.h
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ class RenderTheme {
#if USE(SYSTEM_PREVIEW)
virtual void paintSystemPreviewBadge(Image&, const PaintInfo&, const FloatRect&);
#endif
virtual Seconds switchCheckedChangeAnimationDuration() const { return 0_s; }
virtual Seconds switchAnimationVisuallyOnDuration() const { return 0_s; }
float switchPointerTrackingMagnitudeProportion() const { return 0.4f; }

protected:
Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/rendering/RenderThemeMac.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class RenderThemeMac final : public RenderThemeCocoa {

WEBCORE_EXPORT static RetainPtr<NSImage> iconForAttachment(const String& fileName, const String& attachmentType, const String& title);

Seconds switchCheckedChangeAnimationDuration() const final { return 300_ms; }
Seconds switchAnimationVisuallyOnDuration() const final { return 300_ms; }

private:
RenderThemeMac();
Expand Down

0 comments on commit 942d831

Please sign in to comment.