slint icon indicating copy to clipboard operation
slint copied to clipboard

How to make an element lose focus

Open jamesblacklock opened this issue 2 years ago • 20 comments

I might be stupidly missing something, but I can't figure out how to handle focus at all. There's no way to tell if an element loses focus, there's no way to programmatically make an element unfocus, multiple elements seem to get focused at once, and I don't understand how to use forward-focus. Am I missing something, or is focus just not ready yet? And is there anything I can do to help?

jamesblacklock avatar May 02 '22 00:05 jamesblacklock

I came up with a somewhat hacky solution for the lack of a blur equivalent:

export TableCell := Rectangle {
    property<bool> input;
    property<bool> active;
    property<string> text;
    property<string> placeholder-text;
    property<TextHorizontalAlignment> horizontal-alignment: left;

    callback accepted(string);

    forward-focus: edit;

    edit := LineEdit {
        visible: edit.has-focus ? active : false;
        placeholder-text: root.placeholder-text;
        width: 100%;
        height: 100%;
        accepted(val) => {
            root.accepted(val);
            active = false;
        }
    }

    touch-area := TouchArea {
        enabled: input;
        clicked => {
            edit.text = root.text;
            edit.focus();
            active = true;
        }
    }

    text := Text {
        visible: !edit.visible;
        preferred-width: 0;
        width: root.width - 12px;
        x: input ? 8px : 6px;
        vertical-alignment: center;
        horizontal-alignment: root.horizontal-alignment;
        height: 100%;
        text: {
            if (input && root.text == "") {
                root.placeholder-text
            } else {
                root.text
            }
        }
        overflow: elide;
    }
}

The key here is visible: edit.has-focus ? active : false; I feel that this is hacky, because the element actually keeps its focus even after being set to invisible by active = false. It seems like there should be a better way.

jamesblacklock avatar May 02 '22 01:05 jamesblacklock

because the element actually keeps its focus even after being set to invisible

There was some change regarding focus in the master branch so this particular issue should be fixed.

Otherwise right now the best way to remove the focus is to send the focus to another FocusScope using focus() and that FocusScope can be hidden or disabled.

This is true that we should add a function to remove the focus. I'm not convinced .blur() is the best name. Maybe .unfocus() or .remove-focus()

ogoffart avatar May 02 '22 07:05 ogoffart

Ok great, I was intending to sync up with the master branch anyway so I will try that. Good point about using a different FocusScope as well.

I 100% agree that “blur” is a stupid name for it lol. “unfocus” or “focus(false)” both seem good to me.

jamesblacklock avatar May 02 '22 15:05 jamesblacklock

I really need some focused and unfocused callbacks for my project. If I work on implementing this functionality in the library, would it be likely to be accepted?

jamesblacklock avatar May 02 '22 17:05 jamesblacklock

Also, is there any discussion of this feature that I should be aware of? It looks like .focus() gets injected into every element, but I don't see a way to inject a callback property into every element.

jamesblacklock avatar May 02 '22 17:05 jamesblacklock

Of course, we could simply trigger the callback inside of ItemVTable::focus_event, but then you would have to implement it separately for any element that can receive focus.

jamesblacklock avatar May 02 '22 17:05 jamesblacklock

I'm looking at your #[vtable] macro (very cool crate!) and it doesn't appear that there's currently a way to add a default trait impl. Is this true?

jamesblacklock avatar May 02 '22 18:05 jamesblacklock

It seems that focusability is determined on an ad-hoc basis by returning FocusEventResult::FocusAccepted. Thus it would seem that the most practical thing to do is simply add a callback focused and callback unfocused to each element and to trigger these callbacks inside of Item::focus_event. Let me know if this is the intended implementation and I will do it!

jamesblacklock avatar May 02 '22 20:05 jamesblacklock

What seems to me would be preferable, though, would be for every item to always get these callbacks. Not sure exactly how to implement that, though.

jamesblacklock avatar May 02 '22 20:05 jamesblacklock

Could also be added to the property_declarations of every Element

jamesblacklock avatar May 02 '22 21:05 jamesblacklock

I really need some focused and unfocused callbacks for my project.

I guess these focus callbacks would make the most sense for FocusScope rather than every elements.

Also, is there any discussion of this feature that I should be aware of? It looks like .focus() gets injected into every element, but I don't see a way to inject a callback property into every element.

Some discussion in https://github.com/slint-ui/slint/issues/145 note that focus() is only accepted on element that can have the focus.

I'm looking at your #[vtable] macro (very cool crate!) and it doesn't appear that there's currently a way to add a default trait impl.

Right, no default trait impl. This would be usefull though.


In sumary, i think the best would be to add a unfocus() similar to focus() , and/or some callbacks in FocusScope

ogoffart avatar May 03 '22 06:05 ogoffart

That makes sense, I think. So how would you detect a focus change for an element other than a FocusScope? Would you use forward-focus for that?

jamesblacklock avatar May 03 '22 16:05 jamesblacklock

One way I can think of is to look at the has-focus property on the FocusScope.

We do that in the default widgets:

export Button := Rectangle {
    ...
    property<bool> has-focus <=> fs.has-focus;
    ...

    l := HorizontalLayout { ... }

    touch := TouchArea {}

    fs := FocusScope { ... }

    Rectangle { // Focus rectangle
        border-width: enabled && has-focus? 1px : 0px;
    }
}

Full code

hunger avatar May 03 '22 16:05 hunger

Ahhh, I never thought of that. Excellent point, thanks.

jamesblacklock avatar May 03 '22 17:05 jamesblacklock

However, I still think I have need of the callbacks on FocusScope. Correct me if I am wrong: I want a text field to "submit" its contents when it loses focus. AFAICT, there is no way to achieve this without either 1) a callback or 2) something really hacky. Am I missing something here?

jamesblacklock avatar May 03 '22 18:05 jamesblacklock

I want a text field to "submit" its contents when it loses focus.

I think it make sense to add a callback in the TextInput for that purpose. QLineEdit has such a signal: https://doc.qt.io/qt-5/qlineedit.html#editingFinished

ogoffart avatar May 03 '22 18:05 ogoffart

It seems to me that adding such a callback to FocusScope would be a much more flexible approach. Or did you mean that it would make sense to do both?

jamesblacklock avatar May 03 '22 18:05 jamesblacklock

Yes, I think it makes sense to do both

ogoffart avatar May 03 '22 18:05 ogoffart

Using just a callback on FocusScope is not sufficient, because either

  1. the FocusScope gets focused, so you can't type in the TextInput, or
  2. the TextInput gets focused, so the callbacks are not appropriately triggered.

This seems like a rather limiting design. Are you sure you don't want elements to have the generalized ability to handle focus?

(I know you mentioned also adding the callback to the TextInput, but this seems like a much more general problem to me)

jamesblacklock avatar May 03 '22 23:05 jamesblacklock

In my mind we have widgets which a user interact with and simple building blocks to build these widgets out of like FocusScope, TouchArea, Rectangle and similar. I am not 100% certain whether you are arguing at the widget level or the building block level here.

I agree that we need concept like has-focus, geometry, visibility and a lot more at the widget level.

I do not see the need at the building block level though. We only need very basic concepts there and only those that make sense for the specialized task the particular building block has. Basically a FocusScope needs to be able to handle focus and it needs to be enabled (so that you can easily turn of its keyboard handling) and not much else. A Rectangle on the other hand needs all the properties needed to render all kinds of rectangles, but not more.

E.g. a line edit widget that you will want to use in a UI will consist of a TextInput, a couple of Rectangles (or Path/Images/whatever) and a FocusScope. The widget will need to have a has-focus property, it can be enabled or read-only and probably some more.

Those widget properties are then implemented based on the properties in the contained building blocks. E.g. the widget's has-focus could be a simple alias to the FocusScopes has-focus. Both the TextInput and the FocusScope will have their enabled property aliased to the Widget's, so that you can turn the entire widget on/off. Some other building block will only be visible if the widget has focus (to draw some kind of focus rectangle or such).

Conceptually we should not even have a basic TextInput element. That could be built out of a FocusScope to handle the keyboard plus a Text to render the actual text. But that is pretty much a implementation detail:-)

hunger avatar May 04 '22 07:05 hunger