Handling Mouse Input Events

mouse

The mouse device is an essential input device for 2-in-1 devices. It enables click or swipe actions through button presses, scrolling through the scroll wheel, and also has other buttons. These are reported to the application via MouseEvent and AxisEvent, respectively.

NOTE

All single-finger touch events and gesture events can be triggered and responded to using a left mouse click.

  • For example, to implement a feature where clicking a Button navigates to another page, supporting both finger tap and left mouse click, you can achieve this by simply binding a single onClick event.
  • If you want to implement different effects for the finger touch and the left mouse click, use the source parameter in the onClick callback to determine whether the current event is triggered by a finger touch or a mouse device.

Processing Mouse Movement

Mouse events are handled by registering a callback using the onMouse API. When a mouse action occurs, the event is dispatched to the component located beneath the cursor. This dispatch process adheres to the event bubbling mechanism.

onMouse

onMouse(event: (event?: MouseEvent) => void)

Triggered when a mouse event occurs. If the mouse pointer performs an action (MouseAction) on a component bound to this API, the corresponding callback is triggered and receives a MouseEvent object as its parameter. Event bubbling is supported and can be customized; by default, events bubble between parent and child components. This API is typically used to implement custom mouse interaction logic.

The MouseEvent object in the callback provides the following information: coordinates (displayX, displayY, windowX, windowY, x, y), button (MouseButton), action (MouseAction), timestamp (timestamp), target area (EventTarget), and event source (SourceType). The stopPropagation callback of MouseEvent can be used to prevent the event from bubbling up.

NOTE

MouseButton indicates the physical mouse button (pressed or released) that triggers the mouse event. The values are Left, Right, Middle, Back, Forward, and None. None indicates that the event is triggered only when the mouse is moved without any mouse button pressed or released.

@Entry
@Component
struct MouseMove {
  @State buttonText: string = '';
  @State columnText: string = '';
  @State text: string = 'OnMouse Sample Button';
  @State color: Color = Color.Gray;

