跳转至

ui/widget/material/base/state_machine.cpp

Material Design State Machine Implementation. More...

Namespaces

Name
cf
cf::ui
cf::ui::widget
cf::ui::widget::material
cf::ui::widget::material::base

Detailed Description

Material Design State Machine Implementation.

Author: Material Design Framework Team

Version: 0.1

Since: 0.1

Date: 2026-02-28

Manages visual state transitions for Material widgets, driving StateLayer opacity animations following Material Design 3 specifications.

State opacity values (Material Design 3):

  • Normal: 0.00
  • Hovered: 0.08
  • Pressed: 0.12
  • Focused: 0.12
  • Dragged: 0.16
  • Disabled: 0.00

Source code

#include "state_machine.h"
#include "components/animation.h"
#include "components/material/cfmaterial_animation_factory.h"
#include "components/material/cfmaterial_animation_strategy.h"

namespace cf::ui::widget::material::base {

using namespace cf::ui::components::material;
using namespace cf::ui::components;

StateMachine::StateMachine(cf::WeakPtr<components::material::CFMaterialAnimationFactory> factory,
                           QObject* parent)
    : QObject(parent), m_state(State::StateNormal), m_opacity(0.0f) {
    // WeakPtr is stored but not locked here to avoid circular dependency
    // Store the WeakPtr in a member variable for later use
    m_animator = factory;
}

StateMachine::~StateMachine() {
    cancelCurrentAnimation();
}

float StateMachine::targetOpacityForState(States s) const {
    // Priority order: Disabled > Pressed > Dragged > Focused > Hovered > Normal
    // When multiple states are active, use the highest priority

    if (s & State::StateDisabled) {
        return 0.0f;
    }
    if (s & State::StatePressed) {
        return 0.12f;
    }
    if (s & State::StateDragged) {
        return 0.16f;
    }
    if (s & State::StateFocused) {
        return 0.12f;
    }
    if (s & State::StateHovered) {
        return 0.08f;
    }
    if (s & State::StateChecked) {
        return 0.08f; // Checked state uses same as hovered
    }

    return 0.0f; // StateNormal
}

void StateMachine::cancelCurrentAnimation() {
    if (m_currentAnimation) {
        auto* anim = m_currentAnimation.Get();
        if (anim) {
            // Disconnect all signals from this animation to this object FIRST
            // This prevents any late-arriving progressChanged signals after we've stopped
            disconnect(anim, &components::ICFAbstractAnimation::progressChanged, this, nullptr);
            disconnect(anim, &components::ICFAbstractAnimation::finished, this, nullptr);
            // Stop the animation
            anim->stop();
        }
        m_currentAnimation = nullptr;
    }
    // Reset m_opacity to the correct target value for the current state
    // This ensures we don't carry over stale progress values from cancelled animations
    m_opacity = targetOpacityForState(m_state);
    emit stateLayerOpacityChanged(m_opacity);
}

void StateMachine::onAnimationFinished() {
    m_currentAnimation = nullptr;
    // Ensure m_opacity is exactly at the target value for the current state
    // This guards against any rounding errors or incomplete animations
    float targetOpacity = targetOpacityForState(m_state);
    if (m_opacity != targetOpacity) {
        m_opacity = targetOpacity;
        emit stateLayerOpacityChanged(m_opacity);
    }
}

void StateMachine::animateOpacityTo(float to) {
    // Performance mode check: if animations are disabled, set directly
    auto* factory = m_animator.Get();
    if (!factory || !factory->isAllEnabled()) {
        m_opacity = to;
        emit stateLayerOpacityChanged(m_opacity);
        return;
    }

    // CRITICAL FIX: Cancel any currently running animation before starting a new one.
    // This prevents multiple animations from competing to update m_opacity,
    // which causes visual glitches like flickering or incorrect opacity values
    // during rapid hover enter/leave events.
    cancelCurrentAnimation();

    // Start animation from current opacity value
    float from = m_opacity;

    // IMPORTANT: Use createAnimation instead of getAnimation to avoid sharing
    // the same animation instance across multiple StateMachines.
    // getAnimation returns a cached animation per token, which causes multiple
    // StateMachines to connect their lambdas to the same animation object,
    // resulting in cross-talk when any animation progresses.
    // createAnimation creates a separate animation instance per call.
    // See ElevationController::animatePressOffsetTo for the same pattern.
    AnimationDescriptor desc(
        "fade",                 // Animation type
        "md.motion.shortEnter", // Motion spec (short duration for hover states)
        "opacity",              // Property (we'll override with setRange)
        from,                   // Start value
        to                      // End value
    );

    auto anim = factory->createAnimation(desc, nullptr, this);
    if (!anim) {
        // Fallback: direct set if animation creation fails
        m_opacity = to;
        emit stateLayerOpacityChanged(m_opacity);
        return;
    }

    // Save animation reference for cancellation
    m_currentAnimation = anim;

    // Get raw pointer
    auto* rawAnim = anim.Get();

    // Connect progress signal
    // Note: Qt::UniqueConnection cannot be used with lambdas, but cancelCurrentAnimation()
    // disconnects all signals before starting a new animation, preventing duplicates
    //
    // CRITICAL: progressChanged emits 0-1 normalized progress, NOT actual opacity values.
    // We must interpolate between from and to to get the actual opacity.
    connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this,
            [this, from, to](float progress) {
                // progress is 0-1, interpolate to get actual opacity value
                m_opacity = from + (to - from) * progress;
                emit stateLayerOpacityChanged(m_opacity);
            });

    // Connect finished signal to clear the animation reference
    connect(rawAnim, &components::ICFAbstractAnimation::finished, this,
            &StateMachine::onAnimationFinished, Qt::UniqueConnection);

    // Start animation
    rawAnim->start(components::ICFAbstractAnimation::Direction::Forward);
}

// ============================================================================
// Event Handlers
// ============================================================================

void StateMachine::onHoverEnter() {
    if (m_state & State::StateDisabled)
        return;

    States oldState = m_state;
    m_state |= State::StateHovered;

    if (oldState != m_state) {
        emit stateChanged(m_state, oldState);
        animateOpacityTo(targetOpacityForState(m_state));
    }
}

void StateMachine::onHoverLeave() {
    States oldState = m_state;
    m_state &= ~static_cast<States>(State::StateHovered);

    if (oldState != m_state) {
        emit stateChanged(m_state, oldState);
        animateOpacityTo(targetOpacityForState(m_state));
    }
}

void StateMachine::onPress(const QPoint& pos) {
    Q_UNUSED(pos)
    if (m_state & State::StateDisabled)
        return;

    States oldState = m_state;
    m_state |= State::StatePressed;

    if (oldState != m_state) {
        emit stateChanged(m_state, oldState);
        animateOpacityTo(targetOpacityForState(m_state));
    }
}

void StateMachine::onRelease() {
    States oldState = m_state;
    m_state &= ~static_cast<States>(State::StatePressed);

    if (oldState != m_state) {
        emit stateChanged(m_state, oldState);
        animateOpacityTo(targetOpacityForState(m_state));
    }
}

void StateMachine::onFocusIn() {
    if (m_state & State::StateDisabled)
        return;

    States oldState = m_state;
    m_state |= State::StateFocused;

    if (oldState != m_state) {
        emit stateChanged(m_state, oldState);
        animateOpacityTo(targetOpacityForState(m_state));
    }
}

void StateMachine::onFocusOut() {
    States oldState = m_state;
    m_state &= ~static_cast<States>(State::StateFocused);

    if (oldState != m_state) {
        emit stateChanged(m_state, oldState);
        animateOpacityTo(targetOpacityForState(m_state));
    }
}

void StateMachine::onEnable() {
    States oldState = m_state;
    m_state &= ~static_cast<States>(State::StateDisabled);

    if (oldState != m_state) {
        emit stateChanged(m_state, oldState);
        animateOpacityTo(targetOpacityForState(m_state));
    }
}

void StateMachine::onDisable() {
    States oldState = m_state;
    m_state |= State::StateDisabled;

    if (oldState != m_state) {
        emit stateChanged(m_state, oldState);
        animateOpacityTo(targetOpacityForState(m_state));
    }
}

void StateMachine::onCheckedChanged(bool checked) {
    if (m_state & State::StateDisabled)
        return;

    States oldState = m_state;

    if (checked) {
        m_state |= State::StateChecked;
    } else {
        m_state &= ~static_cast<States>(State::StateChecked);
    }

    if (oldState != m_state) {
        emit stateChanged(m_state, oldState);
        animateOpacityTo(targetOpacityForState(m_state));
    }
}

// ============================================================================
// State Queries
// ============================================================================

StateMachine::States StateMachine::currentState() const {
    return m_state;
}

bool StateMachine::hasState(State s) const {
    return (m_state & s) != States();
}

float StateMachine::stateLayerOpacity() const {
    return m_opacity;
}

} // namespace cf::ui::widget::material::base

Updated on 2026-03-09 at 10:14:01 +0000