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

Add a Textual backend. #2065

Merged
merged 10 commits into from Aug 19, 2023
Merged

Add a Textual backend. #2065

merged 10 commits into from Aug 19, 2023

Conversation

freakboy3742
Copy link
Member

Adds a terminal backend, using Textual as a TUI API.

Screenshot 2023-08-06 at 10 47 47 am

This is very early alpha; but it's sufficiently mature to run Tutorial 0 and Tutorial 1, with the following caveats:

  1. Tutorial 0 uses print to write to console, so that output isn't visible
  2. Tutorial 1 uses a readonly TextInput, but readonly isn't implemented (this might be a limitation in Textual?)
  3. Styles are only barely honoured. Basic layout is preserved, but flex sizing isn't.

This implementation maps a Textual "Screen" to Toga's Window class. This should (eventually) allow an app to have multiple windows, and allow moving between them with a keyboard shortcuts. Dialogs would also be implemented as screens.

Fixing style/layout will be a little bit complication; however, it will be a lot easier when #2020 lands. That PR makes Pack DPI independent, which means we will be able to implement a conversion between "textual DPI" (which has a default screen resolution of 80x25) to Toga's "96dpi ideal screen", and back again. This should mean we can convert Pack's padding, height and width into a character count, and apply those sizes to Textual's "CSS" styles.

To use this backend, you need to either:

  1. Ensure toga-textual is the only toga backend that is installed in your virtual environment, or
  2. Set TOGA_BACKEND=toga_textual in your runtime environment (e.g., TOGA_BACKEND=toga_textual python -m myapp on macOS/Linux).

At least for now, you won't be able to deploy a Textual app with Briefcase on any platform other than Linux. See beeware/briefcase#556 for details. However, you can run a Toga-textual app as python -m app.

The RTD build is known to fail at present because of a failed URL check - the README references the location in the GitHub repo where this code will live once this PR has been merged.

Fixes #1867.

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

@willmcgugan
Copy link

Textual dev here. Very happy about this work! Mention me if you ever have any Textual questions.

BTW, you can make Textual look a little better on MacOS with this tip: https://github.com/Textualize/textual/blob/main/FAQ.md#why-doesn't-textual-look-good-on-macos

@freakboy3742
Copy link
Member Author

@willmcgugan

Textual dev here. Very happy about this work! Mention me if you ever have any Textual questions.

Thanks! For now, I think the only question I have is about event handlers. From everything I'm reading, event handlers are entirely at the level of the screen/app - there's no way to install a handler on a specific widget instance. Is that correct, or have I missed something?

The reason I ask: if you've got a GUI got 10 buttons, each of which does something different, you essentially need to write a app/screen-level handler that is essentially a switch statement on the ID of the widget (or some similar redirection). That's not hard to do (in my case, it's a 1 line function)... but it seems like an odd omission in the Textual API, given the prior art of other GUI toolkits I've used.

BTW, you can make Textual look a little better on MacOS with this tip: https://github.com/Textualize/textual/blob/main/FAQ.md#why-doesn't-textual-look-good-on-macos

Thanks for the heads up on that - I've added a note to the docs for the backend.

@willmcgugan
Copy link

Thanks! For now, I think the only question I have is about event handlers. From everything I'm reading, event handlers are entirely at the level of the screen/app - there's no way to install a handler on a specific widget instance. Is that correct, or have I missed something?

You can manage events at the widget level, in the same way as screen / app. You could extend the Button class and implemented a handler there, for instance. Most events bubble so you can handle them on a parent. It's a very JS like model.

The on decorator can help you manage a bunch of buttons. If you give each button an id, you can write a handler for just that button:

def compose(self):
    yield Button("Ding", id="bell")
@on(Button.Pressed, "#bell")
def bell(self):
    self.app.bell()

@freakboy3742
Copy link
Member Author

You can manage events at the widget level, in the same way as screen / app. You could extend the Button class and implemented a handler there, for instance. Most events bubble so you can handle them on a parent. It's a very JS like model.

Ah - I was overthinking things, and thinking that the button prefix would be dropped if you were on the Button widget. I see now that on_button_pressed works at the level of a button.

The on decorator can help you manage a bunch of buttons. If you give each button an id, you can write a handler for just that button:

I can see how that might be helpful if you're writing directly against the Textual API, but it's not as helpful in Toga's situation because it's an abstraction layer - we can't register against an ID (at least, not easily) because we don't know what ID the end user is using, or even if they're using an ID at all. Subclassing Button works really well, though.

@freakboy3742
Copy link
Member Author

I've merged #2020 into this branch; and made a first stab at applying layout to the resulting app. It's not getting the widget sizing completely correct - there's something going on with the rendering size of borders and margins on widgets like button and TextInput; and it's not applying padding around widgets - but it is manifesting flexible sizing that broadly reflects what Pack intends.

@freakboy3742
Copy link
Member Author

I've worked out what was going on with layout, and I've got it rendering Tutorial0 and Tutorial1 correctly. The issue was that left/top content offsets are relative to the parent, not the predecessor.

@mhsmith If you don't get to this first, I'll merge this at the start of the PyCon AU sprints so that we'll have a stable base for contributors to work against.

Copy link
Member

@mhsmith mhsmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, just one minor comment:

Comment on lines 9 to 10
HORIZONTAL_SCALE = 10
VERTICAL_SCALE = 25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

600 // 25 == 24. Suggest making the calculation explicit:

Suggested change
HORIZONTAL_SCALE = 10
VERTICAL_SCALE = 25
HORIZONTAL_SCALE = 800 // 80
VERTICAL_SCALE = 600 // 25

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the 24 lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant was that 600 / 25 gives 24 pixels per line, not 25. I don't think subtracting one line for the title bar really makes sense, because the title bar is included in the hypothetical default terminal height of 600 pixels.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - I was overthinking things. The aspect ratio mapping of of 80x25->800x600 shouldn't be excluding the titlebar.

@freakboy3742 freakboy3742 merged commit 4dd84fa into beeware:main Aug 19, 2023
40 of 41 checks passed
@freakboy3742 freakboy3742 deleted the textual branch August 19, 2023 23:00
@freakboy3742 freakboy3742 mentioned this pull request Aug 22, 2023
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

New backend - Textual
3 participants