  build() {
    Column() {
      Button(this.text, { type: ButtonType.Capsule })
        .width(200)
        .height(100)
        .backgroundColor(this.color)
        .onMouse((event?: MouseEvent) => { // Set the onMouse callback for the Button component.
          if (event) {
            this.buttonText = 'Button onMouse:\n' + '' +
              'button = ' + event.button + '\n' +
              'action = ' + event.action + '\n' +
              'x,y = ' + '\n' + '(' + event.x + ',' + event.y + ')' + '\n' +
              'windowXY=' + '\n' + '(' + event.windowX + ',' + event.windowY + ')';
          }
        })
      Column() {
        Divider()
        Text(this.buttonText).fontColor(Color.Green).padding(5)
        Divider()
        Text(this.columnText).fontColor(Color.Red).padding(5)
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .borderWidth(2)
    .borderColor(Color.Red)
    .onMouse((event?: MouseEvent) => { // Set the onMouse callback for the column.
      if (event) {
        this.columnText = 'Column onMouse:\n' + '' +
          'button = ' + event.button + '\n' +
          'action = ' + event.action + '\n' +
          'x,y = ' + '\n' + '(' + event.x + ',' + event.y + ')' + '\n' +
          'windowXY=' + '\n' + '(' + event.windowX + ',' + event.windowY + ')';
      }
    })
  }
}

In the preceding example, the onMouse API is bound to the Button component. The callback displays the values of callback parameters, such as button and action. Apply the same settings to the outer Column container. The entire process can be divided into the following two actions:

  1. Moving the mouse pointer: Before the mouse pointer moves from outside the Button component to inside, only the onMouse callback of the Column component is triggered. When the mouse pointer enters the Button component, as the onMouse event bubbles up by default, both the onMouse callbacks of the Column and Button components are invoked. Because no mouse button is clicked during this process, the displayed information shows button as 0 (enumerated value of MouseButton.None) and action as 3 (enumerated value of MouseAction.Move).

  2. Clicking the mouse button: After the mouse pointer enters the Button component, clicking the component twice (left-click and right-click) produces the following results:

    Left-click: button = 1 (enumerated value of MouseButton.Left); action = 1 (enumerated value of MouseAction.Press); action = 2 (enumerated value of MouseAction.Release).

    Right-click: button = 2 (enumerated value of MouseButton.Right); action = 1 (enumerated value of MouseAction.Press); action = 2 (enumerated value of MouseAction.Release)

onMouse1

To prevent the mouse event from bubbling, call the stopPropagation API.

@Entry
@Component
struct StopPropagation {
  @State buttonText: string = '';
  @State columnText: string = '';
  @State text: string = 'OnMouse Sample Button';
  @State color: Color = Color.Gray;

  build() {
    Column() {
      Button(this.text, { type: ButtonType.Capsule })
        .width(200)
        .height(100)
        .backgroundColor(this.color)
        .onMouse((event?: MouseEvent) => { // Set the onMouse callback for the Button component.
          if (event) {
            event.stopPropagation(); // Prevent the onMouse event from bubbling.
            this.buttonText = 'Button onMouse:\n' + '' +
              'button = ' + event.button + '\n' +
              'action = ' + event.action + '\n' +
              'x,y = ' + '\n' + '(' + event.x + ',' + event.y + ')' + '\n' +
              'windowXY=' + '\n' + '(' + event.windowX + ',' + event.windowY + ')';
          }
        })
      Column() {
        Divider()
        Text(this.buttonText).fontColor(Color.Green).padding(5)
        Divider()
        Text(this.columnText).fontColor(Color.Red).padding(5)
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .borderWidth(2)
    .borderColor(Color.Red)
    .onMouse((event?: MouseEvent) => { // Set the onMouse callback for the Column component.
      if (event) {
        this.columnText = 'Column onMouse:\n' + '' +
          'button = ' + event.button + '\n' +
          'action = ' + event.action + '\n' +
          'x,y = ' + '\n' + '(' + event.x + ',' + event.y + ')' + '\n' +
          'windowXY=' + '\n' + '(' + event.windowX + ',' + event.windowY + ')';
      }
    })
  }
}

onMouse2

To prevent mouse events from bubbling up from a child component (Button) to its parent component (Column), call the stopPropagation API using the event parameter in the onMouse callback of Button, as shown in the example above.

onHover

To detect when the mouse pointer enters or exits a component's boundary, you are advised to use the advanced onHover event. This approach simplifies your code and avoids the complexity of manually handling mouse move events.

onHover(event: (isHover: boolean) => void)

Triggered when the mouse pointer enters or leaves the component. The isHover parameter indicates whether the mouse pointer hovers over the component. Event bubbling is supported and can be customized; by default, events bubble between parent and child components.

If this API is bound to a component, it is triggered when the mouse pointer enters the component from outside and the value of isHover is true, or when the mouse pointer leaves the component and the value of isHover is false.

@Entry
@Component
struct OnHover {
  @State hoverText: string = 'Not Hover';
  @State color: Color = Color.Gray;

  build() {
    Column() {
      Button(this.hoverText)
        .width(200).height(100)
        .backgroundColor(this.color)
        .onHover((isHover?: boolean) => { // Listen for whether the mouse cursor is hovered over the button.
          if (isHover) {
            this.hoverText = 'Hovered!';
            this.color = Color.Green;
          } else {
            this.hoverText = 'Not Hover';
            this.color = Color.Gray;
          }
        })
    }.width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}

In this example, a Button component is created, with an initial gray background color and the content Not Hover. The component is bound to the onHover callback. In the callback, this.isHovered is set to the callback parameter isHover.

When the mouse pointer moves from outside the Button component to inside, the callback is invoked, setting the value of isHovered to true. As a result, the background color of the component changes to Color.Green, and the content is updated to Hovered!.

When the mouse pointer moves from inside the Button component to outside, the callback is invoked again, setting the value of isHover to false. The component then reverts to its initial style.

onHover

Processing Mouse Buttons

When a user presses a mouse button, a mouse down event is triggered. You can access key details about the event through the MouseEvent object, such as the timestamp and the specific button pressed (MouseButton: such as left or right). In addition, the getModifierKeyState API allows you to detect the state of modifier keys (Ctrl, Alt, and Shift) on the physical keyboard at the time of the mouse interaction. By combining mouse and keyboard input, you can implement advanced interaction patterns like multi-selection.

The following example demonstrates multi-selection functionality using mouse button processing:

class ListDataSource implements IDataSource {
  private list: number[] = [];
  private listeners: DataChangeListener[] = [];

  constructor(list: number[]) {
    this.list = list;
  }

  totalCount(): number {
    return this.list.length;
  }

  getData(index: number): number {
    return this.list[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  // Notify the controller of data deletion.
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    });
  }

  // Delete an element at the specified index.
  public deleteItem(index: number): void {
    this.list.splice(index, 1);
    this.notifyDataDelete(index);
  }
}

@Entry
@Component
struct ListExample {
  private arr: ListDataSource = new ListDataSource([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
  private allSelectedItems: Array<number> = [];
  @State isSelected: boolean[] = [];

  @Styles
  selectedStyle(): void {
    .backgroundColor(Color.Blue);
  }

  isItemSelected(item: number): boolean {
    for (let i = 0; i < this.allSelectedItems.length; i++) {
      if (this.allSelectedItems[i] === item) {
        this.isSelected[item] = true;
        return true;
      }
    }
    this.isSelected[item] = false;
    return false;
  }

  build() {
    Column() {
      List({ space: 10, initialIndex: 0 }) {
        LazyForEach(this.arr, (index: number) => {
          ListItem() {
            Text('' + index)
              .width('100%')
              .height(100)
              .fontSize(16)
              .fontColor(this.isSelected[index] ? Color.White : Color.Black)
              .textAlign(TextAlign.Center)
          }
          .backgroundColor(Color.White)
          .selectable(true)
          .selected(this.isSelected[index])
          .stateStyles({
            selected: this.selectedStyle
          })
          .onMouse((event: MouseEvent) => {
            // Check whether the left mouse button is pressed.
            if (event.button === MouseButton.Left && event.action === MouseAction.Press) {
              // Check the item selected state.
              let isSelected: boolean = this.isItemSelected(index);
              // Check the Ctrl key state.
              let isCtrlPressing: boolean = false;
              if (event.getModifierKeyState) {
                try {
                  isCtrlPressing = event.getModifierKeyState(['Ctrl']);
                } catch (error) {
                  console.error('Get modifier key state failed!')
                }
              }
              // If the mouse is clicked without the Ctrl key held down, forcefully clear other selected items and keep only the current item selected.
              if (!isCtrlPressing) {
                this.allSelectedItems = [];
                for (let i = 0; i < this.isSelected.length; i++) {
                  this.isSelected[i] = false;
                }
              }
              if (isSelected) {
                this.allSelectedItems.filter(item => item !== index);
                this.isSelected[index] = false;
              } else {
                this.allSelectedItems.push(index);
                this.isSelected[index] = true;
              }
            }
          })
        }, (item: string) => item)
      }
      .listDirection(Axis.Vertical)
      .scrollBar(BarState.Off)
      .friction(0.6)
      .edgeEffect(EdgeEffect.Spring)
      .width('90%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor(0xDCDCDC)
    .padding({ top: 5 })
  }
}

ChangeMouse

Processing Mouse Wheel Input

The mouse wheel is primarily used for vertical scrolling. When a user scrolls the wheel, the system generates a vertical axis event and dispatches it to the application. Components can handle this event using the onAxisEvent API. The event includes the cursor position and the angle of wheel rotation, accessible using the axisVertical property of the BaseEvent object.

Mouse wheel axis events follow a structured lifecycle: Each scroll interaction begins with an AxisAction.BEGIN event. When scrolling ends, an AxisAction.End event is emitted. For slow or intermittent scrolling, multiple BEGIN and END events may be reported. The axisVertical value represents the angular change during a single scroll tick. Key characteristics include:

  • It reflects the delta of one scroll action, not the cumulative scroll distance.
  • The value is influenced by system-level wheel sensitivity settings.
  • The sensitivity is specified by the AxisEvent parameter scrollStep in system settings.
  • A forward scroll (wheel up) yields a negative value. A backward scroll (wheel down) yields a positive value.

Built-in scrollable components automatically handle wheel input. No additional configuration is required.

When PanGesture is used, wheel input maps to vertical offset values: For forward scrolling, offsetY is positive; for backward scrolling, offsetY is negative.

NOTE

  1. Wheel-generated vertical axis values only trigger vertical scrolling gestures.
  2. If the system detects that only horizontally scrollable components are under the cursor, it may trigger horizontal scrolling.
  3. When both vertical and horizontal scrollable elements are present, vertical scrolling takes precedence.

This example demonstrates how a List component responds to mouse wheel input in both vertical and horizontal orientations.

export class ListDataSource implements IDataSource {
  private list: number[] = [];
  private listeners: DataChangeListener[] = [];

  constructor(list: number[]) {
    this.list = list;
  }

  totalCount(): number {
    return this.list.length;
  }

  getData(index: number): number {
    return this.list[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  // Notify the controller of data deletion.
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    });
  }

  // Notify the controller of data insertion.
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    });
  }

  // Delete an element at the specified index.
  public deleteItem(index: number): void {
    this.list.splice(index, 1);
    this.notifyDataDelete(index);
  }

  // Insert an element at the specified index.
  public insertItem(index: number, data: number): void {
    this.list.splice(index, 0, data);
    this.notifyDataAdd(index);
  }
}
import { ListDataSource } from './ListDataSource';

@Entry
@Component
struct MouseWheel {
  private arr: ListDataSource = new ListDataSource([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
  @State dir1: Axis = Axis.Vertical;

  build() {
    Column() {
      Button('Click to Change ListDirection')
        .margin(20)
        .onClick(() => {
          if (this.dir1 === Axis.Vertical) {
            this.dir1 = Axis.Horizontal
          } else {
            this.dir1 = Axis.Vertical
          }
        })
      List({ space: 20, initialIndex: 0 }) {
        LazyForEach(this.arr, (item: number) => {
          ListItem() {
            Text('' + item)
              .width('100%')
              .height(100)
              .fontSize(16)
              .textAlign(TextAlign.Center)
              .borderRadius(10)
              .backgroundColor(0xFFFFFF)
          }
          .margin(20)
          // Bind a pan gesture to the ListItem. When the mouse wheel is scrolled over the ListItem, the ListItem's own pan gesture is triggered first.
          .gesture(PanGesture({ direction: PanDirection.Vertical })
            .onActionStart(() => {
            })
            .onActionUpdate(() => {
            }))
        }, (item: number) => item.toString())
      }
      .borderWidth(1)
      .listDirection(this.dir1) // Arrangement direction
      .scrollBar(BarState.Off)
      .friction(0.6)
      .divider({
        strokeWidth: 2,
        color: 0xFFFFFF,
        startMargin: 20,
        endMargin: 20
      }) // Divider between rows
      .edgeEffect(EdgeEffect.Spring) // Set the edge scrolling effect to Spring.
      .width('90%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor(0xDCDCDC)
    .padding(20)
  }
}

ListAxis