Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easy-to-use declarative tooltips #6446

Open
PanCakeConnaisseur opened this issue Oct 4, 2024 · 5 comments
Open

Easy-to-use declarative tooltips #6446

PanCakeConnaisseur opened this issue Oct 4, 2024 · 5 comments
Labels
a:language-slint Compiler for the .slint language (mO,bF) enhancement New feature or request

Comments

@PanCakeConnaisseur
Copy link

PanCakeConnaisseur commented Oct 4, 2024

I am currently trying to add tooltips to my application. By tooltip I mean a small box of text that appears when the users hovers over widgets for a few seconds that provides additional information/explanation of the setting modeled by the widget. The tooltip appears without a click and disappears when the user moves the mouse away from the widget (but not while he is still on the widget).

AFAICS the documentation recommends using a PopupWindow. AFAICS it has the following disadvantages:

  • a PopupWindow must be explicitly activated using show(). When I want it to appear on hover (without a click), the only way is to add a TouchArea and a rather complicated
pointer-event(event) => {
    if (event.kind == PointerEventKind.move) {
       popup.show();
  }
  • in some cases a PopupWindow becomes a separate window, with its own window decorators, that needs to be closed manually
  • the position of the PopupWindow must be set manually
  • the size of the PopupWindow must be set manually
  • surrounding components with TouchArea can break the layout, e.g. when using it inside a Row

What I would prefer is to have

  • a property tooltip : "This is some additional information; that can be added to most components including layouts such as HorizontalLayout or
  • a component Tooltip { text : "This is some additional information; delay: 1s;} that can be a child of most components.

Slint would then display this tooltip whenever the user hovers over the area of the component, whether it is a button or a whole area. Slint would also take care of the size and position of the tooltip. It would adjust the size to be just big enough to contain the text and would position the tooltip such that it is close to the cursor, but out of its way and still inside the window. The background of the tooltip would be different enough for it to pop out visually.

Is anything like this planned?

@ogoffart ogoffart changed the title Are there plans to implement easy-to-use declarative tooltips? Easy-to-use declarative tooltips Oct 4, 2024
@ogoffart ogoffart added enhancement New feature or request a:language-slint Compiler for the .slint language (mO,bF) labels Oct 4, 2024
@ogoffart
Copy link
Member

ogoffart commented Oct 4, 2024

Yes, a Tooltip { text : "This is some additional information; } seems like something we'd like to have in slint.

@PanCakeConnaisseur
Copy link
Author

I wlll probably use this workaround. Would be interesting if anyone has a more elegant/shorter solution using the current feature set (Slint 1.8.0).

myTouchArea := TouchArea {
  Button {
    text: "Press me!"
  }
}


Rectangle {
  background: black; 
  x: 20px; y: 20px; height: 80px; width: 120px;
  opacity: myTouchArea.has-hover ? 1 : 0;
  border-radius: 5px;
   
  Text {
    text: "If you press this button, something cool will happen!";
    padding: 10px;
    color: white;
    }
}

This has the mentioned disadvantages of being a lot of code and having to manually set the position and size of the Rectangle.

@PanCakeConnaisseur
Copy link
Author

PanCakeConnaisseur commented Oct 4, 2024

While we wait for a clean upstream implementation, I build a reusable ToolTip component. Maybe this is useful to other people.

export component ToolTip inherits Window {
    in property <string> text  <=> text.text;
    in property  <bool> user_is_hovering;
    
    property <bool> has_waited;
    z: 100;

    Rectangle {
        visible: has_waited && parent.user_is_hovering;
        background: Palette.foreground;
        border-radius: 0.5rem;
   
        text := Text {
            width: parent.width - 1rem;
            height: parent.height - 1rem;
            color: Palette.background;
            font-size: root.default-font-size * 0.9;
            horizontal-alignment: left;
            vertical-alignment: top;
            wrap: word-wrap;
            
        }
    }

    Timer {
        // Delay until tooltip appears.
        interval: 1s;
        running: parent.user_is_hovering || self.needs_resetting;
        // The resetting property allows the timer to reset to the default state
        // where it has not waited to show the tooltip yet. This is needed
        // because we can not listen to a "not hovering anymore" event. Thus, we
        // let the timer run a little bit longer and reset the state before
        // stopping `running`.
        property <bool> needs_resetting : false;
        triggered() => {
            if needs_resetting && !parent.user_is_hovering {
                // User is not hovering anymore.
                parent.has_waited = false;
                needs_resetting = false;
            }
            else {
                // User is hovering and waited, so tooltip can be shown.
                parent.has_waited = true;
                needs_resetting = true;
            }
        }
    }
}

You can use is as follows

// Outside of any layouts define as many tooltups as you want like this.
crfToolTip := ToolTip {
    x: crfTouch.x + 50px; y: crfTouch.y + 140px; // <-- references any or no component, not necessarily a TouchArea
    height: 10rem;
    width: 25rem;
    text: "The constant rate factor (CRF) determines the quality of the resulting video stream. The "
    + "lower the CRF, the more bits AV1 will use to encode the video stream. A lower "
    + "CRF will lead to better quality but also a larger file. Thus, here you decide between quality and size.";
    user_is_hovering: crfTouch.has-hover; // <-- references one or more TouchArea below
    // To show this tooltip for several TouchAreas:
    // user_is_hovering: crfTouch.has-hover || yourOtherTouchArea.has-hover;
}


// You can put one or more components in a TouchArea. The tooltip will
// activate on all components in the TouchArea. The TouchArea is needed
// as a container, since this is the only component that has a `has-hover` property.
crfTouch := TouchArea {
    HorizontalLayout {
      Text {
            text: "CRF";
        }
        Slider {
            min-width: 100px;
            minimum: 0;
            maximum: 63;
        }
    }
}

For each tooltip you need to manually set the coordinates (x, y) and the size (height, width). I didn't find a function in Slint that gives me the length of a text property, so I don't think you can calculate this automatically. You could do this in Rust, but I didn't want to define tooltips there.

This is how it looks (Here UI is more complex than in above example)
Screenshot_20241004_230628

@ogoffart
Copy link
Member

ogoffart commented Oct 5, 2024

Thanks @PanCakeConnaisseur for this example!

(for reference, there was also some discussion about tooltip before in #1617)

@PanCakeConnaisseur
Copy link
Author

PanCakeConnaisseur commented Oct 6, 2024

@ogoffart Thank you for the link. Your solution using states and the mouse position is much cleaner than using a Timer and hardcoded positions. It introduces two problems though, that I wasn't able to solve yet.

  1. The Rectangle tooltip only covers components from its own TouchArea by default.
  2. There is no easy way to get the main window's width.

I combined mine and your solution and came up with

component ToolTipArea inherits Window {
    preferred-height: 100%;
    preferred-width: 100%;
     
    in property <string> text;
    in property <length> window-width;

    Rectangle {
        states [ 
            visible when ta.has-hover: { 
                opacity: 1;
                in { 
                    animate opacity { duration: 175ms; delay: 1000ms; }
                }
            }
        ]

        x: max(-parent.absolute-position.x, min(ta.mouse-x - 2rem, - parent.absolute-position.x + window-width - self.width));
        y:  ta.mouse-y + 2rem;
        background: Palette.foreground;
        border-radius: 0.4rem;
        opacity: 0;
        width: hlayout.preferred-width;
        height: hlayout.preferred-height;
        hlayout := HorizontalLayout { 
            padding: 0.6rem;
            Text {
                text <=> root.text;
                wrap:word-wrap;
                width: 15rem;
                color: Palette.background;
                font-size: root.default-font-size * 0.9;
            }
        }
    }
    ta := TouchArea {
        @children
     }
}


// Use later

ToolTipArea {
  text : "Don't reencode audio stream(s) using Opus, but copy them as is.";
  window-width: root.width; // <-- assuming that root is the main window component. avoid this?
  copyAudio := Switch {
      z:-7; // <-- avoid this?
      text: "Copy original streams";
      toggled => {
          root.copy_toggled(self.checked);
      }
  }
}

The problems in detail

1. Rectangle's z

The tooltip rectangle does not cover elements that are outside of the same ToolTipArea (TouchArea).
Screenshot_20241006_155844
I can solve it partly by setting the z value of TouchAreas below to a lower value.

ToolTipArea {
  z: -1;
  // content
}

ToolTipArea {
  z: -2;
  // content
}

ToolTipArea {
  z: -3;
  // content
  
  //...
}

But setting a low z to e.g. a HorizontalLayout or a Switch seems to have no effect, it is still drawn above the tooltip. And the workaround only works as long as each tooltip covers only elements below it. Is there a better solution for this? AFAIK z must be determined at compile time but I would like to set something that makes the tooltip be on top of everything else in the entire window while it is shown.

2. Main Window Width

I would like to keep the tooltip always inside the window. I created an expression for the Rectangle's x position but it needs the width of the main window. AFAICS there is no direct way to get this value. I had to define the property window-width and pass it to each ToolTipArea component, which is quite cumbersome. Should I open a new issue for this as a feature request or is it already implemented?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a:language-slint Compiler for the .slint language (mO,bF) enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants