Skip to content

ControlState precedence issue with button states #4409

Closed
@Bbalduzz

Description

@Bbalduzz

Duplicate Check

Describe the bug

When defining both HOVERED and PRESSED states for a button property using ControlStateValue, the PRESSED state is ignored if HOVERED is present. This appears to be due to a lack of state precedence handling, where the HOVERED state always takes precedence over PRESSED since the button is technically still being hovered while pressed.

Code sample

Code
import flet as ft


def main(page: ft.Page):
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.vertical_alignment = ft.MainAxisAlignment.CENTER
    button = ft.TextButton(
        "Test Button",
        style=ft.ButtonStyle(
            bgcolor={
                ft.ControlState.DEFAULT: ft.colors.with_opacity(0.786, "ffffff"),
                ft.ControlState.HOVERED: ft.colors.GREEN,
                ft.ControlState.PRESSED: ft.colors.BLUE,
            }
        ),
    )
    page.add(button)


ft.app(target=main)

When clicking the button:

  • On hover: Changes to HOVERED color
  • On press: Stays at HOVERED color instead of changing to PRESSED color
  • On release while still hovering: Stays at HOVERED color
  • On mouse leave: Returns to DEFAULT color

To reproduce

  1. run the repro code

Expected behavior

The button states should follow a clear precedence order with predictable transitions between states:

State Precedence (highest to lowest):

  1. DISABLED/ERROR (if present)
  2. PRESSED
  3. HOVERED
  4. DEFAULT

Screenshots / Videos

Captures
controlstate-bug.mp4

Operating System

Windows

Operating system details

windows 11

Flet version

0.24.1

Regression

I'm not sure / I don't know

Suggestions

in the _wrap_attr_dict method of the Control class, since this is where Flet handles the ControlState dictionary conversion, we can process the state precedence:

class Control:
  # ... etc
    
  def _wrap_attr_dict(self, value: Optional[Union[Dict, Any]]) -> Optional[Dict]:
      if value is None or isinstance(value, Dict):
          # If it's already a dictionary (state values), process for precedence
          if isinstance(value, Dict):
              return self._process_state_dict(value)
          return value
      return {ControlState.DEFAULT: value}
  
  def _process_state_dict(self, state_dict: Dict) -> Dict:
      """
      Process a state dictionary to ensure proper state precedence.
      Called internally by _wrap_attr_dict.
      """
      # If there's only one state, no need for precedence handling
      if len(state_dict) <= 1:
          return state_dict
          
      # Get the current active states
      active_states = set()
      if self.disabled:
          active_states.add(ControlState.DISABLED)
      if self._get_attr("error", data_type="bool", def_value=False):
          active_states.add(ControlState.ERROR)
      if self._get_attr("pressed", data_type="bool", def_value=False):
          active_states.add(ControlState.PRESSED)
      if self._get_attr("hovered", data_type="bool", def_value=False):
          active_states.add(ControlState.HOVERED)
      
      # Return value based on priority
      for state in [
          ControlState.DISABLED,
          ControlState.ERROR,
          ControlState.PRESSED,
          ControlState.HOVERED,
          ControlState.DEFAULT
      ]:
          if state in active_states and state in state_dict:
              return {ControlState.DEFAULT: state_dict[state]}
              
      # Fallback to original default or first available value
      return {ControlState.DEFAULT: state_dict.get(
          ControlState.DEFAULT,
          next(iter(state_dict.values()))
      )}

Logs

Logs

Additional details

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcontrols

Type

No type

Projects

Status

✅ Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions