diff --git a/.github/workflows/black_format.yml b/.github/workflows/black_format.yml new file mode 100644 index 000000000..063068c35 --- /dev/null +++ b/.github/workflows/black_format.yml @@ -0,0 +1,22 @@ + +name: Black format check + +on: + pull_request: + paths: + - '.github/workflows/black_format.yml' + - '**.py' + +jobs: + black-format-check: + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v3.5.2 + - name: Set up Python 3.11 + uses: actions/setup-python@v4.6.0 + with: + python-version: 3.11 + - name: Install black + run: python -m pip install black + - name: Run black + run: black --check src diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml index 46cf1677f..d26259360 100644 --- a/.github/workflows/comment.yml +++ b/.github/workflows/comment.yml @@ -1,7 +1,8 @@ -name: issues +name: Closed issue comment on: issues: types: [closed] + jobs: add-comment: runs-on: ubuntu-latest @@ -14,5 +15,5 @@ jobs: issue-number: ${{ github.event.issue.number }} body: | Don't forget to [star](https://github.com/Textualize/textual) the repository! - + Follow [@textualizeio](https://twitter.com/textualizeio) for Textual updates. diff --git a/.github/workflows/new_issue.yml b/.github/workflows/new_issue.yml index 3dd9d9a3a..cb55213ec 100644 --- a/.github/workflows/new_issue.yml +++ b/.github/workflows/new_issue.yml @@ -1,7 +1,8 @@ -name: issues +name: FAQ issue comment on: issues: types: [opened] + jobs: add-comment: if: ${{ !contains( 'willmcgugan,darrenburns,davep,rodrigogiraoserrao', github.actor ) }} diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 2891b4bcf..27f36d406 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -3,6 +3,7 @@ name: Test Textual module on: pull_request: paths: + - '.github/workflows/pythonpackage.yml' - '**.py' - '**.pyi' - '**.css' @@ -21,27 +22,28 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - architecture: x64 - - name: Install and configure Poetry + - uses: actions/checkout@v3.5.2 + - name: Install and configure Poetry # This could be cached, too... uses: snok/install-poetry@v1.3.3 with: version: 1.4.2 virtualenvs-in-project: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4.6.0 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} - name: Install dependencies run: poetry install --extras "dev" if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - - name: Format check with black - run: | - source $VENV - make format-check # - name: Typecheck with mypy # run: | -# source $VENV # make typecheck - name: Test with pytest run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b136f51..42bf44a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## Unrealeased +## [0.25.0] - 2023-05-17 ### Changed - App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521 +- `DirectoryTree` now loads directory contents in a worker https://github.com/Textualize/textual/issues/2456 +- Only a single error will be written by default, unless in dev mode ("debug" in App.features) https://github.com/Textualize/textual/issues/2480 +- Using `Widget.move_child` where the target and the child being moved are the same is now a no-op https://github.com/Textualize/textual/issues/1743 +- Calling `dismiss` on a screen that is not at the top of the stack now raises an exception https://github.com/Textualize/textual/issues/2575 +- `MessagePump.call_after_refresh` and `MessagePump.call_later` will now return `False` if the callback could not be scheduled. https://github.com/Textualize/textual/pull/2584 ### Fixed @@ -18,6 +23,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed `TreeNode.expand` and `TreeNode.expand_all` not posting a `Tree.NodeExpanded` message https://github.com/Textualize/textual/issues/2535 - Fixed `TreeNode.collapse` and `TreeNode.collapse_all` not posting a `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535 - Fixed `TreeNode.toggle` and `TreeNode.toggle_all` not posting a `Tree.NodeExpanded` or `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535 +- `footer--description` component class was being ignored https://github.com/Textualize/textual/issues/2544 +- Pasting empty selection in `Input` would raise an exception https://github.com/Textualize/textual/issues/2563 +- `Screen.AUTO_FOCUS` now focuses the first _focusable_ widget that matches the selector https://github.com/Textualize/textual/issues/2578 +- `Screen.AUTO_FOCUS` now works on the default screen on startup https://github.com/Textualize/textual/pull/2581 +- Fix for setting dark in App `__init__` https://github.com/Textualize/textual/issues/2583 +- Fix issue with scrolling and docks https://github.com/Textualize/textual/issues/2525 +- Fix not being able to use CSS classes with `Tab` https://github.com/Textualize/textual/pull/2589 + +### Added + +- Class variable `AUTO_FOCUS` to screens https://github.com/Textualize/textual/issues/2457 +- Added `NULL_SPACING` and `NULL_REGION` to geometry.py ## [0.24.1] - 2023-05-08 @@ -948,6 +965,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.25.0]: https://github.com/Textualize/textual/compare/v0.24.1...v0.25.0 [0.24.1]: https://github.com/Textualize/textual/compare/v0.24.0...v0.24.1 [0.24.0]: https://github.com/Textualize/textual/compare/v0.23.0...v0.24.0 [0.23.0]: https://github.com/Textualize/textual/compare/v0.22.3...v0.23.0 diff --git a/docs/custom_theme/main.html b/docs/custom_theme/main.html index 2f490a483..fbbfd659a 100644 --- a/docs/custom_theme/main.html +++ b/docs/custom_theme/main.html @@ -17,4 +17,17 @@ + + {% endblock %} diff --git a/docs/examples/guide/screens/modal02.py b/docs/examples/guide/screens/modal02.py index bb6c49c59..418030f80 100644 --- a/docs/examples/guide/screens/modal02.py +++ b/docs/examples/guide/screens/modal02.py @@ -42,6 +42,7 @@ class ModalApp(App): yield Footer() def action_request_quit(self) -> None: + """Action to display the quit dialog.""" self.push_screen(QuitScreen()) diff --git a/docs/examples/guide/screens/modal03.py b/docs/examples/guide/screens/modal03.py new file mode 100644 index 000000000..e19fc527b --- /dev/null +++ b/docs/examples/guide/screens/modal03.py @@ -0,0 +1,57 @@ +from textual.app import App, ComposeResult +from textual.containers import Grid +from textual.screen import ModalScreen +from textual.widgets import Button, Footer, Header, Label + +TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain.""" + + +class QuitScreen(ModalScreen[bool]): # (1)! + """Screen with a dialog to quit.""" + + def compose(self) -> ComposeResult: + yield Grid( + Label("Are you sure you want to quit?", id="question"), + Button("Quit", variant="error", id="quit"), + Button("Cancel", variant="primary", id="cancel"), + id="dialog", + ) + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "quit": + self.dismiss(True) + else: + self.dismiss(False) + + +class ModalApp(App): + """An app with a modal dialog.""" + + CSS_PATH = "modal01.css" + BINDINGS = [("q", "request_quit", "Quit")] + + def compose(self) -> ComposeResult: + yield Header() + yield Label(TEXT * 8) + yield Footer() + + def action_request_quit(self) -> None: + """Action to display the quit dialog.""" + + def check_quit(quit: bool) -> None: + """Called when QuitScreen is dismissed.""" + if quit: + self.exit() + + self.push_screen(QuitScreen(), check_quit) + + +if __name__ == "__main__": + app = ModalApp() + app.run() diff --git a/docs/examples/how-to/layout.py b/docs/examples/how-to/layout.py new file mode 100644 index 000000000..430670673 --- /dev/null +++ b/docs/examples/how-to/layout.py @@ -0,0 +1,69 @@ +from textual.app import App, ComposeResult +from textual.containers import HorizontalScroll, VerticalScroll +from textual.screen import Screen +from textual.widgets import Placeholder + + +class Header(Placeholder): + DEFAULT_CSS = """ + Header { + height: 3; + dock: top; + } + """ + + +class Footer(Placeholder): + DEFAULT_CSS = """ + Footer { + height: 3; + dock: bottom; + } + """ + + +class Tweet(Placeholder): + DEFAULT_CSS = """ + Tweet { + height: 5; + width: 1fr; + border: tall $background; + } + """ + + +class Column(VerticalScroll): + DEFAULT_CSS = """ + Column { + height: 1fr; + width: 32; + margin: 0 2; + } + """ + + def compose(self) -> ComposeResult: + for tweet_no in range(1, 20): + yield Tweet(id=f"Tweet{tweet_no}") + + +class TweetScreen(Screen): + def compose(self) -> ComposeResult: + yield Header(id="Header") + yield Footer(id="Footer") + with HorizontalScroll(): + yield Column() + yield Column() + yield Column() + yield Column() + + +class LayoutApp(App): + CSS_PATH = "layout.css" + + def on_ready(self) -> None: + self.push_screen(TweetScreen()) + + +if __name__ == "__main__": + app = LayoutApp() + app.run() diff --git a/docs/examples/how-to/layout01.py b/docs/examples/how-to/layout01.py new file mode 100644 index 000000000..2d9c7964b --- /dev/null +++ b/docs/examples/how-to/layout01.py @@ -0,0 +1,27 @@ +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Placeholder + + +class Header(Placeholder): # (1)! + pass + + +class Footer(Placeholder): # (2)! + pass + + +class TweetScreen(Screen): + def compose(self) -> ComposeResult: + yield Header(id="Header") # (3)! + yield Footer(id="Footer") # (4)! + + +class LayoutApp(App): + def on_mount(self) -> None: + self.push_screen(TweetScreen()) + + +if __name__ == "__main__": + app = LayoutApp() + app.run() diff --git a/docs/examples/how-to/layout02.py b/docs/examples/how-to/layout02.py new file mode 100644 index 000000000..7aa39c349 --- /dev/null +++ b/docs/examples/how-to/layout02.py @@ -0,0 +1,37 @@ +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Placeholder + + +class Header(Placeholder): + DEFAULT_CSS = """ + Header { + height: 3; + dock: top; + } + """ + + +class Footer(Placeholder): + DEFAULT_CSS = """ + Footer { + height: 3; + dock: bottom; + } + """ + + +class TweetScreen(Screen): + def compose(self) -> ComposeResult: + yield Header(id="Header") + yield Footer(id="Footer") + + +class LayoutApp(App): + def on_ready(self) -> None: + self.push_screen(TweetScreen()) + + +if __name__ == "__main__": + app = LayoutApp() + app.run() diff --git a/docs/examples/how-to/layout03.py b/docs/examples/how-to/layout03.py new file mode 100644 index 000000000..f5d2d856c --- /dev/null +++ b/docs/examples/how-to/layout03.py @@ -0,0 +1,48 @@ +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Placeholder + + +class Header(Placeholder): + DEFAULT_CSS = """ + Header { + height: 3; + dock: top; + } + """ + + +class Footer(Placeholder): + DEFAULT_CSS = """ + Footer { + height: 3; + dock: bottom; + } + """ + + +class ColumnsContainer(Placeholder): + DEFAULT_CSS = """ + ColumnsContainer { + width: 1fr; + height: 1fr; + border: solid white; + } + """ # (1)! + + +class TweetScreen(Screen): + def compose(self) -> ComposeResult: + yield Header(id="Header") + yield Footer(id="Footer") + yield ColumnsContainer(id="Columns") + + +class LayoutApp(App): + def on_ready(self) -> None: + self.push_screen(TweetScreen()) + + +if __name__ == "__main__": + app = LayoutApp() + app.run() diff --git a/docs/examples/how-to/layout04.py b/docs/examples/how-to/layout04.py new file mode 100644 index 000000000..fbd541336 --- /dev/null +++ b/docs/examples/how-to/layout04.py @@ -0,0 +1,39 @@ +from textual.app import App, ComposeResult +from textual.containers import HorizontalScroll +from textual.screen import Screen +from textual.widgets import Placeholder + + +class Header(Placeholder): + DEFAULT_CSS = """ + Header { + height: 3; + dock: top; + } + """ + + +class Footer(Placeholder): + DEFAULT_CSS = """ + Footer { + height: 3; + dock: bottom; + } + """ + + +class TweetScreen(Screen): + def compose(self) -> ComposeResult: + yield Header(id="Header") + yield Footer(id="Footer") + yield HorizontalScroll() # (1)! + + +class LayoutApp(App): + def on_ready(self) -> None: + self.push_screen(TweetScreen()) + + +if __name__ == "__main__": + app = LayoutApp() + app.run() diff --git a/docs/examples/how-to/layout05.py b/docs/examples/how-to/layout05.py new file mode 100644 index 000000000..1fb6de3a3 --- /dev/null +++ b/docs/examples/how-to/layout05.py @@ -0,0 +1,53 @@ +from textual.app import App, ComposeResult +from textual.containers import HorizontalScroll, VerticalScroll +from textual.screen import Screen +from textual.widgets import Placeholder + + +class Header(Placeholder): + DEFAULT_CSS = """ + Header { + height: 3; + dock: top; + } + """ + + +class Footer(Placeholder): + DEFAULT_CSS = """ + Footer { + height: 3; + dock: bottom; + } + """ + + +class Tweet(Placeholder): + pass + + +class Column(VerticalScroll): + def compose(self) -> ComposeResult: + for tweet_no in range(1, 20): + yield Tweet(id=f"Tweet{tweet_no}") + + +class TweetScreen(Screen): + def compose(self) -> ComposeResult: + yield Header(id="Header") + yield Footer(id="Footer") + with HorizontalScroll(): + yield Column() + yield Column() + yield Column() + yield Column() + + +class LayoutApp(App): + def on_ready(self) -> None: + self.push_screen(TweetScreen()) + + +if __name__ == "__main__": + app = LayoutApp() + app.run() diff --git a/docs/examples/how-to/layout06.py b/docs/examples/how-to/layout06.py new file mode 100644 index 000000000..79be0ab9a --- /dev/null +++ b/docs/examples/how-to/layout06.py @@ -0,0 +1,67 @@ +from textual.app import App, ComposeResult +from textual.containers import HorizontalScroll, VerticalScroll +from textual.screen import Screen +from textual.widgets import Placeholder + + +class Header(Placeholder): + DEFAULT_CSS = """ + Header { + height: 3; + dock: top; + } + """ + + +class Footer(Placeholder): + DEFAULT_CSS = """ + Footer { + height: 3; + dock: bottom; + } + """ + + +class Tweet(Placeholder): + DEFAULT_CSS = """ + Tweet { + height: 5; + width: 1fr; + border: tall $background; + } + """ + + +class Column(VerticalScroll): + DEFAULT_CSS = """ + Column { + height: 1fr; + width: 32; + margin: 0 2; + } + """ + + def compose(self) -> ComposeResult: + for tweet_no in range(1, 20): + yield Tweet(id=f"Tweet{tweet_no}") + + +class TweetScreen(Screen): + def compose(self) -> ComposeResult: + yield Header(id="Header") + yield Footer(id="Footer") + with HorizontalScroll(): + yield Column() + yield Column() + yield Column() + yield Column() + + +class LayoutApp(App): + def on_ready(self) -> None: + self.push_screen(TweetScreen()) + + +if __name__ == "__main__": + app = LayoutApp() + app.run() diff --git a/docs/guide/screens.md b/docs/guide/screens.md index fe8e3616f..0119ef655 100644 --- a/docs/guide/screens.md +++ b/docs/guide/screens.md @@ -219,3 +219,40 @@ Let's see what happens when we use `ModalScreen`. Now when we press ++q++, the dialog is displayed over the main screen. The main screen is darkened to indicate to the user that it is not active, and only the dialog will respond to input. + +## Returning data from screens + +It is a common requirement for screens to be able to return data. +For instance, you may want a screen to show a dialog and have the result of that dialog processed *after* the screen has been popped. + +To return data from a screen, call [`dismiss()`][textual.screen.dismiss] on the screen with the data you wish to return. +This will pop the screen and invoke a callback set when the screen was pushed (with [`push_screen`][textual.app.push_screen]). + +Let's modify the previous example to use `dismiss` rather than an explicit `pop_screen`. + +=== "modal03.py" + + ```python title="modal03.py" hl_lines="15 27-30 47-50 52" + --8<-- "docs/examples/guide/screens/modal03.py" + ``` + + 1. See below for an explanation of the `[bool]` + +=== "modal01.css" + + ```sass title="modal01.css" + --8<-- "docs/examples/guide/screens/modal01.css" + ``` + +In the `on_button_pressed` message handler we call `dismiss` with a boolean that indicates if the user has chosen to quit the app. +This boolean is passed to the `check_quit` function we provided when `QuitScreen` was pushed. + +Although this example behaves the same as the previous code, it is more flexible because it has removed responsibility for exiting from the modal screen to the caller. +This makes it easier for the app to perform any cleanup actions prior to exiting, for example. + +Returning data in this way can help keep your code manageable by making it easy to re-use your `Screen` classes in other contexts. + +### Typing screen results + +You may have noticed in the previous example that we changed the base class to `ModalScreen[bool]`. +The addition of `[bool]` adds typing information that tells the type checker to expect a boolean in the call to `dismiss`, and that any callback set in `push_screen` should also expect the same type. As always, typing is optional in Textual, but this may help you catch bugs. diff --git a/docs/how-to/design-a-layout.md b/docs/how-to/design-a-layout.md new file mode 100644 index 000000000..5123e574a --- /dev/null +++ b/docs/how-to/design-a-layout.md @@ -0,0 +1,200 @@ +# Design a Layout + +This article discusses an approach you can take when designing the layout for your applications. + +Textual's layout system is flexible enough to accommodate just about any application design you could conceive of, but it may be hard to know where to start. We will go through a few tips which will help you get over the initial hurdle of designing an application layout. + + +## Tip 1. Make a sketch + +The initial design of your application is best done with a sketch. +You could use a drawing package such as [Excalidraw](https://excalidraw.com/) for your sketch, but pen and paper is equally as good. + +Start by drawing a rectangle to represent a blank terminal, then draw a rectangle for each element in your application. Annotate each of the rectangles with the content they will contain, and note wether they will scroll (and in what direction). + +For the purposes of this article we are going to design a layout for a Twitter or Mastodon client, which will have a header / footer and a number of columns. + +!!! note + + The approach we are discussing here is applicable even if the app you want to build looks nothing like our sketch! + +Here's our sketch: + +
+--8<-- "docs/images/how-to/layout.excalidraw.svg" +
+ +It's rough, but it's all we need. + + +## Tip 2. Work outside in + +Like a sculpture with a block of marble, it is best to work from the outside towards the center. +If your design has fixed elements (like a header, footer, or sidebar), start with those first. + +In our sketch we have a header and footer. +Since these are the outermost widgets, we will begin by adding them. + +!!! tip + + Textual has builtin [Header](../widgets/header.md) and [Footer](../widgets/footer.md) widgets which you could use in a real application. + +The following example defines an [app](../guide/app.md), a [screen](../guide/screens.md), and our header and footer widgets. +Since we're starting from scratch and don't have any functionality for our widgets, we are going to use the [Placeholder][textual.widgets.Placeholder] widget to help us visualize our design. + +In a real app, we would replace these placeholders with more useful content. + +=== "layout01.py" + + ```python + --8<-- "docs/examples/how-to/layout01.py" + ``` + + 1. The Header widget extends Placeholder. + 2. The footer widget extends Placeholder. + 3. Creates the header widget (the id will be displayed within the placeholder widget). + 4. Creates the footer widget. + +=== "Output" + + ```{.textual path="docs/examples/how-to/layout01.py"} + ``` + +## Tip 3. Apply docks + +This app works, but the header and footer don't behave as expected. +We want both of these widgets to be fixed to an edge of the screen and limited in height. +In Textual this is known as *docking* which you can apply with the [dock](../styles/dock.md) rule. + +We will dock the header and footer to the top and bottom edges of the screen respectively, by adding a little [CSS](../guide/CSS.md) to the widget classes: + +=== "layout02.py" + + ```python hl_lines="7-12 16-21" + --8<-- "docs/examples/how-to/layout02.py" + ``` + +=== "Output" + + ```{.textual path="docs/examples/how-to/layout02.py"} + ``` + +The `DEFAULT_CSS` class variable is used to set CSS directly in Python code. +We could define these in an external CSS file, but writing the CSS inline like this can be convenient if it isn't too complex. + +When you dock a widget, it reduces the available area for other widgets. +This means that Textual will automatically compensate for the 6 additional lines reserved for the header and footer. + +## Tip 4. Use FR Units for flexible things + +After we've added the header and footer, we want the remaining space to be used for the main interface, which will contain the columns in the sketch. +This area is flexible (will change according to the size of the terminal), so how do we ensure that it takes up precisely the space needed? + +The simplest way is to use [fr](../css_types/scalar.md#fraction) units. +By setting both the width and height to `1fr`, we are telling Textual to divide the space equally amongst the remaining widgets. +There is only a single widget, so that widget will fill all of the remaining space. + +Let's make that change. + +=== "layout03.py" + + ```python hl_lines="24-31 38" + --8<-- "docs/examples/how-to/layout03.py" + ``` + + 1. Here's where we set the width and height to `1fr`. We also add a border just to illustrate the dimensions better. + +=== "Output" + + ```{.textual path="docs/examples/how-to/layout03.py"} + ``` + +As you can see, the central Columns area will resize with the terminal window. + +## Tip 5. Use containers + +Before we add content to the Columns area, we have an opportunity to simplify. +Rather than extend `Placeholder` for our `ColumnsContainer` widget, we can use one of the builtin *containers*. +A container is simply a widget designed to *contain* other widgets. +Containers are styled with `fr` units to fill the remaining space so we won't need to add any more CSS. + +Let's replace the `ColumnsContainer` class in the previous example with a `HorizontalScroll` container, which also adds an automatic horizontal scrollbar. + +=== "layout04.py" + + ```python hl_lines="2 29" + --8<-- "docs/examples/how-to/layout04.py" + ``` + + 1. The builtin container widget. + + +=== "Output" + + ```{.textual path="docs/examples/how-to/layout04.py"} + ``` + +The container will appear as blank space until we add some widgets to it. + +Let's add the columns to the `HorizontalScroll`. +A column is itself a container which will have a vertical scrollbar, so we will define our `Column` by subclassing `VerticalScroll`. +In a real app, these columns will likely be added dynamically from some kind of configuration, but let's add 4 to visualize the layout. + +We will also define a `Tweet` placeholder and add a few to each column. + +=== "layout05.py" + + ```python hl_lines="2 25-26 29-32 39-43" + --8<-- "docs/examples/how-to/layout05.py" + ``` + +=== "Output" + + ```{.textual path="docs/examples/how-to/layout05.py"} + ``` + +Note from the output that each `Column` takes a quarter of the screen width. +This happens because `Column` extends a container which has a width of `1fr`. + +It makes more sense for a column in a Twitter / Mastodon client to use a fixed width. +Let's set the width of the columns to 32. + +We also want to reduce the height of each "tweet". +In the real app, you might set the height to "auto" so it fits the content, but lets set it to 5 lines for now. + +Here's the final example and a reminder of the sketch. + +=== "layout06.py" + + ```python hl_lines="25-32 36-46" + --8<-- "docs/examples/how-to/layout06.py" + ``` + +=== "Output" + + ```{.textual path="docs/examples/how-to/layout06.py" columns="100" lines="32"} + ``` + +=== "Sketch" + +
+ --8<-- "docs/images/how-to/layout.excalidraw.svg" +
+ + +A layout like this is a great starting point. +In a real app, you would start replacing each of the placeholders with [builtin](../widget_gallery.md) or [custom](../guide/widgets.md) widgets. + + +## Summary + +Layout is the first thing you will tackle when building a Textual app. +The following tips will help you get started. + +1. Make a sketch (pen and paper is fine). +2. Work outside in. Start with the entire space of the terminal, add the outermost content first. +3. Dock fixed widgets. If the content doesn't move or scroll, you probably want to *dock* it. +4. Make use of `fr` for flexible space within layouts. +5. Use containers to contain other widgets, particularly if they scroll! + +If you need further help, we are here to [help](/help/). diff --git a/docs/how-to/index.md b/docs/how-to/index.md new file mode 100644 index 000000000..c127ae174 --- /dev/null +++ b/docs/how-to/index.md @@ -0,0 +1,7 @@ +# How To + +Welcome to the How To section. + +Here you will find How To articles which cover various topics at a higher level than the Guide or Reference. +We will be adding more articles in the future. +If there is anything you would like to see covered, [open an issue](https://github.com/Textualize/textual/issues) in the Textual repository! diff --git a/docs/images/how-to/layout.excalidraw.svg b/docs/images/how-to/layout.excalidraw.svg new file mode 100644 index 000000000..9e4b53f19 --- /dev/null +++ b/docs/images/how-to/layout.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWK7kv1+f0WQ92VcdTAwMDa46ktcdTAwMTbJXCJ5gcHAi1x1MDAxY1mOl8iKt8GDoaUtyda+eHvIf5+ivKjVUsstW7JbuTaQxGktzWafOudcdTAwMTRZTf7njy9fvvbv2v7Xv7989W9LhXqt3C3cfP3TXHUwMDFkv/a7vVqrSS/B8P+91qBbXHUwMDFhvrPa77d7f//1V6PQvfL77Xqh5HvXtd6gUO/1XHUwMDA35VrLK7VcdTAwMWF/1fp+o/e/7u+9QsP/n3arUe53vdFJUn651m91XHUwMDFmzuXX/Ybf7Pfo2/+P/v/ly3+Gf1x1MDAwN1rX9Uv9QrNS94dcdTAwMWZcdTAwMTi+NGogZ0aFXHUwMDBm77Waw9ZyLoxGxVxyPL+j1tukXHUwMDEz9v0yvXxBjfZHr7hDX1x1MDAwZnL9dT+3q/ay56WbXCK2djHdXHUwMDFkjM57UavXXHUwMDBm+3f1YbtK3Vavl6pcdTAwMTb6peroXHUwMDFkvX63deVcdTAwMWbXyv3qU/dcdTAwMDWOP3+216KuXHUwMDE4farbXHUwMDFhVKpNv+d6gT9cdTAwMWZttVx1MDAwYqVa/254lez56ENX/P1ldOSW/pdC4Vx0LY1VqLW1KtAr7lx1MDAwYkAoXHUwMDBmhFx1MDAxNKhcdTAwMDVarahfQi3baNXpjlDL/sWGP6O2XHUwMDE1XHUwMDBipatcbjWwWX5+T79baPbahS7dt9H7blx1MDAxZa9ZSemhcCdjinM+akfVr1WqfXqHQOZcdTAwMTnOrJQohLs9OGqMP7w1XHUwMDFjjNCopVx1MDAxNs+vuCa0t8tDnPw72HHN8mPHNVx1MDAwN/X6qNXuhXRcdTAwMDBbo89cZtrlwlx1MDAwM1x1MDAwMjhcdTAwMWFcdIpJ1Fxio1x1MDAwZanXmlfhr6u3Slcj0Fxmj/7681x1MDAxNXDVKlx1MDAxYa1WcVx1MDAwNIlcdTAwMThcdTAwMWKsuVLn5367sL63s3+Lza2fXHUwMDA3nZ3tTlx1MDAwNFhDgPtImFx1MDAxYcGVtIpxJjhBIIRT6Vx1MDAxOaUlgcJcdTAwMDJKi8vEqfBcdTAwMThHXHUwMDA1dD6urTaTQFx1MDAwNeMhMiFccjJAQaCeXHUwMDAwKt0zJYGuZvVw6tfrtXZvKkpxXHUwMDA2p1x1MDAxYZCcI1cmNkzbP86OS+lstssvc9f5+3TqrtG8fVxyTPn7wVRxz1xuhdTxQljOQIbolDjOMCMl11x1MDAwZcqgw1x1MDAxMTRcdTAwMTdM/3VRUKBgXHUwMDEyolxceJJcdTAwMDEoi0D/SGPkJEY5eFxuKVKISFx1MDAxOVxiXHUwMDAxUocxyql9ilNcdTAwMWL5b1x1MDAwNVItbFx1MDAxNEi1QXfNXHUwMDEwX/dP1jrpu8ZB7qC4tnXT2upvna3jTcIxXG7KY9TdyLnhlsBcdTAwMDEhiFxuT1x1MDAxObRkXG4kUZhV8o1cdTAwMTAtMqaWXHUwMDA1Uc1cdTAwMTXpgeC/XHUwMDE5Qq2IQijnjJSFS81iQ7TeSbdTV1x1MDAxNexs1upXWLrrb53UXHUwMDBlk1xyUVx1MDAxMljgXHUwMDAyQWjBmWRWh1x1MDAxMFxujmQl3Vx1MDAxNVxyxLVv41BcdTAwMGVFY3BZXHUwMDAwpcsgqkcuV1DoZ+dPKlx1MDAxMqPu1iihXHUwMDAzb3hcdKLr37ZcbrsldbLV2V1vnt3+OL6pXHUwMDFjNJJcctFcdTAwMTTlXCKk4lZcdTAwMTFFcnBJyThGXHUwMDExXHQ+hFxmTTTKJcOlYFRJ5oym4Vx1MDAxNCnaQiBOXHUwMDAyOZMnNaekytphO0CFIUquTJGLNYrNXHUwMDAx0SewjOBcIlx1MDAxZY/8ikbu82dGn1x1MDAwZcCt79+O3HZcdTAwMDBcdTAwMWPd9mG1cbB9tj/YK+VzlbXWgVx1MDAxOdx9fX7fr8ffosJCXGImjTLWLiosxtpcdTAwMTmMXGJcdTAwMTNcdTAwMTVcdTAwMTCczs5dvqBiR8T0i55cdTAwMWVcdTAwMTHVQqk66PpcdTAwMWZcdTAwMWZcdTAwMTNcXCO5W+JKQIK+YTLE21x1MDAxYTxcdTAwMDdB4nNcdTAwMTlcbpnXXHUwMDA0XHUwMDA1xVx1MDAxZbuYYn4p8oBcdTAwMTPaXHUwMDE5p1x1MDAwZSeRmJafTVx1MDAwNoFcdTAwMDWuyZ3PXHUwMDE1XHUwMDA0r+Pp11x1MDAwMHJ041vN/mHt3vU9sLGjW4VGrX43du+GSKWuyviFst/9OnZ8rV6rONR+LVF7gy9cdTAwMTJ2+7VSof78hkatXFxcdTAwMGVqQIlOVqg1/e52XHUwMDFj7m51a5Vas1DPR7SFrt3PPOupXHUwMDE3uDPFQs93r1x1MDAwZVx1MDAwM+j1Qlx1MDAwNVx1MDAxOJmTkns0QltcdTAwMTN/5GRtc6O+M2Bddn/XuK/YfL5/lc68LizfcexEXHUwMDE5j5NRkoxsfUhcdTAwMDPcN1xihp7ipGQ8XHUwMDE4XHUwMDE3XHUwMDBiXHUwMDFlNeGKuEFScvEkiKPrXHUwMDFmhSX5KbCGXHUwMDFisnXKqCA/PLkp0IZy6sDgz1x1MDAxMt2UJt5cdTAwMGVkP0t1U1x1MDAwMapcZouHM/zW5fmxUdqpbbT2sr3NXGZ2N1x1MDAwZTK57av9evpV43vvOXBcIjy6t+hcdTAwMTJwMis6hFBuKCdcdTAwMTX0gmIhYLzGTalCoXgh5TTPzz1GSIUpg89Selx1MDAxMjSzj01cYjbxXHUwMDExnUJcdTAwMWKlmVx0XGZcYr5cdTAwMTWcrzNM2Xbnvt45Ov/x48f5Sf3i6vgwfXxcdTAwMWMwTH9O/9qHXHUwMDBm51t7lUIu5Z9095pH9lx1MDAxMCv1y+1cdTAwMWbjZ3k6f6Hbbd3MYcQ4XHUwMDAwhZRQi4qoSCNcdTAwMTY5vkMmjGhcdTAwMGWtiZ+aTO/MhFx1MDAxYrFcdTAwMTRcdTAwMTGpIOtDySBlXHUwMDA1aMLhJJin6bBloFxirnYh2cm0eJLW46QsmnJBy6ZGVcDEPI2MazJvxr5cdTAwMDPFXHUwMDBiSpjIi1x1MDAwNYj1dUaM49jRXHUwMDE5Rix/4/v9JfmwXHUwMDE3SD/sw0JNiWfD5Fx1MDAxYmyYgPDR5/FcdTAwMDLBXGYjb1x1MDAxMt+GVXnaNrYvdzqp6lx1MDAwNuxfyrPTn0onX+BcZnJcdTAwMDGGh+1cdTAwMTdcdTAwMWGPRIVcdTAwMDJEaGCaiVCbPsUteOu3mtlccr9/36hWz5pq/7qU7drcSXxcdTAwMTFcdTAwMDLFKVx1MDAwMdNq+aNcdTAwMDFcdTAwMDFWXGKrkLbcXHUwMDE4w+dcdTAwMTggm37VK6BCJD+KSWHleHYxxL1hXHUwMDFlJSXyzXnHylx1MDAwYpCYXHUwMDAzjMlcdTAwMTWgXHUwMDE3SPnDXHUwMDA1yLLw0ZEtVJJroUT8uen1k5udY5VLnzXRXFzdf9va6ejNXtJcdTAwMTWISN5cdTAwMDBcdC03XHUwMDFjJ4dcdTAwMDEkMOp24iVOIYtGvNFcdTAwMTT+5jqUXHUwMDFmXFyv5fa+X/LU/u3deW692Pl29X1OXHUwMDFkUjJQXHUwMDE4sixcdTAwMWTSkb5cdTAwMGKYkFxcQrBW5iXYT7/qxOuQ8JRigCBcdTAwMTXnlIKG0iHiYU9qXHUwMDEwlFx1MDAwYjFBXHUwMDE27Y1cdTAwMDbsU4xcdTAwMTIgRi/w80eLkTA8fPQpKlxySM1cdTAwMDH1aIzrpaBs9b5cdTAwMWSet9bWe2epn+1jW7lnJ7yQdC1SXHUwMDFlkTjprlx1MDAxNVO1SLteXHUwMDE3Slx1MDAxYm6QXHTFlydGU8ah5cTAc1JU5yB9WG9uXHUwMDFmVlx1MDAwN/5ajde+dUSnXdqZU3U0LF91Zk26kOxcdTAwMDCoOZKf6Vx1MDAxN71cdTAwMDKig0hcdTAwMTIvNJsqOkYsXHUwMDE04Z+qk1x1MDAwMNV5gYk/WnU4RqpcdTAwMGVcdTAwMDHEWUGr45codPZcdTAwMGWvXHUwMDFi6Hc6WXPon9rDwkb7eDPZsmOYp13dNVk9YVx0fCM6f5hlolBcdTAwMTGCXCKTQmk8Yl9cdTAwMTOSXHUwMDAwpuhPKywzXHUwMDFlc1Vj04ORXGLa01x1MDAxOJ6MfZz+ZHTcXG7BR/q0xFwiXHUwMDA1VFxc2PcpJlx1MDAwM1x1MDAxYpmkcCGlq+Ww8f3Qjl83379l6rtcdTAwMTfqqr2pdLfWbq4nXHUwMDFimK6aXGaBOdplXHUwMDE2UYcqXHUwMDFlldJcdTAwMWWlxXRYg5VcdTAwMTbeVvFooGS5P1x0zH9ENVmnWMzk8T6T7V3ft3WuslNax/R8XHUwMDBlytBcdTAwMWTgi4qLyLxcdTAwMWRlVEhoXHUwMDAzwDmzc1D11ItOuIPiWntcdTAwMTKtq8IgymMqVE2mUHiMoXtcdTAwMDe5TXzjXHUwMDEwcmQxmSTAWyvoNCBcZlx1MDAwNNK2hFx1MDAxNJMtyD/FLybbarX6Sysme4G6w1x1MDAwNirclqVcdTAwMTeTXHUwMDA1XHUwMDA3JENRKSkkiacxvk5cdTAwMWSfyduLTnFXZHbSm5mN3Lp/1j9KejFcdTAwMTnnnPyJtGjMw2zl6GtcdTAwMWVqySxFpXJPOHF8c43nSlaUTdelx6Gajla63vJ57fREXFyKrl/cuD1fWHVccmk2W5gwzTZsMnp2k9hcdTAwMWHcJEL86fxcdTAwMDNd6N521i94tre/0Thkx1x1MDAwN+e902RcdTAwMWI2QrdngFx1MDAxMftCuJZ5XHUwMDE4XHUwMDA2wDyrjEW7iDD47SdT1vKnu9vbP1x1MDAwZnY2+1x1MDAxOdk46lx1MDAxZmyd5W3cirXs9o+LU6wp1chu9ZrXR5XCzdXu4irW3NTg0s1cdTAwMWXMKP90s0RcdTAwMWGZjVx1MDAxZE7TezPpbo9SXHUwMDFjrYiOXHUwMDE5Ob6JXHUwMDA0SFxi4ZGlUlxcPZYwvy2cPlx1MDAwN8timr1lXHUwMDBllr3A+lx1MDAxZj1YJmT0xKlcIoiilvGHsEV3/fJ252fZ7MHZcVVV8VStdZOvcKRvJOVcdTAwMTiWsGFEalI4So0oXHUwMDAzs1x1MDAwYnjA7TdcdTAwMTe4/PptrWvWXHUwMDBlb1tcdTAwMWIoikff7zavXHUwMDBi+3M9wyZcdTAwMTRcYlxc+qhDcGooLEQgmUBleXzUT7/qxFx1MDAwYlx1MDAxMXpcdTAwMTZdMkdJ/6RcdTAwMTBcdTAwMTlcdTAwMTJcIvc4r3vY9+3FmqsqRK/AY3KF6Fx1MDAwNXL+cCGy0c8zUL5H2TaK+KtcdTAwMDGcsfNO6qJvMsfVtZ3Uxam6zlx1MDAwNzLQhCqR8lx1MDAxNFx1MDAwNzR0qUJPKJFcdTAwMDTwXGZl92RcdTAwMWQ1uHj5VKJcdTAwMTlKxLLp0rbMs61Byp5cdTAwMWOcXHUwMDE2JWtcdTAwMTVcdTAwMDbzjX9LYiZcdTAwMTbE/1KUKLpCXHUwMDA2NWHBTVx1MDAwYsVcdTAwMDb99IteXHUwMDAxIZJuXHUwMDEwmaDjVlx1MDAxYVx1MDAxOFx1MDAxN1wiKZTHkFx1MDAwMGdcZqi3V2uuqlx1MDAxMP1WXHUwMDE50Vx1MDAwYtz80UIkZ1RQk1WSXHUwMDA20cZcdTAwMTeiXvNO+er7fjs/yFxmWje8t7X5bTfpQiQ98r9aXHUwMDAyQ+VcbmdHXGb0ULQmSIiY0kYwMz5h9Y8uWjtcdTAwMTPrezc3crN4spMvpG4vq7uVg0pcdTAwMDIlR+jIKVeyIHS/jOXxNWf6Va+A5lxi4chcdTAwMWXc8LWRIatl1EJcdTAwMTG+4qozz9pcdTAwMDDJVZ1cdTAwMTeI+KNVh7xeVFSCMlxiwOJPNG01N3c7W6ksbMP5oLnm64vNb1FcdTAwMDNcdTAwMTJcdNFcdTAwMWNQzFOMXHUwMDFiTVx1MDAxZW+yXGJCXHUwMDAwp9RILWpJnZUuWVx1MDAxYq7Ywd6rZE1gdMlcdTAwMWFcdTAwMThB3oDJ+KVcdTAwMDCbOyeH7eyp3fyW/nFweqK+i3w/asWOxJRcdTAwMDLQZXqKYFx1MDAwMUrgtFx1MDAxMWLOPFSWhJPDm59r/u0qXHUwMDAxdvs3p8Wj01x1MDAxY5rMnbzoraW3blpnXHUwMDBim7WUYFx1MDAwMlx1MDAxNfhLrVx1MDAwNFx1MDAxMJFcdTAwMTUxnJO2U0Ns/JVrjptnxVRd18o1xnJZOEinipuphFx1MDAxM7QxRNBaI2pwSz2HaopcdTAwMDE8gr5WXGa5q/JXn/Mks1KFa57ezd3traV+yPtTuGfFjctKMW4hwPFdeW3ztsVcdTAwMGYzuWo5n8FBrnmbW1xcIYCVbOkpyMxVXHUwMDAztNKcXHUwMDFiXHUwMDFi/2nN6b2Z8Fx1MDAxNITMtSeFVdZcdTAwMTlszoJcdTAwMTHzUFx04HJw7Vx1MDAxYziGXHUwMDE20/hMQVY1XHUwMDA1eYH2PzpcdTAwMDVcdTAwMTFcInLgXHUwMDBiKE92i1xixlx1MDAxZvdK93aE/H7q31xcXHUwMDFmd1x1MDAwYmv3ftpcdTAwMWVVo8aiXHUwMDEzI3HaXHUwMDE1u1lheFjEXHUwMDFlSlx1MDAwMcAjkUelXHUwMDFmpkTDXHL7lLjg/b+6YoNGZuOuVsZu75JlZfv84GLO0TD3Z9lSXHUwMDA0PPpcdTAwMDFcdTAwMDRAVFx1MDAwNIb4XHUwMDBmIEy/6MQrkXElaVx1MDAxYbmRjPAvxlFvSIhIXHUwMDE4mISQ7/vUoVXVoVx1MDAxN7j5w3XIRJaJUrRcbuJnmGNcdJutVvU8ZVtHfnX/JNPM1lxuulHNJl+IXGJtQqCSROdhIZIgPWRMuVx1MDAxZFjQ2OC04KdcdTAwMTJNKtH29fqV8Sv5Sr3Fi6Zlv12y9LzzMu+iRFx1MDAxOF1cdTAwMDAjlVuxiFx1MDAwNYpcdTAwMDVegv30q15cdTAwMDEpcvVWyEBcdTAwMWJcdTAwMTGWXCIp0DPcjXxcdG5ccshFbIjxKUVcdTAwMWYrRS+w80dLkdSRUoSKvJLSc9Sk2SNbNlBLr/XyV6XizV1e/cgmfCVcdTAwMDEw6GlGcWCUskqHXHUwMDAzUitPUpxQsqRcdTAwMDAkLi8jWqlCgFx1MDAxMzi9zJTklrxs71c3dnZz+6U7lUDBXHUwMDEx0etkSPdwXHUwMDFiSX98vZl+0SugN1x1MDAxY6zbeskyJoIrRD/UXHUwMDAx6EXi+1NwXHUwMDEyIDgvkPBHXHUwMDBiXHUwMDBlRG+AqrVGXHUwMDAwruOnPt30UZrt7O83hTpJy8Za5uw+9zPZguOqnIGSXG5BXHUwMDA0NNysYywgXHUwMDA1uMdcdTAwMTJcdTAwMDJjcG9cbsjVL1x1MDAwM1x1MDAxMDqQXGIvtVxmQEfv+oRCgWVyjpVr9lmrv5nL+oY31/X6xe4mXjRcdTAwMTO/qix6buEowp52i5mFXHUwMDE28qNcdTAwMWXwrFvv+nFcdTAwMTW0t42SRVx1MDAwMVO6labdhlNuq1x1MDAwM/c1OFx0T5Bu116SXHUwMDExPVxcx1x1MDAwMyZcdTAwMGJcdTAwMDDeXHUwMDE3o1x1MDAxMoRcXPoq5DNYU7n1XHUwMDAyOJujiv460ylmXHUwMDFh9udm+eLozFx1MDAxN1x1MDAwNzlTgGbSnVxmXHUwMDExJlx1MDAwMVx1MDAxMNFwI1x1MDAxMVhgfYSHrSS14yxOyTORhjViedtGk5eR5ExcdTAwMTRcYlx1MDAwYqFAmbGUXGY1yY1wwLKt+9KLUlx1MDAxNuSV5linpnbrlyO8Ut2/6M9wSv1WO8omjTV3Yjma8VMuYjWa6MdjXCLHxITbbFCjiV8nUDXr+3fn1fIgla1s5Vx1MDAxYq3LuzxL/OxcZoWFR1xcL4XVhlEqMl58hpx0h6SXQKeMU55/dFxc58hcdTAwMTZcdTAwMTbyV2dcXF9cdTAwMWNuX36HUjrTb3/G9Vx1MDAwN8b1QzdPM5Qztlx1MDAxMVVuu2vkMv7I2mxCT2pkg/bc9ijKKKVItsdcdTAwMWQloPBcdTAwMDSS41x1MDAxNNxcIuCbxrpnXHUwMDA2tmWeNW5FVMtcdTAwMTRcdTAwMTnHXHUwMDEx31x1MDAwNi0loqA2MOOe0MNcdEvJiXtcdTAwMTSToOdcdTAwMTmMmFxcXGZcdTAwMTFcdTAwMWWPzFhcZnFG5Zog9tOv2my01y90++u1ZrnWrIw3zH849XZcZl84jOjSwLWSeaApYXLzXHUwMDE0bjhSju6s65hC2/G2Ry6c2lxmlj9MsE1cXLrfLL/cpNn11IEmpZhnXHUwMDE0pztkhit6XHUwMDE4hWKiTZR5o9vcW5I9dONQdqJN9UKvv9FqNGp96vmDVq3ZXHUwMDBm9/CwK9dcXNBX/cJcdTAwMDTh0DVcdTAwMDVfXHUwMDBis0PbfeM4yY9++zKKnuF/nn//959T352KwrX7mUD06Nv+XGL+Ozet2ehHq1xmUr9Solx1MDAxY3/4ZracJZXVXHUwMDE0o1x1MDAwNJQpUIbwL9n4g1x1MDAxY1xi3ONusFx1MDAwMJlcdTAwMTaISyyWt8JDbYZLW4xtXGY4mj/gXHUwMDFlXHUwMDEzlFxmW0roXHRcdTAwMWWT01x0XHUwMDFjKWWiTJnNY15cdTAwMTbOasxcYnzVY78xWW22KVx1MDAxZadcdTAwMTBiNNdXXGJARKLZiO6fOcR40m1cdTAwMDIurGTuwVFjZvPa+HWsXHUwMDE0u0yH1/C1XHRkzckukfM1NnIyUlx0NG5p8/iL1lx1MDAxY1X29zZQ9vzaXHUwMDFl+Nnvt53Dw42bpJNLSqJLd4hBrEIlJIyzXHUwMDBiV+5JXHUwMDFkrVxyMolumONNw3Czn8Ux2uNCXHUwMDE5lNHZ0MScXHK3lFxyKVx1MDAwN4x3SYfesMftn7O+91xyK1x1MDAxMc783iU+2OD2one9/8b0Lf5cdTAwMTRcdTAwMTahaNBo9r7811N29qVX6rbq9f9+35QuRjNcdTAwMTYxpVx1MDAxNemHzIxcdTAwMDc9lFtseq5nzWdcdTAwMDM6oZwljEdGXHUwMDEzrHFcdTAwMWJccihcdTAwMDXjXHUwMDAzOI5HXHUwMDE0XHUwMDEzRltcdTAwMTKS5Y7MkunVJM1cbshccmshg9tCjCa3QHiGh2r+nthcdTAwMGKcnVx1MDAxYptSeG8/5MLY7dm3RD80W1x1MDAxN8ezPNBkcyVKpa1Ukk2meZy6XFxcInPVkbONUFRrZu92PO7OXHUwMDE4Uk5jQIOWXG5IlfREczQhkfwsvW4lXHUwMDEycuVEo1bKg0VcdTAwMDPa/Vx1MDAwNKE8p1x1MDAwMYukM2TRdCY510zMs13BbFx1MDAxZE0onVnjaW5cdGSEXCJcdCZUmsxccvfc7CRcYiWXTGfaeJRmSLe/MFx1MDAxMavk09hMoEfo0I55hzvdXHUwMDA0nkp95LRhnVx1MDAwMVx1MDAwNFx1MDAxN194f05T1Ly5dtpYXHUwMDFhp1x1MDAxMYtI4G6tU1wiXG6plbTTSMRyx2jSXHUwMDBlf8zkMFEsZpu9LO5cdTAwMTjPckKVXHUwMDE0YIxcdTAwMDX6VUzSLHiUhJEyuM50T2/halx1MDAxM1skst3PJKZcdTAwMTdGbjxyXHUwMDEynVTOclxu9vjcNtvLJ5TbXGI4XHUwMDFlKkBKLCWA0eOT6NxQz1x1MDAxM945d0umkl1b3pg8R+5m891CXHUwMDBlklx1MDAwMlLBlEJcdTAwMGbKOSlYJcWhXHUwMDFlrvRcdTAwMTBMdlx1MDAxZshNKtRqrlxc8/d1a47ZyPRcZvdop0ZJXHUwMDE2XHUwMDFjXHUwMDEwfnJrZFx1MDAxOEjFXHTsZlg680rPNvsp33FmQ+ZG+MDtduVOOTko71x1MDAxNlx1MDAxNzFuSS73yFx1MDAwMzVcdHGiUatEbdHAXHUwMDFlvjpcdTAwMDHpObkter3nSNvGnfZqPUel88Fpo8GuQF9tnaX3i93SbvbnbtRcIueJoTaNnuSkXHUwMDE3XHUwMDA0fjfXKMeX70CiNiNcZqM/Llx1MDAwZmVLXHUwMDFjN1NIpt1cdTAwMWHlRuhcZuppS9hMKatcdTAwMTSMzLVcclxc5YruXvNYRvDWcaj4ZVx1MDAwNNVWt3bvxoueRn7ed/xpxumXWl5cdTAwMTCoXHUwMDFiXHUwMDBll1xyKcvA7VpcdTAwMTY73mfDIaHxzqX2iF6F21x1MDAwN5TcitLjhdSopVx1MDAwN4xcdTAwMWOkojcuN+Ct52qDrZ3Ykz7gZKwnnVx1MDAwMNqIkSdtXHUwMDE5XHSHlfOE/6In4ubedWpeKzNbVr6EJuKoP9zjXCJ0f8mowlx1MDAxNN/gJvNcdTAwMWZ2d32li5m5a9uYi2Hu4Vx1MDAxOMVcdTAwMDRnROtcdTAwMDKnjIOBoFx1MDAxYqxcdTAwMWTna5fIMTZ5L1bKxURh2v2kJuBcdTAwMWNlYv54PMHXQrt92Ce4Pd9cdTAwMGXCd638yPWjq/x6XfNv1qfUi19cZn+cXGZccvvTUZI/jIFff/z6f3dfuVAifQ== + + + + HeaderTweetTweetTweetTweetFooterTweetTweetTweetTweetTweetTweetTweetTweetFixedFixedColumns (vertical scroll)horizontal scroll diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index 6688f3ca3..f5a9ab4f1 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -157,6 +157,7 @@ nav: - "widgets/text_log.md" - "widgets/tree.md" - API: + - "api/index.md" - "api/app.md" - "api/await_remove.md" - "api/binding.md" @@ -189,6 +190,9 @@ nav: - "api/work.md" - "api/worker.md" - "api/worker_manager.md" + - "How To": + - "how-to/index.md" + - "how-to/design-a-layout.md" - "roadmap.md" - "Blog": - blog/index.md diff --git a/pyproject.toml b/pyproject.toml index af9942407..5f84cbc23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.24.1" +version = "0.25.0" homepage = "https://github.com/Textualize/textual" description = "Modern Text User Interface framework" authors = ["Will McGugan "] @@ -83,5 +83,5 @@ markers = [ ] [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core>=1.2.0"] build-backend = "poetry.core.masonry.api" diff --git a/reference/spacing.monopic b/reference/spacing.monopic new file mode 100644 index 000000000..474d7d3ad Binary files /dev/null and b/reference/spacing.monopic differ diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py index b415d8983..87879573a 100644 --- a/src/textual/_arrange.py +++ b/src/textual/_arrange.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import defaultdict from fractions import Fraction from operator import attrgetter -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING, Iterable, Mapping, Sequence from ._layout import DockArrangeResult, WidgetPlacement from ._partition import partition @@ -16,6 +16,21 @@ if TYPE_CHECKING: TOP_Z = 2**31 - 1 +def _build_dock_layers(widgets: Iterable[Widget]) -> Mapping[str, Sequence[Widget]]: + """Organize widgets into layers. + + Args: + widgets: The widgets. + + Returns: + A mapping of layer name onto the widgets within the layer. + """ + layers: defaultdict[str, list[Widget]] = defaultdict(list) + for widget in widgets: + layers[widget.layer].append(widget) + return layers + + def arrange( widget: Widget, children: Sequence[Widget], size: Size, viewport: Size ) -> DockArrangeResult: @@ -30,107 +45,120 @@ def arrange( Widget arrangement information. """ - arrange_widgets: set[Widget] = set() - - dock_layers: defaultdict[str, list[Widget]] = defaultdict(list) - for child in children: - if child.display: - dock_layers[child.layer].append(child) - - width, height = size - placements: list[WidgetPlacement] = [] - add_placement = placements.append - - _WidgetPlacement = WidgetPlacement - top_z = TOP_Z scroll_spacing = Spacing() - null_spacing = Spacing() get_dock = attrgetter("styles.dock") styles = widget.styles + # Widgets which will be displayed + display_widgets = [child for child in children if child.styles.display != "none"] + + # Widgets organized into layers + dock_layers = _build_dock_layers(display_widgets) + layer_region = size.region for widgets in dock_layers.values(): region = layer_region + # Partition widgets into "layout" widgets (those that appears in the normal 'flow' of the + # document), and "dock" widgets which are positioned relative to an edge layout_widgets, dock_widgets = partition(get_dock, widgets) - arrange_widgets.update(dock_widgets) - top = right = bottom = left = 0 - - for dock_widget in dock_widgets: - edge = dock_widget.styles.dock - - box_model = dock_widget._get_box_model( - size, viewport, Fraction(size.width), Fraction(size.height) - ) - widget_width_fraction, widget_height_fraction, margin = box_model - - widget_width = int(widget_width_fraction) + margin.width - widget_height = int(widget_height_fraction) + margin.height - - if edge == "bottom": - dock_region = Region( - 0, height - widget_height, widget_width, widget_height - ) - bottom = max(bottom, widget_height) - elif edge == "top": - dock_region = Region(0, 0, widget_width, widget_height) - top = max(top, widget_height) - elif edge == "left": - dock_region = Region(0, 0, widget_width, widget_height) - left = max(left, widget_width) - elif edge == "right": - dock_region = Region( - width - widget_width, 0, widget_width, widget_height - ) - right = max(right, widget_width) - else: - # Should not occur, mainly to keep Mypy happy - raise AssertionError("invalid value for edge") # pragma: no-cover - - align_offset = dock_widget.styles._align_size( - (widget_width, widget_height), size - ) - dock_region = dock_region.shrink(margin).translate(align_offset) - add_placement( - _WidgetPlacement(dock_region, null_spacing, dock_widget, top_z, True) - ) - - dock_spacing = Spacing(top, right, bottom, left) - region = region.shrink(dock_spacing) - layout_placements, arranged_layout_widgets = widget._layout.arrange( - widget, layout_widgets, region.size + # Arrange docked widgets + _dock_placements, dock_spacing = _arrange_dock_widgets( + dock_widgets, size, viewport ) - if arranged_layout_widgets: + placements.extend(_dock_placements) + + # Reduce the region to compensate for docked widgets + region = region.shrink(dock_spacing) + + if layout_widgets: + # Arrange layout widgets (i.e. not docked) + layout_placements = widget._layout.arrange( + widget, + layout_widgets, + region.size, + ) + scroll_spacing = scroll_spacing.grow_maximum(dock_spacing) - arrange_widgets.update(arranged_layout_widgets) placement_offset = region.offset + # Perform any alignment of the widgets. if styles.align_horizontal != "left" or styles.align_vertical != "top": - placement_size = Region.from_union( - [ - placement.region.grow(placement.margin) - for placement in layout_placements - ] - ).size + bounding_region = WidgetPlacement.get_bounds(layout_placements) placement_offset += styles._align_size( - placement_size, region.size + bounding_region.size, region.size ).clamped if placement_offset: - layout_placements = [ - _WidgetPlacement( - _region + placement_offset, - margin, - layout_widget, - order, - fixed, - overlay, - ) - for _region, margin, layout_widget, order, fixed, overlay in layout_placements - ] + # Translate placements if required. + layout_placements = WidgetPlacement.translate( + layout_placements, placement_offset + ) - placements.extend(layout_placements) + placements.extend(layout_placements) - return DockArrangeResult(placements, arrange_widgets, scroll_spacing) + return DockArrangeResult(placements, set(display_widgets), scroll_spacing) + + +def _arrange_dock_widgets( + dock_widgets: Sequence[Widget], size: Size, viewport: Size +) -> tuple[list[WidgetPlacement], Spacing]: + """Arrange widgets which are *docked*. + + Args: + dock_widgets: Widgets with a non-empty dock. + size: Size of the container. + viewport: Size of the viewport. + + Returns: + A tuple of widget placements, and additional spacing around them + """ + _WidgetPlacement = WidgetPlacement + top_z = TOP_Z + width, height = size + null_spacing = Spacing() + + top = right = bottom = left = 0 + + placements: list[WidgetPlacement] = [] + append_placement = placements.append + + for dock_widget in dock_widgets: + edge = dock_widget.styles.dock + + box_model = dock_widget._get_box_model( + size, viewport, Fraction(size.width), Fraction(size.height) + ) + widget_width_fraction, widget_height_fraction, margin = box_model + + widget_width = int(widget_width_fraction) + margin.width + widget_height = int(widget_height_fraction) + margin.height + + if edge == "bottom": + dock_region = Region(0, height - widget_height, widget_width, widget_height) + bottom = max(bottom, widget_height) + elif edge == "top": + dock_region = Region(0, 0, widget_width, widget_height) + top = max(top, widget_height) + elif edge == "left": + dock_region = Region(0, 0, widget_width, widget_height) + left = max(left, widget_width) + elif edge == "right": + dock_region = Region(width - widget_width, 0, widget_width, widget_height) + right = max(right, widget_width) + else: + # Should not occur, mainly to keep Mypy happy + raise AssertionError("invalid value for edge") # pragma: no-cover + + align_offset = dock_widget.styles._align_size( + (widget_width, widget_height), size + ) + dock_region = dock_region.shrink(margin).translate(align_offset) + append_placement( + _WidgetPlacement(dock_region, null_spacing, dock_widget, top_z, True) + ) + dock_spacing = Spacing(top, right, bottom, left) + + return (placements, dock_spacing) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 61e4f3de8..300b52029 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -33,7 +33,7 @@ from . import errors from ._cells import cell_len from ._context import visible_screen_stack from ._loop import loop_last -from .geometry import NULL_OFFSET, Offset, Region, Size +from .geometry import NULL_OFFSET, NULL_SPACING, Offset, Region, Size, Spacing from .strip import Strip, StripRenderable if TYPE_CHECKING: @@ -71,6 +71,8 @@ class MapGeometry(NamedTuple): """The container [size][textual.geometry.Size] (area not occupied by scrollbars).""" virtual_region: Region """The [region][textual.geometry.Region] relative to the container (but not necessarily visible).""" + dock_gutter: Spacing + """Space from the container reserved by docked widgets.""" @property def visible_region(self) -> Region: @@ -484,7 +486,7 @@ class Compositor: # Widgets and regions in render order visible_widgets = [ (order, widget, region, clip) - for widget, (region, order, clip, _, _, _) in map.items() + for widget, (region, order, clip, _, _, _, _) in map.items() if in_screen(region) and overlaps(clip, region) ] visible_widgets.sort(key=itemgetter(0), reverse=True) @@ -522,6 +524,7 @@ class Compositor: layer_order: int, clip: Region, visible: bool, + dock_gutter: Spacing, _MapGeometry: type[MapGeometry] = MapGeometry, ) -> None: """Called recursively to place a widget and its children in the map. @@ -591,10 +594,8 @@ class Compositor: get_layer_index = layers_to_index.get - scroll_spacing = arrange_result.scroll_spacing - # Add all the widgets - for sub_region, margin, sub_widget, z, fixed, overlay in reversed( + for sub_region, _, sub_widget, z, fixed, overlay in reversed( placements ): layer_index = get_layer_index(sub_widget.layer, 0) @@ -602,11 +603,6 @@ class Compositor: if fixed: widget_region = sub_region + placement_offset else: - total_region = total_region.union( - sub_region.grow( - margin if layer_index else margin + scroll_spacing - ) - ) widget_region = sub_region + placement_scroll_offset widget_order = order + ((layer_index, z, layer_order),) @@ -629,6 +625,7 @@ class Compositor: layer_order, no_clip if overlay else sub_clip, visible, + arrange_result.scroll_spacing, ) layer_order -= 1 @@ -646,6 +643,7 @@ class Compositor: container_size, container_size, chrome_region, + dock_gutter, ) map[widget] = _MapGeometry( @@ -655,6 +653,7 @@ class Compositor: total_region.size, container_size, virtual_region, + dock_gutter, ) elif visible: @@ -666,6 +665,7 @@ class Compositor: region.size, container_size, virtual_region, + dock_gutter, ) # Add top level (root) widget @@ -677,6 +677,7 @@ class Compositor: layer_order, size.region, True, + NULL_SPACING, ) return map, widgets diff --git a/src/textual/_layout.py b/src/textual/_layout.py index e0061c148..575dc547f 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -2,17 +2,17 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import TYPE_CHECKING, ClassVar, NamedTuple +from typing import TYPE_CHECKING, ClassVar, Iterable, NamedTuple from ._spatial_map import SpatialMap -from .geometry import Region, Size, Spacing +from .geometry import Offset, Region, Size, Spacing if TYPE_CHECKING: from typing_extensions import TypeAlias from .widget import Widget -ArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget]]" +ArrangeResult: TypeAlias = "list[WidgetPlacement]" @dataclass @@ -51,7 +51,8 @@ class DockArrangeResult: Returns: A Region. """ - return self.spatial_map.total_region + _top, right, bottom, _left = self.scroll_spacing + return self.spatial_map.total_region.grow((0, right, bottom, 0)) def get_visible_placements(self, region: Region) -> list[WidgetPlacement]: """Get the placements visible within the given region. @@ -76,6 +77,41 @@ class WidgetPlacement(NamedTuple): fixed: bool = False overlay: bool = False + @classmethod + def translate( + cls, placements: list[WidgetPlacement], offset: Offset + ) -> list[WidgetPlacement]: + """Move all placements by a given offset. + + Args: + placements: List of placements. + offset: Offset to add to placements. + + Returns: + Placements with adjusted region, or same instance if offset is null. + """ + if offset: + return [ + cls(region + offset, margin, layout_widget, order, fixed, overlay) + for region, margin, layout_widget, order, fixed, overlay in placements + ] + return placements + + @classmethod + def get_bounds(cls, placements: Iterable[WidgetPlacement]) -> Region: + """Get a bounding region around all placements. + + Args: + placements: A number of placements. + + Returns: + An optimal binding box around all placements. + """ + bounding_region = Region.from_union( + [placement.region.grow(placement.margin) for placement in placements] + ) + return bounding_region + class Layout(ABC): """Responsible for arranging Widgets in a view and rendering them.""" diff --git a/src/textual/_spatial_map.py b/src/textual/_spatial_map.py index af38065dd..3f778a06b 100644 --- a/src/textual/_spatial_map.py +++ b/src/textual/_spatial_map.py @@ -72,11 +72,11 @@ class SpatialMap(Generic[ValueType]): _region_to_grid = self._region_to_grid_coordinates total_region = self.total_region for region, fixed, overlay, value in regions_and_values: - if not overlay: - total_region = total_region.union(region) if fixed: append_fixed(value) else: + if not overlay: + total_region = total_region.union(region) for grid in _region_to_grid(region): get_grid_list(grid).append(value) self.total_region = total_region diff --git a/src/textual/_work_decorator.py b/src/textual/_work_decorator.py index 2b47848b9..2688eccc4 100644 --- a/src/textual/_work_decorator.py +++ b/src/textual/_work_decorator.py @@ -87,14 +87,15 @@ def work( self = args[0] assert isinstance(self, DOMNode) - positional_arguments = ", ".join(repr(arg) for arg in args[1:]) - keyword_arguments = ", ".join( - f"{name}={value!r}" for name, value in kwargs.items() - ) - tokens = [positional_arguments, keyword_arguments] - worker_description = ( - f"{method.__name__}({', '.join(token for token in tokens if token)})" - ) + try: + positional_arguments = ", ".join(repr(arg) for arg in args[1:]) + keyword_arguments = ", ".join( + f"{name}={value!r}" for name, value in kwargs.items() + ) + tokens = [positional_arguments, keyword_arguments] + worker_description = f"{method.__name__}({', '.join(token for token in tokens if token)})" + except Exception: + worker_description = "" worker = cast( "Worker[ReturnType]", self.run_worker( diff --git a/src/textual/app.py b/src/textual/app.py index 81ddee40b..5ba8089ac 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -156,7 +156,23 @@ class ScreenError(Exception): class ScreenStackError(ScreenError): - """Raised when attempting to pop the last screen from the stack.""" + """Raised when trying to manipulate the screen stack incorrectly.""" + + +class ModeError(Exception): + """Base class for exceptions related to modes.""" + + +class InvalidModeError(ModeError): + """Raised if there is an issue with a mode name.""" + + +class UnknownModeError(ModeError): + """Raised when attempting to use a mode that is not known.""" + + +class ActiveModeError(ModeError): + """Raised when attempting to remove the currently active mode.""" class ModeError(Exception): @@ -652,14 +668,7 @@ class App(Generic[ReturnType], DOMNode): """ self.set_class(dark, "-dark-mode") self.set_class(not dark, "-light-mode") - try: - self.refresh_css() - except ScreenStackError: - # It's possible that `dark` can be set before we have a default - # screen, in an app's `on_load`, for example. So let's eat the - # ScreenStackError -- the above styles will be handled once the - # screen is spun up anyway. - pass + self.call_later(self.refresh_css) def get_driver_class(self) -> Type[Driver]: """Get a driver class for this platform. @@ -1550,7 +1559,7 @@ class App(Generic[ReturnType], DOMNode): Args: screen: A Screen instance or the name of an installed screen. - callback: An optional callback function that is called if the screen is dismissed with a result. + callback: An optional callback function that will be called if the screen is [dismissed][textual.screen.Screen.dismiss] with a result. Returns: An optional awaitable that awaits the mounting of the screen and its children. @@ -1766,8 +1775,22 @@ class App(Generic[ReturnType], DOMNode): def _print_error_renderables(self) -> None: """Print and clear exit renderables.""" - for renderable in self._exit_renderables: - self.error_console.print(renderable) + error_count = len(self._exit_renderables) + if "debug" in self.features: + for renderable in self._exit_renderables: + self.error_console.print(renderable) + if error_count > 1: + self.error_console.print( + f"\n[b]NOTE:[/b] {error_count} errors show above.", markup=True + ) + elif self._exit_renderables: + self.error_console.print(self._exit_renderables[0]) + if error_count > 1: + self.error_console.print( + f"\n[b]NOTE:[/b] 1 of {error_count} errors show. Run with [b]--dev[/] to see all errors.", + markup=True, + ) + self._exit_renderables.clear() async def _process_messages( @@ -2269,6 +2292,7 @@ class App(Generic[ReturnType], DOMNode): screen = Screen(id=f"_default") self._register(self, screen) self._screen_stacks[self._current_mode].append(screen) + screen.post_message(events.ScreenResume()) await super().on_event(event) elif isinstance(event, events.InputEvent) and not event.is_forwarded: diff --git a/src/textual/dom.py b/src/textual/dom.py index e136a5588..c6efd1c4f 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -887,7 +887,7 @@ class DOMNode(MessagePump): Example: - Here's how you could detect when the app changes from dark to light mode (and visa versa). + Here's how you could detect when the app changes from dark to light mode (and vice versa). ```python def on_dark_change(old_value:bool, new_value:bool): diff --git a/src/textual/geometry.py b/src/textual/geometry.py index 706e264f5..41ca459f6 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -839,7 +839,7 @@ class Region(NamedTuple): @lru_cache(maxsize=1024) def split_horizontal(self, cut: int) -> tuple[Region, Region]: - """Split a region in to two, from a given x offset. + """Split a region in to two, from a given y offset. ``` ┌─────────┐ @@ -852,8 +852,8 @@ class Region(NamedTuple): ``` Args: - cut: An offset from self.x where the cut should be made. May be negative, - for the offset to start from the right edge. + cut: An offset from self.y where the cut should be made. May be negative, + for the offset to start from the lower edge. Returns: Two regions, which add up to the original (self). @@ -907,9 +907,21 @@ class Region(NamedTuple): class Spacing(NamedTuple): - """The spacing around a renderable, such as padding and border + """The spacing around a renderable, such as padding and border. - Spacing is defined by four integers for the space at the top, right, bottom, and left of a region, + Spacing is defined by four integers for the space at the top, right, bottom, and left of a region. + + ``` + ┌ ─ ─ ─ ─ ─ ─ ─▲─ ─ ─ ─ ─ ─ ─ ─ ┐ + │ top + │ ┏━━━━━▼━━━━━━┓ │ + ◀──────▶┃ ┃◀───────▶ + │ left ┃ ┃ right │ + ┃ ┃ + │ ┗━━━━━▲━━━━━━┛ │ + │ bottom + └ ─ ─ ─ ─ ─ ─ ─▼─ ─ ─ ─ ─ ─ ─ ─ ┘ + ``` Example: ```python @@ -928,7 +940,7 @@ class Spacing(NamedTuple): top: int = 0 """Space from the top of a region.""" right: int = 0 - """Space from the left of a region.""" + """Space from the right of a region.""" bottom: int = 0 """Space from the bottom of a region.""" left: int = 0 @@ -1083,3 +1095,9 @@ class Spacing(NamedTuple): NULL_OFFSET: Final = Offset(0, 0) """An [offset][textual.geometry.Offset] constant for (0, 0).""" + +NULL_REGION: Final = Region(0, 0, 0, 0) +"""A [Region][textual.geometry.Region] constant for a null region (at the origin, with both width and height set to zero).""" + +NULL_SPACING: Final = Spacing(0, 0, 0, 0) +"""A [Spacing][textual.geometry.Spacing] constant for no space.""" diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index 3ade70ab2..3b030f30f 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -156,4 +156,4 @@ class GridLayout(Layout): add_placement(WidgetPlacement(region, margin, widget)) add_widget(widget) - return (placements, set(widgets)) + return placements diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index d2fb3146e..a1c4eea4a 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -65,8 +65,6 @@ class HorizontalLayout(Layout): x = Fraction(box_models[0].margin.left if box_models else 0) - displayed_children = [child for child in children if child.display] - _Region = Region _WidgetPlacement = WidgetPlacement for widget, box_model, margin in zip(children, box_models, margins): @@ -86,4 +84,4 @@ class HorizontalLayout(Layout): if not overlay: x = next_x + margin - return placements, set(displayed_children) + return placements diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index 0001efdb6..c74332535 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -86,4 +86,4 @@ class VerticalLayout(Layout): if not overlay: y = next_y + margin - return placements, set(children) + return placements diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 7439919ce..a4dfc8256 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -349,20 +349,25 @@ class MessagePump(metaclass=_MessagePumpMeta): self._timers.add(timer) return timer - def call_after_refresh(self, callback: Callable, *args: Any, **kwargs: Any) -> None: + def call_after_refresh(self, callback: Callable, *args: Any, **kwargs: Any) -> bool: """Schedule a callback to run after all messages are processed and the screen has been refreshed. Positional and keyword arguments are passed to the callable. Args: callback: A callable. + + Returns: + `True` if the callback was scheduled, or `False` if the callback could not be + scheduled (may occur if the message pump was closed or closing). + """ # We send the InvokeLater message to ourselves first, to ensure we've cleared # out anything already pending in our own queue. message = messages.InvokeLater(partial(callback, *args, **kwargs)) - self.post_message(message) + return self.post_message(message) - def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> None: + def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> bool: """Schedule a callback to run after all messages are processed in this object. Positional and keywords arguments are passed to the callable. @@ -370,9 +375,14 @@ class MessagePump(metaclass=_MessagePumpMeta): callback: Callable to call next. *args: Positional arguments to pass to the callable. **kwargs: Keyword arguments to pass to the callable. + + Returns: + `True` if the callback was scheduled, or `False` if the callback could not be + scheduled (may occur if the message pump was closed or closing). + """ message = events.Callback(callback=partial(callback, *args, **kwargs)) - self.post_message(message) + return self.post_message(message) def call_next(self, callback: Callable, *args: Any, **kwargs: Any) -> None: """Schedule a callback to run immediately after processing the current message. diff --git a/src/textual/pilot.py b/src/textual/pilot.py index eaab42334..041e00e13 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -65,6 +65,7 @@ class Pilot(Generic[ReturnType]): """ if keys: await self._app._press_keys(keys) + await self._wait_for_screen() async def click( self, @@ -132,13 +133,49 @@ class Pilot(Generic[ReturnType]): app.post_message(MouseMove(**message_arguments)) await self.pause() + async def _wait_for_screen(self, timeout: float = 30.0) -> bool: + """Wait for the current screen to have processed all pending events. + + Args: + timeout: A timeout in seconds to wait. + + Returns: + `True` if all events were processed, or `False` if the wait timed out. + """ + children = [self.app, *self.app.screen.walk_children(with_self=True)] + count = 0 + count_zero_event = asyncio.Event() + + def decrement_counter() -> None: + """Decrement internal counter, and set an event if it reaches zero.""" + nonlocal count + count -= 1 + if count == 0: + # When count is zero, all messages queued at the start of the method have been processed + count_zero_event.set() + + # Increase the count for every successful call_later + for child in children: + if child.call_later(decrement_counter): + count += 1 + + if count: + # Wait for the count to return to zero, or a timeout + try: + await asyncio.wait_for(count_zero_event.wait(), timeout=timeout) + except asyncio.TimeoutError: + return False + + return True + async def pause(self, delay: float | None = None) -> None: """Insert a pause. Args: delay: Seconds to pause, or None to wait for cpu idle. """ - # These sleep zeros, are to force asyncio to give up a time-slice, + # These sleep zeros, are to force asyncio to give up a time-slice. + await self._wait_for_screen() if delay is None: await wait_for_idle(0) else: @@ -152,7 +189,9 @@ class Pilot(Generic[ReturnType]): async def wait_for_scheduled_animations(self) -> None: """Wait for any current and scheduled animations to complete.""" + await self._wait_for_screen() await self._app.animator.wait_until_complete() + await self._wait_for_screen() await wait_for_idle() self.app.screen._on_timer_update() @@ -162,5 +201,6 @@ class Pilot(Generic[ReturnType]): Args: result: The app result returned by `run` or `run_async`. """ + await self._wait_for_screen() await wait_for_idle() self.app.exit(result) diff --git a/src/textual/screen.py b/src/textual/screen.py index 34db473ef..4d38d25e5 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -9,6 +9,7 @@ from typing import ( TYPE_CHECKING, Awaitable, Callable, + ClassVar, Generic, Iterable, Iterator, @@ -30,7 +31,7 @@ from ._types import CallbackType from .binding import Binding from .css.match import match from .css.parse import parse_selectors -from .css.query import QueryType +from .css.query import NoMatches, QueryType from .dom import DOMNode from .geometry import Offset, Region, Size from .reactive import Reactive @@ -93,6 +94,13 @@ class ResultCallback(Generic[ScreenResultType]): class Screen(Generic[ScreenResultType], Widget): """The base class for screens.""" + AUTO_FOCUS: ClassVar[str | None] = "*" + """A selector to determine what to focus automatically when the screen is activated. + + The widget focused is the first that matches the given [CSS selector](/guide/queries/#query-selectors). + Set to `None` to disable auto focus. + """ + DEFAULT_CSS = """ Screen { layout: vertical; @@ -100,7 +108,6 @@ class Screen(Generic[ScreenResultType], Widget): background: $surface; } """ - focused: Reactive[Widget | None] = Reactive(None) """The focused [widget][textual.widget.Widget] or `None` for no focus.""" stack_updates: Reactive[int] = Reactive(0, repaint=False) @@ -577,6 +584,7 @@ class Screen(Generic[ScreenResultType], Widget): virtual_size, container_size, _, + _, ) in layers: if widget in exposed_widgets: if widget._size_updated( @@ -607,6 +615,7 @@ class Screen(Generic[ScreenResultType], Widget): virtual_size, container_size, _, + _, ) in layers: widget._size_updated(region.size, virtual_size, container_size) if widget in send_resize: @@ -661,6 +670,11 @@ class Screen(Generic[ScreenResultType], Widget): size = self.app.size self._refresh_layout(size, full=True) self.refresh() + if self.AUTO_FOCUS is not None and self.focused is None: + for widget in self.query(self.AUTO_FOCUS): + if widget.focusable: + self.set_focus(widget) + break def _on_screen_suspend(self) -> None: """Screen has suspended.""" @@ -754,16 +768,23 @@ class Screen(Generic[ScreenResultType], Widget): def dismiss(self, result: ScreenResultType | Type[_NoResult] = _NoResult) -> None: """Dismiss the screen, optionally with a result. + If `result` is provided and a callback was set when the screen was [pushed][textual.app.push_screen], then + the callback will be invoked with `result`. + Args: result: The optional result to be passed to the result callback. - Note: - If the screen was pushed with a callback, the callback will be - called with the given result and then a call to - [`App.pop_screen`][textual.app.App.pop_screen] is performed. If - no callback was provided calling this method is the same as - simply calling [`App.pop_screen`][textual.app.App.pop_screen]. + Raises: + ScreenStackError: If trying to dismiss a screen that is not at the top of + the stack. + """ + if self is not self.app.screen: + from .app import ScreenStackError + + raise ScreenStackError( + f"Can't dismiss screen {self} that's not at the top of the stack." + ) if result is not self._NoResult and self._result_callbacks: self._result_callbacks[-1](cast(ScreenResultType, result)) self.app.pop_screen() diff --git a/src/textual/strip.py b/src/textual/strip.py index 554acdecf..c1f9e2587 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -379,16 +379,22 @@ class Strip: """ pos = 0 + cell_length = self.cell_length + cuts = [cut for cut in cuts if cut <= cell_length] cache_key = tuple(cuts) cached = self._divide_cache.get(cache_key) if cached is not None: return cached - strips: list[Strip] = [] - add_strip = strips.append - for segments, cut in zip(Segment.divide(self._segments, cuts), cuts): - add_strip(Strip(segments, cut - pos)) - pos = cut + strips: list[Strip] + if cuts == [cell_length]: + strips = [self] + else: + strips = [] + add_strip = strips.append + for segments, cut in zip(Segment.divide(self._segments, cuts), cuts): + add_strip(Strip(segments, cut - pos)) + pos = cut self._divide_cache[cache_key] = strips return strips diff --git a/src/textual/widget.py b/src/textual/widget.py index 4fc364ebf..6d14cad74 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -57,7 +57,7 @@ from .box_model import BoxModel from .css.query import NoMatches, WrongType from .css.scalar import ScalarOffset from .dom import DOMNode, NoScreen -from .geometry import Offset, Region, Size, Spacing, clamp +from .geometry import NULL_REGION, NULL_SPACING, Offset, Region, Size, Spacing, clamp from .layouts.vertical import VerticalLayout from .message import Message from .messages import CallbackType @@ -795,19 +795,15 @@ class Widget(DOMNode): Args: child: The child widget to move. - before: Optional location to move before. An `int` is the index - of the child to move before, a `str` is a `query_one` query to - find the widget to move before. - after: Optional location to move after. An `int` is the index - of the child to move after, a `str` is a `query_one` query to - find the widget to move after. + before: Child widget or location index to move before. + after: Child widget or location index to move after. Raises: WidgetError: If there is a problem with the child or target. Note: - Only one of ``before`` or ``after`` can be provided. If neither - or both are provided a ``WidgetError`` will be raised. + Only one of `before` or `after` can be provided. If neither + or both are provided a `WidgetError` will be raised. """ # One or the other of before or after are required. Can't do @@ -817,6 +813,10 @@ class Widget(DOMNode): elif before is not None and after is not None: raise WidgetError("Only one of `before` or `after` can be handled.") + # We short-circuit the no-op, otherwise it will error later down the road. + if child is before or child is after: + return + def _to_widget(child: int | Widget, called: str) -> Widget: """Ensure a given child reference is a Widget.""" if isinstance(child, int): @@ -1375,10 +1375,20 @@ class Widget(DOMNode): """ try: return self.screen.find_widget(self).region - except NoScreen: - return Region() - except errors.NoWidget: - return Region() + except (NoScreen, errors.NoWidget): + return NULL_REGION + + @property + def dock_gutter(self) -> Spacing: + """Space allocated to docks in the parent. + + Returns: + Space to be subtracted from scrollable area. + """ + try: + return self.screen.find_widget(self).dock_gutter + except (NoScreen, errors.NoWidget): + return NULL_SPACING @property def container_viewport(self) -> Region: @@ -2263,7 +2273,7 @@ class Widget(DOMNode): else: scroll_offset = container.scroll_to_region( region, - spacing=widget.parent.gutter, + spacing=widget.parent.gutter + widget.dock_gutter, animate=animate, speed=speed, duration=duration, diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index fb6d7c5c5..46001882d 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -1,15 +1,17 @@ from __future__ import annotations +from asyncio import Queue from dataclasses import dataclass from pathlib import Path -from typing import ClassVar, Iterable +from typing import ClassVar, Iterable, Iterator from rich.style import Style from rich.text import Text, TextType -from ..events import Mount +from .. import work from ..message import Message from ..reactive import var +from ..worker import Worker, WorkerCancelled, WorkerFailed, get_current_worker from ._tree import TOGGLE_STYLE, Tree, TreeNode @@ -90,7 +92,7 @@ class DirectoryTree(Tree[DirEntry]): """ return self.tree - path: var[str | Path] = var["str | Path"](Path("."), init=False) + path: var[str | Path] = var["str | Path"](Path("."), init=False, always_update=True) """The path that is the root of the directory tree. Note: @@ -116,6 +118,7 @@ class DirectoryTree(Tree[DirEntry]): classes: A space-separated list of classes, or None for no classes. disabled: Whether the directory tree is disabled or not. """ + self._load_queue: Queue[TreeNode[DirEntry]] = Queue() super().__init__( str(path), data=DirEntry(Path(path)), @@ -126,10 +129,26 @@ class DirectoryTree(Tree[DirEntry]): ) self.path = path + def _add_to_load_queue(self, node: TreeNode[DirEntry]) -> None: + """Add the given node to the load queue. + + Args: + node: The node to add to the load queue. + """ + assert node.data is not None + node.data.loaded = True + self._load_queue.put_nowait(node) + def reload(self) -> None: """Reload the `DirectoryTree` contents.""" self.reset(str(self.path), DirEntry(Path(self.path))) - self._load_directory(self.root) + # Orphan the old queue... + self._load_queue = Queue() + # ...and replace the old load with a new one. + self._loader() + # We have a fresh queue, we have a fresh loader, get the fresh root + # loading up. + self._add_to_load_queue(self.root) def validate_path(self, path: str | Path) -> Path: """Ensure that the path is of the `Path` type. @@ -229,37 +248,115 @@ class DirectoryTree(Tree[DirEntry]): """ return paths - def _load_directory(self, node: TreeNode[DirEntry]) -> None: + @staticmethod + def _safe_is_dir(path: Path) -> bool: + """Safely check if a path is a directory. + + Args: + path: The path to check. + + Returns: + `True` if the path is for a directory, `False` if not. + """ + try: + return path.is_dir() + except PermissionError: + # We may or may not have been looking at a directory, but we + # don't have the rights or permissions to even know that. Best + # we can do, short of letting the error blow up, is assume it's + # not a directory. A possible improvement in here could be to + # have a third state which is "unknown", and reflect that in the + # tree. + return False + + def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> None: + """Populate the given tree node with the given directory content. + + Args: + node: The Tree node to populate. + content: The collection of `Path` objects to populate the node with. + """ + for path in content: + node.add( + path.name, + data=DirEntry(path), + allow_expand=self._safe_is_dir(path), + ) + node.expand() + + def _directory_content(self, location: Path, worker: Worker) -> Iterator[Path]: + """Load the content of a given directory. + + Args: + location: The location to load from. + worker: The worker that the loading is taking place in. + + Yields: + Path: An entry within the location. + """ + try: + for entry in location.iterdir(): + if worker.is_cancelled: + break + yield entry + except PermissionError: + pass + + @work + def _load_directory(self, node: TreeNode[DirEntry]) -> list[Path]: """Load the directory contents for a given node. Args: node: The node to load the directory contents for. + + Returns: + The list of entries within the directory associated with the node. """ assert node.data is not None - node.data.loaded = True - directory = sorted( - self.filter_paths(node.data.path.iterdir()), - key=lambda path: (not path.is_dir(), path.name.lower()), + return sorted( + self.filter_paths( + self._directory_content(node.data.path, get_current_worker()) + ), + key=lambda path: (not self._safe_is_dir(path), path.name.lower()), ) - for path in directory: - node.add( - path.name, - data=DirEntry(path), - allow_expand=path.is_dir(), - ) - node.expand() - def _on_mount(self, _: Mount) -> None: - self._load_directory(self.root) + @work(exclusive=True) + async def _loader(self) -> None: + """Background loading queue processor.""" + worker = get_current_worker() + while not worker.is_cancelled: + # Get the next node that needs loading off the queue. Note that + # this blocks if the queue is empty. + node = await self._load_queue.get() + content: list[Path] = [] + try: + # Spin up a short-lived thread that will load the content of + # the directory associated with that node. + content = await self._load_directory(node).wait() + except WorkerCancelled: + # The worker was cancelled, that would suggest we're all + # done here and we should get out of the loader in general. + break + except WorkerFailed: + # This particular worker failed to start. We don't know the + # reason so let's no-op that (for now anyway). + pass + else: + # We're still here and we have directory content, get it into + # the tree. + if content: + self._populate_node(node, content) + # Mark this iteration as done. + self._load_queue.task_done() def _on_tree_node_expanded(self, event: Tree.NodeExpanded) -> None: event.stop() dir_entry = event.node.data if dir_entry is None: return - if dir_entry.path.is_dir(): + if self._safe_is_dir(dir_entry.path): if not dir_entry.loaded: - self._load_directory(event.node) + self._add_to_load_queue(event.node) else: self.post_message(self.FileSelected(self, event.node, dir_entry.path)) @@ -268,5 +365,5 @@ class DirectoryTree(Tree[DirEntry]): dir_entry = event.node.data if dir_entry is None: return - if not dir_entry.path.is_dir(): + if not self._safe_is_dir(dir_entry.path): self.post_message(self.FileSelected(self, event.node, dir_entry.path)) diff --git a/src/textual/widgets/_footer.py b/src/textual/widgets/_footer.py index c9b74ddc0..a52e05785 100644 --- a/src/textual/widgets/_footer.py +++ b/src/textual/widgets/_footer.py @@ -98,6 +98,7 @@ class Footer(Widget): highlight_style = self.get_component_rich_style("footer--highlight") highlight_key_style = self.get_component_rich_style("footer--highlight-key") key_style = self.get_component_rich_style("footer--key") + description_style = self.get_component_rich_style("footer--description") bindings = [ binding @@ -122,7 +123,7 @@ class Footer(Widget): (f" {key_display} ", highlight_key_style if hovered else key_style), ( f" {binding.description} ", - highlight_style if hovered else base_style, + highlight_style if hovered else base_style + description_style, ), meta={ "@click": f"app.check_bindings('{binding.key}')", diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 9e5bf2d07..e14dcdf10 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -332,8 +332,9 @@ class Input(Widget, can_focus=True): event.prevent_default() def _on_paste(self, event: events.Paste) -> None: - line = event.text.splitlines()[0] - self.insert_text_at_cursor(line) + if event.text: + line = event.text.splitlines()[0] + self.insert_text_at_cursor(line) event.stop() async def _on_click(self, event: events.Click) -> None: diff --git a/src/textual/widgets/_option_list.py b/src/textual/widgets/_option_list.py index 3b9413181..dfe530543 100644 --- a/src/textual/widgets/_option_list.py +++ b/src/textual/widgets/_option_list.py @@ -627,11 +627,17 @@ class OptionList(ScrollView, can_focus=True): """ self._contents.clear() self._options.clear() - self._refresh_content_tracking(force=True) self.highlighted = None self._mouse_hovering_over = None self.virtual_size = Size(self.scrollable_content_region.width, 0) - self.refresh() + # TODO: See https://github.com/Textualize/textual/issues/2582 -- it + # should not be necessary to do this like this here; ideally here in + # clear_options it would be a forced refresh, and also in a + # `on_show` it would be the same (which, I think, would actually + # solve the problem we're seeing). But, until such a time as we get + # to the bottom of 2582... this seems to delay the refresh enough + # that things fall into place. + self._request_content_tracking_refresh() return self def _set_option_disabled(self, index: int, disabled: bool) -> Self: diff --git a/src/textual/widgets/_select.py b/src/textual/widgets/_select.py index 25564af95..dddd9e157 100644 --- a/src/textual/widgets/_select.py +++ b/src/textual/widgets/_select.py @@ -185,10 +185,6 @@ class Select(Generic[SelectType], Vertical, can_focus=True): border: tall $accent; } - Select { - height: auto; - } - Select > SelectOverlay { width: 1fr; display: none; diff --git a/src/textual/widgets/_tabs.py b/src/textual/widgets/_tabs.py index ffeb3fe5b..78c7a6869 100644 --- a/src/textual/widgets/_tabs.py +++ b/src/textual/widgets/_tabs.py @@ -117,15 +117,17 @@ class Tab(Static): label: TextType, *, id: str | None = None, + classes: str | None = None, ) -> None: """Initialise a Tab. Args: label: The label to use in the tab. id: Optional ID for the widget. + classes: Space separated list of class names. """ self.label = Text.from_markup(label) if isinstance(label, str) else label - super().__init__(id=id) + super().__init__(id=id, classes=classes) self.update(label) @property diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index cbee7ba35..c0e2e96b9 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -21,137 +21,138 @@ font-weight: 700; } - .terminal-369237853-matrix { + .terminal-644510384-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-369237853-title { + .terminal-644510384-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-369237853-r1 { fill: #c5c8c6 } - .terminal-369237853-r2 { fill: #7ae998 } - .terminal-369237853-r3 { fill: #0a180e;font-weight: bold } - .terminal-369237853-r4 { fill: #008139 } - .terminal-369237853-r5 { fill: #e1e1e1 } - .terminal-369237853-r6 { fill: #e76580 } - .terminal-369237853-r7 { fill: #f5e5e9;font-weight: bold } - .terminal-369237853-r8 { fill: #780028 } + .terminal-644510384-r1 { fill: #c5c8c6 } + .terminal-644510384-r2 { fill: #7ae998 } + .terminal-644510384-r3 { fill: #4ebf71;font-weight: bold } + .terminal-644510384-r4 { fill: #008139 } + .terminal-644510384-r5 { fill: #e3dbce } + .terminal-644510384-r6 { fill: #e1e1e1 } + .terminal-644510384-r7 { fill: #e76580 } + .terminal-644510384-r8 { fill: #f5e5e9;font-weight: bold } + .terminal-644510384-r9 { fill: #780028 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AlignContainersApp + AlignContainersApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - center - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - middle - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + center + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + middle + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + @@ -346,202 +347,202 @@ font-weight: 700; } - .terminal-2978213952-matrix { + .terminal-3056812568-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2978213952-title { + .terminal-3056812568-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2978213952-r1 { fill: #c5c8c6 } - .terminal-2978213952-r2 { fill: #e3e3e3 } - .terminal-2978213952-r3 { fill: #004578 } - .terminal-2978213952-r4 { fill: #e1e1e1 } - .terminal-2978213952-r5 { fill: #632ca6 } - .terminal-2978213952-r6 { fill: #dde6ed;font-weight: bold } - .terminal-2978213952-r7 { fill: #14191f } - .terminal-2978213952-r8 { fill: #23568b } + .terminal-3056812568-r1 { fill: #c5c8c6 } + .terminal-3056812568-r2 { fill: #e3e3e3 } + .terminal-3056812568-r3 { fill: #004578 } + .terminal-3056812568-r4 { fill: #e1e1e1 } + .terminal-3056812568-r5 { fill: #632ca6 } + .terminal-3056812568-r6 { fill: #dde6ed;font-weight: bold } + .terminal-3056812568-r7 { fill: #14191f } + .terminal-3056812568-r8 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - - - - MyApp - ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── - oktest - ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ -  0 ────────────────────────────────────── 1 ────────────────────────────────────── 2 ───── - -  Foo       Bar         Baz               Foo       Bar         Baz               Foo      -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY▁▁ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY▁▁ ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH -  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH - ───────────────────────────────────────────────────────────────────────────────────────────── - - ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + + MyApp + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + oktest + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ +  0 ────────────────────────────────────── 1 ────────────────────────────────────── 2 ───── + +  Foo       Bar         Baz               Foo       Bar         Baz               Foo      +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY▁▁ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY▁▁ ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH + ───────────────────────────────────────────────────────────────────────────────────────────── + + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── @@ -571,136 +572,136 @@ font-weight: 700; } - .terminal-3956291897-matrix { + .terminal-1625062503-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3956291897-title { + .terminal-1625062503-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3956291897-r1 { fill: #c5c8c6 } - .terminal-3956291897-r2 { fill: #e3e3e3 } - .terminal-3956291897-r3 { fill: #1e1e1e } - .terminal-3956291897-r4 { fill: #0178d4 } - .terminal-3956291897-r5 { fill: #e1e1e1 } - .terminal-3956291897-r6 { fill: #e2e2e2 } - .terminal-3956291897-r7 { fill: #ddedf9 } + .terminal-1625062503-r1 { fill: #c5c8c6 } + .terminal-1625062503-r2 { fill: #e3e3e3 } + .terminal-1625062503-r3 { fill: #1e1e1e } + .terminal-1625062503-r4 { fill: #0178d4 } + .terminal-1625062503-r5 { fill: #e1e1e1 } + .terminal-1625062503-r6 { fill: #e2e2e2 } + .terminal-1625062503-r7 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - InputWidthAutoApp + InputWidthAutoApp - - - - InputWidthAutoApp - ▔▔▔▔▔▔▔▔▔▔ - Hello - ▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - - - + + + + InputWidthAutoApp + ▔▔▔▔▔▔▔▔▔▔ + Hello + ▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + @@ -731,136 +732,137 @@ font-weight: 700; } - .terminal-2059832628-matrix { + .terminal-2470781732-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2059832628-title { + .terminal-2470781732-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2059832628-r1 { fill: #1e1e1e } - .terminal-2059832628-r2 { fill: #c5c8c6 } - .terminal-2059832628-r3 { fill: #183118 } - .terminal-2059832628-r4 { fill: #124512 } - .terminal-2059832628-r5 { fill: #0c580c } - .terminal-2059832628-r6 { fill: #066c06 } - .terminal-2059832628-r7 { fill: #008000 } + .terminal-2470781732-r1 { fill: #1e1e1e } + .terminal-2470781732-r2 { fill: #c5c8c6 } + .terminal-2470781732-r3 { fill: #e1e1e1 } + .terminal-2470781732-r4 { fill: #183118 } + .terminal-2470781732-r5 { fill: #124512 } + .terminal-2470781732-r6 { fill: #0c580c } + .terminal-2470781732-r7 { fill: #066c06 } + .terminal-2470781732-r8 { fill: #008000 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderAlphaApp + BorderAlphaApp - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - - - + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + + @@ -1055,161 +1057,162 @@ font-weight: 700; } - .terminal-3643133712-matrix { + .terminal-619468389-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3643133712-title { + .terminal-619468389-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3643133712-r1 { fill: #c5c8c6 } - .terminal-3643133712-r2 { fill: #e1e1e1;font-weight: bold } - .terminal-3643133712-r3 { fill: #454a50 } - .terminal-3643133712-r4 { fill: #35383c } - .terminal-3643133712-r5 { fill: #24292f;font-weight: bold } - .terminal-3643133712-r6 { fill: #7c7d7e;font-weight: bold } - .terminal-3643133712-r7 { fill: #000000 } - .terminal-3643133712-r8 { fill: #0c0c0c } - .terminal-3643133712-r9 { fill: #507bb3 } - .terminal-3643133712-r10 { fill: #3c5577 } - .terminal-3643133712-r11 { fill: #dde6ed;font-weight: bold } - .terminal-3643133712-r12 { fill: #75828b;font-weight: bold } - .terminal-3643133712-r13 { fill: #001541 } - .terminal-3643133712-r14 { fill: #0c1833 } - .terminal-3643133712-r15 { fill: #7ae998 } - .terminal-3643133712-r16 { fill: #559767 } - .terminal-3643133712-r17 { fill: #0a180e;font-weight: bold } - .terminal-3643133712-r18 { fill: #192e1f;font-weight: bold } - .terminal-3643133712-r19 { fill: #008139 } - .terminal-3643133712-r20 { fill: #0c592e } - .terminal-3643133712-r21 { fill: #ffcf56 } - .terminal-3643133712-r22 { fill: #a5883f } - .terminal-3643133712-r23 { fill: #211505;font-weight: bold } - .terminal-3643133712-r24 { fill: #3a2a13;font-weight: bold } - .terminal-3643133712-r25 { fill: #b86b00 } - .terminal-3643133712-r26 { fill: #7a4c0c } - .terminal-3643133712-r27 { fill: #e76580 } - .terminal-3643133712-r28 { fill: #964858 } - .terminal-3643133712-r29 { fill: #f5e5e9;font-weight: bold } - .terminal-3643133712-r30 { fill: #978186;font-weight: bold } - .terminal-3643133712-r31 { fill: #780028 } - .terminal-3643133712-r32 { fill: #540c24 } + .terminal-619468389-r1 { fill: #e1e1e1 } + .terminal-619468389-r2 { fill: #c5c8c6 } + .terminal-619468389-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-619468389-r4 { fill: #454a50 } + .terminal-619468389-r5 { fill: #35383c } + .terminal-619468389-r6 { fill: #24292f;font-weight: bold } + .terminal-619468389-r7 { fill: #7c7d7e;font-weight: bold } + .terminal-619468389-r8 { fill: #000000 } + .terminal-619468389-r9 { fill: #0c0c0c } + .terminal-619468389-r10 { fill: #507bb3 } + .terminal-619468389-r11 { fill: #3c5577 } + .terminal-619468389-r12 { fill: #dde6ed;font-weight: bold } + .terminal-619468389-r13 { fill: #75828b;font-weight: bold } + .terminal-619468389-r14 { fill: #001541 } + .terminal-619468389-r15 { fill: #0c1833 } + .terminal-619468389-r16 { fill: #7ae998 } + .terminal-619468389-r17 { fill: #559767 } + .terminal-619468389-r18 { fill: #0a180e;font-weight: bold } + .terminal-619468389-r19 { fill: #192e1f;font-weight: bold } + .terminal-619468389-r20 { fill: #008139 } + .terminal-619468389-r21 { fill: #0c592e } + .terminal-619468389-r22 { fill: #ffcf56 } + .terminal-619468389-r23 { fill: #a5883f } + .terminal-619468389-r24 { fill: #211505;font-weight: bold } + .terminal-619468389-r25 { fill: #3a2a13;font-weight: bold } + .terminal-619468389-r26 { fill: #b86b00 } + .terminal-619468389-r27 { fill: #7a4c0c } + .terminal-619468389-r28 { fill: #e76580 } + .terminal-619468389-r29 { fill: #964858 } + .terminal-619468389-r30 { fill: #f5e5e9;font-weight: bold } + .terminal-619468389-r31 { fill: #978186;font-weight: bold } + .terminal-619468389-r32 { fill: #780028 } + .terminal-619468389-r33 { fill: #540c24 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ButtonsApp + ButtonsApp - - - - - Standard ButtonsDisabled Buttons - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - DefaultDefault - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Primary!Primary! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Success!Success! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Warning!Warning! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Error!Error! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + + + + Standard ButtonsDisabled Buttons + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + DefaultDefault + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Primary!Primary! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Success!Success! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Warning!Warning! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Error!Error! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + @@ -1736,139 +1739,140 @@ font-weight: 700; } - .terminal-2222688117-matrix { + .terminal-78223076-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2222688117-title { + .terminal-78223076-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2222688117-r1 { fill: #c5c8c6 } - .terminal-2222688117-r2 { fill: #e1e1e1 } - .terminal-2222688117-r3 { fill: #454a50 } - .terminal-2222688117-r4 { fill: #e2e3e3;font-weight: bold } - .terminal-2222688117-r5 { fill: #000000 } - .terminal-2222688117-r6 { fill: #004578 } - .terminal-2222688117-r7 { fill: #dde6ed;font-weight: bold } - .terminal-2222688117-r8 { fill: #dde6ed } - .terminal-2222688117-r9 { fill: #211505 } - .terminal-2222688117-r10 { fill: #e2e3e3 } + .terminal-78223076-r1 { fill: #c5c8c6 } + .terminal-78223076-r2 { fill: #e1e1e1 } + .terminal-78223076-r3 { fill: #454a50 } + .terminal-78223076-r4 { fill: #24292f;font-weight: bold } + .terminal-78223076-r5 { fill: #e2e3e3;font-weight: bold } + .terminal-78223076-r6 { fill: #000000 } + .terminal-78223076-r7 { fill: #004578 } + .terminal-78223076-r8 { fill: #dde6ed;font-weight: bold } + .terminal-78223076-r9 { fill: #dde6ed } + .terminal-78223076-r10 { fill: #211505 } + .terminal-78223076-r11 { fill: #e2e3e3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ContentSwitcherApp + ContentSwitcherApp - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - DataTableMarkdown - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ──────────────────────────────────────────────────────────────────── -  Book                                 Year  -  Dune                                 1965  -  Dune Messiah                         1969  -  Children of Dune                     1976  -  God Emperor of Dune                  1981  -  Heretics of Dune                     1984  -  Chapterhouse: Dune                   1985  - - - - - - - - - - - ──────────────────────────────────────────────────────────────────── + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + DataTableMarkdown + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ──────────────────────────────────────────────────────────────────── +  Book                                 Year  +  Dune                                 1965  +  Dune Messiah                         1969  +  Children of Dune                     1976  +  God Emperor of Dune                  1981  +  Heretics of Dune                     1984  +  Chapterhouse: Dune                   1985  + + + + + + + + + + + ──────────────────────────────────────────────────────────────────── @@ -2482,134 +2486,135 @@ font-weight: 700; } - .terminal-2323733830-matrix { + .terminal-1331556511-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2323733830-title { + .terminal-1331556511-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2323733830-r1 { fill: #808080 } - .terminal-2323733830-r2 { fill: #e1e1e1 } - .terminal-2323733830-r3 { fill: #c5c8c6 } - .terminal-2323733830-r4 { fill: #ddedf9 } + .terminal-1331556511-r1 { fill: #808080 } + .terminal-1331556511-r2 { fill: #e1e1e1 } + .terminal-1331556511-r3 { fill: #c5c8c6 } + .terminal-1331556511-r4 { fill: #ddedf9 } + .terminal-1331556511-r5 { fill: #e2e2e2 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AlignAllApp + AlignAllApp - - - - ──────────────────────────────────────────────────────────────────────── - left topcenter topright top - - - - - ──────────────────────────────────────────────────────────────────────── - - ──────────────────────────────────────────────────────────────────────── - - - left middlecenter middleright middle - - - ──────────────────────────────────────────────────────────────────────── - - ──────────────────────────────────────────────────────────────────────── - - - - - - left bottomcenter bottomright bottom - ──────────────────────────────────────────────────────────────────────── + + + + ──────────────────────────────────────────────────────────────────────── + left topcenter topright top + + + + + ──────────────────────────────────────────────────────────────────────── + + ──────────────────────────────────────────────────────────────────────── + + + left middlecenter middleright middle + + + ──────────────────────────────────────────────────────────────────────── + + ──────────────────────────────────────────────────────────────────────── + + + + + + left bottomcenter bottomright bottom + ──────────────────────────────────────────────────────────────────────── @@ -3273,141 +3278,141 @@ font-weight: 700; } - .terminal-1536397390-matrix { + .terminal-1997861159-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1536397390-title { + .terminal-1997861159-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1536397390-r1 { fill: #c5c8c6 } - .terminal-1536397390-r2 { fill: #fea62b } - .terminal-1536397390-r3 { fill: #fea62b;font-weight: bold } - .terminal-1536397390-r4 { fill: #fea62b;font-weight: bold;font-style: italic; } - .terminal-1536397390-r5 { fill: #cc555a;font-weight: bold } - .terminal-1536397390-r6 { fill: #e1e1e1 } - .terminal-1536397390-r7 { fill: #1e1e1e } - .terminal-1536397390-r8 { fill: #1e1e1e;text-decoration: underline; } - .terminal-1536397390-r9 { fill: #fea62b;text-decoration: underline; } - .terminal-1536397390-r10 { fill: #4b4e55;text-decoration: underline; } - .terminal-1536397390-r11 { fill: #4ebf71 } - .terminal-1536397390-r12 { fill: #b93c5b } + .terminal-1997861159-r1 { fill: #e1e1e1 } + .terminal-1997861159-r2 { fill: #c5c8c6 } + .terminal-1997861159-r3 { fill: #fea62b } + .terminal-1997861159-r4 { fill: #fea62b;font-weight: bold } + .terminal-1997861159-r5 { fill: #fea62b;font-weight: bold;font-style: italic; } + .terminal-1997861159-r6 { fill: #cc555a;font-weight: bold } + .terminal-1997861159-r7 { fill: #1e1e1e } + .terminal-1997861159-r8 { fill: #1e1e1e;text-decoration: underline; } + .terminal-1997861159-r9 { fill: #fea62b;text-decoration: underline; } + .terminal-1997861159-r10 { fill: #4b4e55;text-decoration: underline; } + .terminal-1997861159-r11 { fill: #4ebf71 } + .terminal-1997861159-r12 { fill: #b93c5b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderSubTitleAlignAll + BorderSubTitleAlignAll - - - - - - Border titleLef…▁▁▁▁Left▁▁▁▁ - This is the story ofa Pythondeveloper that - Border subtitleCen…▔▔▔▔@@@▔▔▔▔▔ - - - - - - +--------------+Title───────────────── - |had to fill up|nine labelsand ended up redoing it - +-Left-------+──────────────Subtitle - - - - - Title, but really looo… - Title, but r…Title, but reall… - because the first tryhad some labelsthat were too long. - Subtitle, bu…Subtitle, but re… - Subtitle, but really l… - + + + + + + Border titleLef…▁▁▁▁Left▁▁▁▁ + This is the story ofa Pythondeveloper that + Border subtitleCen…▔▔▔▔@@@▔▔▔▔▔ + + + + + + +--------------+Title───────────────── + |had to fill up|nine labelsand ended up redoing it + +-Left-------+──────────────Subtitle + + + + + Title, but really looo… + Title, but r…Title, but reall… + because the first tryhad some labelsthat were too long. + Subtitle, bu…Subtitle, but re… + Subtitle, but really l… + @@ -5014,132 +5019,132 @@ font-weight: 700; } - .terminal-1564714526-matrix { + .terminal-1840966081-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1564714526-title { + .terminal-1840966081-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1564714526-r1 { fill: #e1e1e1 } - .terminal-1564714526-r2 { fill: #c5c8c6 } - .terminal-1564714526-r3 { fill: #ffffff } + .terminal-1840966081-r1 { fill: #e1e1e1 } + .terminal-1840966081-r2 { fill: #c5c8c6 } + .terminal-1840966081-r3 { fill: #ffffff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DockAllApp + DockAllApp - - - - - - - ────────────────────────────────────────────────────────── - top - - - - - - - leftright - - - - - - - - bottom - ────────────────────────────────────────────────────────── - - + + + + + + + ────────────────────────────────────────────────────────── + top + + + + + + + leftright + + + + + + + + bottom + ────────────────────────────────────────────────────────── + + @@ -6427,132 +6432,134 @@ font-weight: 700; } - .terminal-2726481143-matrix { + .terminal-2838975926-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2726481143-title { + .terminal-2838975926-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2726481143-r1 { fill: #c5c8c6 } - .terminal-2726481143-r2 { fill: #000000 } - .terminal-2726481143-r3 { fill: #e1e1e1 } + .terminal-2838975926-r1 { fill: #efddef } + .terminal-2838975926-r2 { fill: #c5c8c6 } + .terminal-2838975926-r3 { fill: #000000 } + .terminal-2838975926-r4 { fill: #ddefef } + .terminal-2838975926-r5 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LayoutApp + LayoutApp - - - - - Layout - - Is - - Vertical - - - LayoutIsHorizontal - - - - - - - - - - - - - - + + + + + Layout + + Is + + Vertical + + + LayoutIsHorizontal + + + + + + + + + + + + + + @@ -7839,140 +7846,141 @@ font-weight: 700; } - .terminal-4172255139-matrix { + .terminal-2245771963-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4172255139-title { + .terminal-2245771963-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4172255139-r1 { fill: #ffffff } - .terminal-4172255139-r2 { fill: #c5c8c6 } - .terminal-4172255139-r3 { fill: #ece5e5 } - .terminal-4172255139-r4 { fill: #eee8e3 } - .terminal-4172255139-r5 { fill: #e7e0e6 } - .terminal-4172255139-r6 { fill: #eae2e4 } - .terminal-4172255139-r7 { fill: #e3ede7 } - .terminal-4172255139-r8 { fill: #e8ede4 } - .terminal-4172255139-r9 { fill: #e1eceb } - .terminal-4172255139-r10 { fill: #eeeddf } + .terminal-2245771963-r1 { fill: #ffffff } + .terminal-2245771963-r2 { fill: #c5c8c6 } + .terminal-2245771963-r3 { fill: #e0e0e0 } + .terminal-2245771963-r4 { fill: #ece5e5 } + .terminal-2245771963-r5 { fill: #eee8e3 } + .terminal-2245771963-r6 { fill: #e7e0e6 } + .terminal-2245771963-r7 { fill: #eae2e4 } + .terminal-2245771963-r8 { fill: #e3ede7 } + .terminal-2245771963-r9 { fill: #e8ede4 } + .terminal-2245771963-r10 { fill: #e1eceb } + .terminal-2245771963-r11 { fill: #eeeddf } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MarginAllApp + MarginAllApp - - - - ────────────────────────────────────────────────────────────────── - - - - marginmargin: 1  - no marginmargin: 1: 1 51 2 6 - - - - - ────────────────────────────────────────────────────────────────── - - ────────────────────────────────────────────────────────────────── - - - margin-bottom: 4 - - margin-right: margin-left: 3 - 3 - margin-top: 4 - - - - ────────────────────────────────────────────────────────────────── + + + + ────────────────────────────────────────────────────────────────── + + + + marginmargin: 1  + no marginmargin: 1: 1 51 2 6 + + + + + ────────────────────────────────────────────────────────────────── + + ────────────────────────────────────────────────────────────────── + + + margin-bottom: 4 + + margin-right: margin-left: 3 + 3 + margin-top: 4 + + + + ────────────────────────────────────────────────────────────────── @@ -8160,134 +8168,134 @@ font-weight: 700; } - .terminal-987506037-matrix { + .terminal-1398959741-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-987506037-title { + .terminal-1398959741-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-987506037-r1 { fill: #c5c8c6 } - .terminal-987506037-r2 { fill: #e8e0e7 } - .terminal-987506037-r3 { fill: #eae3e5 } - .terminal-987506037-r4 { fill: #ede6e6 } - .terminal-987506037-r5 { fill: #efe9e4 } + .terminal-1398959741-r1 { fill: #c5c8c6 } + .terminal-1398959741-r2 { fill: #e8e0e7 } + .terminal-1398959741-r3 { fill: #eae3e5 } + .terminal-1398959741-r4 { fill: #ede6e6 } + .terminal-1398959741-r5 { fill: #efe9e4 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MaxWidthApp + MaxWidthApp - - - - - - max-width:  - 50h - - - - - max-width: 999 - - - - - - max-width: 50% - - - - - - max-width: 30 - - + + + + + + max-width:  + 50h + + + + + max-width: 999 + + + + + + max-width: 50% + + + + + + max-width: 30 + + @@ -8637,134 +8645,134 @@ font-weight: 700; } - .terminal-3520697079-matrix { + .terminal-292160688-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3520697079-title { + .terminal-292160688-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3520697079-r1 { fill: #000000 } - .terminal-3520697079-r2 { fill: #0000ff } - .terminal-3520697079-r3 { fill: #c5c8c6 } - .terminal-3520697079-r4 { fill: #ff0000 } - .terminal-3520697079-r5 { fill: #008000 } + .terminal-292160688-r1 { fill: #000000 } + .terminal-292160688-r2 { fill: #0000ff } + .terminal-292160688-r3 { fill: #c5c8c6 } + .terminal-292160688-r4 { fill: #ff0000 } + .terminal-292160688-r5 { fill: #008000 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OffsetApp + OffsetApp - - - - - Chani (offset 0  - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀-3) - - - - Paul (offset 8 2)▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - - - - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - - - Duncan (offset 4  - 10) - - - - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - - - + + + + + Chani (offset 0  + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀-3) + + + + Paul (offset 8 2)▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + + + Duncan (offset 4  + 10) + + + + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + + + @@ -9429,136 +9437,136 @@ font-weight: 700; } - .terminal-3720200886-matrix { + .terminal-2990670852-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3720200886-title { + .terminal-2990670852-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3720200886-r1 { fill: #c5c8c6 } - .terminal-3720200886-r2 { fill: #000000 } - .terminal-3720200886-r3 { fill: #008000 } - .terminal-3720200886-r4 { fill: #e5f0e5 } - .terminal-3720200886-r5 { fill: #036a03 } - .terminal-3720200886-r6 { fill: #14191f } + .terminal-2990670852-r1 { fill: #c5c8c6 } + .terminal-2990670852-r2 { fill: #000000 } + .terminal-2990670852-r3 { fill: #008000 } + .terminal-2990670852-r4 { fill: #e5f0e5 } + .terminal-2990670852-r5 { fill: #036a03 } + .terminal-2990670852-r6 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OverflowApp + OverflowApp - - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - I must not fear.I must not fear. - Fear is the mind-killer.Fear is the mind-killer. - Fear is the little-death that Fear is the little-death that  - brings total obliteration.brings total obliteration. - I will face my fear.I will face my fear. - I will permit it to pass over meI will permit it to pass over me  - and through me.and through me. - And when it has gone past, I And when it has gone past, I will  - will turn the inner eye to see turn the inner eye to see its  - its path.▁▁path. - Where the fear has gone there Where the fear has gone there will - will be nothing. Only I will be nothing. Only I will remain. - remain.▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I must not fear. - I must not fear.Fear is the mind-killer. - Fear is the mind-killer.Fear is the little-death that  - Fear is the little-death that brings total obliteration. - brings total obliteration.I will face my fear. - I will face my fear.I will permit it to pass over me  - I will permit it to pass over meand through me. + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death that Fear is the little-death that  + brings total obliteration.brings total obliteration. + I will face my fear.I will face my fear. + I will permit it to pass over meI will permit it to pass over me  + and through me.and through me. + And when it has gone past, I And when it has gone past, I will  + will turn the inner eye to see turn the inner eye to see its  + its path.▁▁path. + Where the fear has gone there Where the fear has gone there will + will be nothing. Only I will be nothing. Only I will remain. + remain.▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I must not fear. + I must not fear.Fear is the mind-killer. + Fear is the mind-killer.Fear is the little-death that  + Fear is the little-death that brings total obliteration. + brings total obliteration.I will face my fear. + I will face my fear.I will permit it to pass over me  + I will permit it to pass over meand through me. @@ -9743,138 +9751,138 @@ font-weight: 700; } - .terminal-2103878337-matrix { + .terminal-1642992271-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2103878337-title { + .terminal-1642992271-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2103878337-r1 { fill: #e7e0e6 } - .terminal-2103878337-r2 { fill: #c5c8c6 } - .terminal-2103878337-r3 { fill: #eae2e4 } - .terminal-2103878337-r4 { fill: #ece5e5 } - .terminal-2103878337-r5 { fill: #eee8e3 } - .terminal-2103878337-r6 { fill: #e8ede4 } - .terminal-2103878337-r7 { fill: #e3ede7 } - .terminal-2103878337-r8 { fill: #e1eceb } - .terminal-2103878337-r9 { fill: #eeeddf } + .terminal-1642992271-r1 { fill: #c5c8c6 } + .terminal-1642992271-r2 { fill: #e7e0e6 } + .terminal-1642992271-r3 { fill: #eae2e4 } + .terminal-1642992271-r4 { fill: #ece5e5 } + .terminal-1642992271-r5 { fill: #eee8e3 } + .terminal-1642992271-r6 { fill: #e8ede4 } + .terminal-1642992271-r7 { fill: #e3ede7 } + .terminal-1642992271-r8 { fill: #e1eceb } + .terminal-1642992271-r9 { fill: #eeeddf } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - PaddingAllApp + PaddingAllApp - - - - no padding - padding: 1padding:padding: 1 1 - 1 52 6 - - - - - - - - - - padding-right: 3padding-bottom: 4padding-left: 3 - - - - padding-top: 4 - - - - - - + + + + no padding + padding: 1padding:padding: 1 1 + 1 52 6 + + + + + + + + + + padding-right: 3padding-bottom: 4padding-left: 3 + + + + padding-top: 4 + + + + + + @@ -12284,141 +12292,141 @@ font-weight: 700; } - .terminal-1052270191-matrix { + .terminal-1938916138-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1052270191-title { + .terminal-1938916138-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1052270191-r1 { fill: #c5c8c6 } - .terminal-1052270191-r2 { fill: #e8e0e7 } - .terminal-1052270191-r3 { fill: #eae3e5 } - .terminal-1052270191-r4 { fill: #ede6e6 } - .terminal-1052270191-r5 { fill: #efe9e4 } - .terminal-1052270191-r6 { fill: #efeedf } - .terminal-1052270191-r7 { fill: #e9eee5 } - .terminal-1052270191-r8 { fill: #e4eee8 } - .terminal-1052270191-r9 { fill: #e2edeb } - .terminal-1052270191-r10 { fill: #dfebed } - .terminal-1052270191-r11 { fill: #ddedf9 } + .terminal-1938916138-r1 { fill: #c5c8c6 } + .terminal-1938916138-r2 { fill: #e8e0e7 } + .terminal-1938916138-r3 { fill: #eae3e5 } + .terminal-1938916138-r4 { fill: #ede6e6 } + .terminal-1938916138-r5 { fill: #efe9e4 } + .terminal-1938916138-r6 { fill: #efeedf } + .terminal-1938916138-r7 { fill: #e9eee5 } + .terminal-1938916138-r8 { fill: #e4eee8 } + .terminal-1938916138-r9 { fill: #e2edeb } + .terminal-1938916138-r10 { fill: #dfebed } + .terminal-1938916138-r11 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HeightComparisonApp + HeightComparisonApp - - - - - - - - - - - - - - - #cells#percent#w#h#vw#vh#auto#fr1#fr3 - - - - - - - - - - - - ····•····•····•····•····•····•····•····•····•····•····•····•····•····•····•····• + + + + + + + + + + + + + + + #cells#percent#w#h#vw#vh#auto#fr1#fr3 + + + + + + + + + + + + ····•····•····•····•····•····•····•····•····•····•····•····•····•····•····•····• @@ -13562,168 +13570,168 @@ font-weight: 700; } - .terminal-614458704-matrix { + .terminal-4033540874-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-614458704-title { + .terminal-4033540874-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-614458704-r1 { fill: #c5c8c6 } - .terminal-614458704-r2 { fill: #e3e3e3 } - .terminal-614458704-r3 { fill: #e1e1e1 } - .terminal-614458704-r4 { fill: #e2e2e2 } - .terminal-614458704-r5 { fill: #14191f } - .terminal-614458704-r6 { fill: #004578 } - .terminal-614458704-r7 { fill: #262626 } - .terminal-614458704-r8 { fill: #e2e2e2;font-weight: bold;text-decoration: underline; } - .terminal-614458704-r9 { fill: #e2e2e2;font-weight: bold } - .terminal-614458704-r10 { fill: #7ae998 } - .terminal-614458704-r11 { fill: #4ebf71;font-weight: bold } - .terminal-614458704-r12 { fill: #008139 } - .terminal-614458704-r13 { fill: #dde8f3;font-weight: bold } - .terminal-614458704-r14 { fill: #ddedf9 } + .terminal-4033540874-r1 { fill: #c5c8c6 } + .terminal-4033540874-r2 { fill: #e3e3e3 } + .terminal-4033540874-r3 { fill: #e1e1e1 } + .terminal-4033540874-r4 { fill: #e2e2e2 } + .terminal-4033540874-r5 { fill: #14191f } + .terminal-4033540874-r6 { fill: #004578 } + .terminal-4033540874-r7 { fill: #262626 } + .terminal-4033540874-r8 { fill: #e2e2e2;font-weight: bold;text-decoration: underline; } + .terminal-4033540874-r9 { fill: #e2e2e2;font-weight: bold } + .terminal-4033540874-r10 { fill: #7ae998 } + .terminal-4033540874-r11 { fill: #4ebf71;font-weight: bold } + .terminal-4033540874-r12 { fill: #008139 } + .terminal-4033540874-r13 { fill: #dde8f3;font-weight: bold } + .terminal-4033540874-r14 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Textual Demo + Textual Demo - - - - Textual Demo - - - TOP - - ▆▆ - - Widgets - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - Rich contentTextual Demo - - Welcome! Textual is a framework for creating sophisticated - applications with the terminal. - CSS - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Start - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - - - - - - -  CTRL+C  Quit  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  + + + + Textual Demo + + + TOP + + ▆▆ + + Widgets + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + Rich contentTextual Demo + + Welcome! Textual is a framework for creating sophisticated + applications with the terminal. + CSS + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Start + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + +  CTRL+C  Quit  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  @@ -13753,161 +13761,162 @@ font-weight: 700; } - .terminal-65653754-matrix { + .terminal-1022810985-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-65653754-title { + .terminal-1022810985-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-65653754-r1 { fill: #454a50 } - .terminal-65653754-r2 { fill: #507bb3 } - .terminal-65653754-r3 { fill: #7ae998 } - .terminal-65653754-r4 { fill: #ffcf56 } - .terminal-65653754-r5 { fill: #e76580 } - .terminal-65653754-r6 { fill: #c5c8c6 } - .terminal-65653754-r7 { fill: #e2e3e3;font-weight: bold } - .terminal-65653754-r8 { fill: #dde6ed;font-weight: bold } - .terminal-65653754-r9 { fill: #0a180e;font-weight: bold } - .terminal-65653754-r10 { fill: #211505;font-weight: bold } - .terminal-65653754-r11 { fill: #f5e5e9;font-weight: bold } - .terminal-65653754-r12 { fill: #000000 } - .terminal-65653754-r13 { fill: #001541 } - .terminal-65653754-r14 { fill: #008139 } - .terminal-65653754-r15 { fill: #b86b00 } - .terminal-65653754-r16 { fill: #780028 } - .terminal-65653754-r17 { fill: #35383c } - .terminal-65653754-r18 { fill: #3c5577 } - .terminal-65653754-r19 { fill: #559767 } - .terminal-65653754-r20 { fill: #a5883f } - .terminal-65653754-r21 { fill: #964858 } - .terminal-65653754-r22 { fill: #7c7d7e;font-weight: bold } - .terminal-65653754-r23 { fill: #75828b;font-weight: bold } - .terminal-65653754-r24 { fill: #192e1f;font-weight: bold } - .terminal-65653754-r25 { fill: #3a2a13;font-weight: bold } - .terminal-65653754-r26 { fill: #978186;font-weight: bold } - .terminal-65653754-r27 { fill: #0c0c0c } - .terminal-65653754-r28 { fill: #0c1833 } - .terminal-65653754-r29 { fill: #0c592e } - .terminal-65653754-r30 { fill: #7a4c0c } - .terminal-65653754-r31 { fill: #540c24 } + .terminal-1022810985-r1 { fill: #454a50 } + .terminal-1022810985-r2 { fill: #507bb3 } + .terminal-1022810985-r3 { fill: #7ae998 } + .terminal-1022810985-r4 { fill: #ffcf56 } + .terminal-1022810985-r5 { fill: #e76580 } + .terminal-1022810985-r6 { fill: #c5c8c6 } + .terminal-1022810985-r7 { fill: #24292f;font-weight: bold } + .terminal-1022810985-r8 { fill: #dde6ed;font-weight: bold } + .terminal-1022810985-r9 { fill: #0a180e;font-weight: bold } + .terminal-1022810985-r10 { fill: #211505;font-weight: bold } + .terminal-1022810985-r11 { fill: #f5e5e9;font-weight: bold } + .terminal-1022810985-r12 { fill: #000000 } + .terminal-1022810985-r13 { fill: #001541 } + .terminal-1022810985-r14 { fill: #008139 } + .terminal-1022810985-r15 { fill: #b86b00 } + .terminal-1022810985-r16 { fill: #780028 } + .terminal-1022810985-r17 { fill: #35383c } + .terminal-1022810985-r18 { fill: #3c5577 } + .terminal-1022810985-r19 { fill: #559767 } + .terminal-1022810985-r20 { fill: #a5883f } + .terminal-1022810985-r21 { fill: #964858 } + .terminal-1022810985-r22 { fill: #7c7d7e;font-weight: bold } + .terminal-1022810985-r23 { fill: #75828b;font-weight: bold } + .terminal-1022810985-r24 { fill: #192e1f;font-weight: bold } + .terminal-1022810985-r25 { fill: #3a2a13;font-weight: bold } + .terminal-1022810985-r26 { fill: #978186;font-weight: bold } + .terminal-1022810985-r27 { fill: #0c0c0c } + .terminal-1022810985-r28 { fill: #0c1833 } + .terminal-1022810985-r29 { fill: #0c592e } + .terminal-1022810985-r30 { fill: #7a4c0c } + .terminal-1022810985-r31 { fill: #540c24 } + .terminal-1022810985-r32 { fill: #e2e3e3;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WidgetDisableTestApp + WidgetDisableTestApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -14094,141 +14103,468 @@ font-weight: 700; } - .terminal-2216843056-matrix { + .terminal-2702154472-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2216843056-title { + .terminal-2702154472-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2216843056-r1 { fill: #c5c8c6 } - .terminal-2216843056-r2 { fill: #1e1e1e } - .terminal-2216843056-r3 { fill: #1f1f1f } - .terminal-2216843056-r4 { fill: #ff0000 } - .terminal-2216843056-r5 { fill: #dde8f3;font-weight: bold } - .terminal-2216843056-r6 { fill: #ddedf9 } - .terminal-2216843056-r7 { fill: #c7cdd2 } + .terminal-2702154472-r1 { fill: #c5c8c6 } + .terminal-2702154472-r2 { fill: #1e1e1e } + .terminal-2702154472-r3 { fill: #1f1f1f } + .terminal-2702154472-r4 { fill: #ff0000 } + .terminal-2702154472-r5 { fill: #dde8f3;font-weight: bold } + .terminal-2702154472-r6 { fill: #ddedf9 } + .terminal-2702154472-r7 { fill: #c7cdd2 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TestApp + TestApp - - - - TestApp - ───────── - this - is - a - sample - sentence - and - here - are - some - wordsthis - is - a - sample - sentence - and - here - are - some - words -  CTRL+Q  Quit  - - - ▇▇ + + + + TestApp + ───────── + this + is + a + sample + sentence + and + here + are + some + wordsthis + is + a + sample + sentence + and + here + are + some + words +  CTRL+Q  Quit  + + + ▇▇ + + + + + ''' +# --- +# name: test_dock_scroll2 + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestApp + + + + + + + + + + TestApp + ───────── + this + is + a + sample + sentence + and + here + are + some + wordsthis + is + a▅▅ + sample + sentence + and + here + are + some + words +  CTRL+Q  Quit  + + + + + + + + ''' +# --- +# name: test_dock_scroll_off_by_one + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollOffByOne + + + + + + + + + + X76 + X77 + X78 + X79 + X80 + X81 + X82 + X83 + X84 + X85 + X86 + X87 + X88 + X89 + X90 + X91 + X92 + X93 + X94▂▂ + X95 + X96 + X97 + X98 + X99 + @@ -14258,147 +14594,148 @@ font-weight: 700; } - .terminal-2863933047-matrix { + .terminal-432027110-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2863933047-title { + .terminal-432027110-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2863933047-r1 { fill: #454a50 } - .terminal-2863933047-r2 { fill: #e1e1e1 } - .terminal-2863933047-r3 { fill: #c5c8c6 } - .terminal-2863933047-r4 { fill: #e2e3e3;font-weight: bold } - .terminal-2863933047-r5 { fill: #262626 } - .terminal-2863933047-r6 { fill: #000000 } - .terminal-2863933047-r7 { fill: #e2e2e2 } - .terminal-2863933047-r8 { fill: #e3e3e3 } - .terminal-2863933047-r9 { fill: #14191f } - .terminal-2863933047-r10 { fill: #b93c5b } - .terminal-2863933047-r11 { fill: #121212 } - .terminal-2863933047-r12 { fill: #1e1e1e } - .terminal-2863933047-r13 { fill: #fea62b } - .terminal-2863933047-r14 { fill: #211505;font-weight: bold } - .terminal-2863933047-r15 { fill: #211505 } - .terminal-2863933047-r16 { fill: #dde8f3;font-weight: bold } - .terminal-2863933047-r17 { fill: #ddedf9 } + .terminal-432027110-r1 { fill: #454a50 } + .terminal-432027110-r2 { fill: #e1e1e1 } + .terminal-432027110-r3 { fill: #c5c8c6 } + .terminal-432027110-r4 { fill: #24292f;font-weight: bold } + .terminal-432027110-r5 { fill: #262626 } + .terminal-432027110-r6 { fill: #000000 } + .terminal-432027110-r7 { fill: #e2e2e2 } + .terminal-432027110-r8 { fill: #e3e3e3 } + .terminal-432027110-r9 { fill: #e2e3e3;font-weight: bold } + .terminal-432027110-r10 { fill: #14191f } + .terminal-432027110-r11 { fill: #b93c5b } + .terminal-432027110-r12 { fill: #121212 } + .terminal-432027110-r13 { fill: #1e1e1e } + .terminal-432027110-r14 { fill: #fea62b } + .terminal-432027110-r15 { fill: #211505;font-weight: bold } + .terminal-432027110-r16 { fill: #211505 } + .terminal-432027110-r17 { fill: #dde8f3;font-weight: bold } + .terminal-432027110-r18 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - EasingApp + EasingApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - round▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0 - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - out_sine - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - out_quint - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - out_quartI must not fear. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. - out_quadFear is the  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  - out_expoobliteration. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  - out_elasticpass over me and  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  - out_cubic - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ CTRL+P  Focus: Duration Input  CTRL+B  Toggle Dark  + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + round▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0 + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + out_sine + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + out_quint + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + out_quartI must not fear. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. + out_quadFear is the  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  + out_expoobliteration. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  + out_elasticpass over me and  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  + out_cubic + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ CTRL+P  Focus: Duration Input  CTRL+B  Toggle Dark  @@ -14428,135 +14765,135 @@ font-weight: 700; } - .terminal-2886576672-matrix { + .terminal-1801121102-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2886576672-title { + .terminal-1801121102-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2886576672-r1 { fill: #c5c8c6 } - .terminal-2886576672-r2 { fill: #e3e3e3 } - .terminal-2886576672-r3 { fill: #ffdddd } - .terminal-2886576672-r4 { fill: #e1e1e1 } - .terminal-2886576672-r5 { fill: #14191f } - .terminal-2886576672-r6 { fill: #ddedf9 } + .terminal-1801121102-r1 { fill: #c5c8c6 } + .terminal-1801121102-r2 { fill: #e3e3e3 } + .terminal-1801121102-r3 { fill: #ffdddd } + .terminal-1801121102-r4 { fill: #e1e1e1 } + .terminal-1801121102-r5 { fill: #14191f } + .terminal-1801121102-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - StyleBugApp + StyleBugApp - - - - StyleBugApp - test widget 0 - test widget 1 - test widget 2 - test widget 3 - test widget 4 - test widget 5 - test widget 6 - test widget 7 - test widget 8 - test widget 9 - test widget 10 - test widget 11 - test widget 12▇▇ - test widget 13 - test widget 14 - test widget 15 - test widget 16 - test widget 17 - test widget 18 - test widget 19 - test widget 20 - test widget 21 + + + + StyleBugApp + test widget 0 + test widget 1 + test widget 2 + test widget 3 + test widget 4 + test widget 5 + test widget 6 + test widget 7 + test widget 8 + test widget 9 + test widget 10 + test widget 11 + test widget 12▇▇ + test widget 13 + test widget 14 + test widget 15 + test widget 16 + test widget 17 + test widget 18 + test widget 19 + test widget 20 + test widget 21 @@ -14744,137 +15081,138 @@ font-weight: 700; } - .terminal-1298369243-matrix { + .terminal-1665781252-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1298369243-title { + .terminal-1665781252-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1298369243-r1 { fill: #008000 } - .terminal-1298369243-r2 { fill: #c5c8c6 } - .terminal-1298369243-r3 { fill: #e0e6e0 } + .terminal-1665781252-r1 { fill: #008000 } + .terminal-1665781252-r2 { fill: #c5c8c6 } + .terminal-1665781252-r3 { fill: #e0e4e0 } + .terminal-1665781252-r4 { fill: #e0e6e0 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TestApp + TestApp - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - Hello - - - - - - - World - - - - - - - !! - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + Hello + + + + + + + World + + + + + + + !! + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -14904,135 +15242,136 @@ font-weight: 700; } - .terminal-2371169958-matrix { + .terminal-1035580841-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2371169958-title { + .terminal-1035580841-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2371169958-r1 { fill: #c5c8c6 } - .terminal-2371169958-r2 { fill: #e3e3e3 } - .terminal-2371169958-r3 { fill: #e3e4e5 } - .terminal-2371169958-r4 { fill: #e2e3e3 } - .terminal-2371169958-r5 { fill: #14191f } - .terminal-2371169958-r6 { fill: #ddedf9 } + .terminal-1035580841-r1 { fill: #c5c8c6 } + .terminal-1035580841-r2 { fill: #e3e3e3 } + .terminal-1035580841-r3 { fill: #ddddff } + .terminal-1035580841-r4 { fill: #e3e4e5 } + .terminal-1035580841-r5 { fill: #e2e3e3 } + .terminal-1035580841-r6 { fill: #14191f } + .terminal-1035580841-r7 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScreenSplitApp + ScreenSplitApp - - - - ScreenSplitApp - This is content This is content number 0 - number 0This is content number 1 - This is content ▄▄This is content number 2 - number 1This is content number 3 - This is content This is content number 4▁▁ - number 2This is content number 5 - This is content This is content number 6 - number 3This is content number 7 - This is content This is content number 8 - number 4This is content number 9 - This is content This is content number 10 - number 5This is content number 11 - This is content This is content number 12 - number 6This is content number 13 - This is content This is content number 14 - number 7This is content number 15 - This is content This is content number 16 - number 8This is content number 17 - This is content This is content number 18 - number 9This is content number 19 - This is content This is content number 20 - number 10This is content number 21 + + + + ScreenSplitApp + This is content This is content number 0 + number 0This is content number 1 + This is content ▄▄This is content number 2 + number 1This is content number 3 + This is content This is content number 4▁▁ + number 2This is content number 5 + This is content This is content number 6 + number 3This is content number 7 + This is content This is content number 8 + number 4This is content number 9 + This is content This is content number 10 + number 5This is content number 11 + This is content This is content number 12 + number 6This is content number 13 + This is content This is content number 14 + number 7This is content number 15 + This is content This is content number 16 + number 8This is content number 17 + This is content This is content number 18 + number 9This is content number 19 + This is content This is content number 20 + number 10This is content number 21 @@ -15687,132 +16026,132 @@ font-weight: 700; } - .terminal-2648118808-matrix { + .terminal-4077214022-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2648118808-title { + .terminal-4077214022-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2648118808-r1 { fill: #c5c8c6 } - .terminal-2648118808-r2 { fill: #e3e3e3 } - .terminal-2648118808-r3 { fill: #e1e1e1 } + .terminal-4077214022-r1 { fill: #c5c8c6 } + .terminal-4077214022-r2 { fill: #e3e3e3 } + .terminal-4077214022-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HeaderApp + HeaderApp - - - - HeaderApp - - - - - - - - - - - - - - - - - - - - - - + + + + HeaderApp + + + + + + + + + + + + + + + + + + + + + + @@ -16473,146 +16812,146 @@ font-weight: 700; } - .terminal-641812469-matrix { + .terminal-4085160594-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-641812469-title { + .terminal-4085160594-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-641812469-r1 { fill: #c5c8c6 } - .terminal-641812469-r2 { fill: #e3e3e3 } - .terminal-641812469-r3 { fill: #e1e1e1 } - .terminal-641812469-r4 { fill: #e1e1e1;text-decoration: underline; } - .terminal-641812469-r5 { fill: #e1e1e1;font-weight: bold } - .terminal-641812469-r6 { fill: #e1e1e1;font-style: italic; } - .terminal-641812469-r7 { fill: #98729f;font-weight: bold } - .terminal-641812469-r8 { fill: #d0b344 } - .terminal-641812469-r9 { fill: #98a84b } - .terminal-641812469-r10 { fill: #00823d;font-style: italic; } - .terminal-641812469-r11 { fill: #ffcf56 } - .terminal-641812469-r12 { fill: #e76580 } - .terminal-641812469-r13 { fill: #211505;font-weight: bold } - .terminal-641812469-r14 { fill: #f5e5e9;font-weight: bold } - .terminal-641812469-r15 { fill: #b86b00 } - .terminal-641812469-r16 { fill: #780028 } + .terminal-4085160594-r1 { fill: #c5c8c6 } + .terminal-4085160594-r2 { fill: #e3e3e3 } + .terminal-4085160594-r3 { fill: #e1e1e1 } + .terminal-4085160594-r4 { fill: #e1e1e1;text-decoration: underline; } + .terminal-4085160594-r5 { fill: #e1e1e1;font-weight: bold } + .terminal-4085160594-r6 { fill: #e1e1e1;font-style: italic; } + .terminal-4085160594-r7 { fill: #98729f;font-weight: bold } + .terminal-4085160594-r8 { fill: #d0b344 } + .terminal-4085160594-r9 { fill: #98a84b } + .terminal-4085160594-r10 { fill: #00823d;font-style: italic; } + .terminal-4085160594-r11 { fill: #ffcf56 } + .terminal-4085160594-r12 { fill: #e76580 } + .terminal-4085160594-r13 { fill: #fea62b;font-weight: bold } + .terminal-4085160594-r14 { fill: #f5e5e9;font-weight: bold } + .terminal-4085160594-r15 { fill: #b86b00 } + .terminal-4085160594-r16 { fill: #780028 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Textual Keys + Textual Keys - - - - Textual Keys - ╭────────────────────────────────────────────────────────────────────────────╮ - Press some keys! - - To quit the app press ctrl+ctwice or press the Quit button below. - ╰────────────────────────────────────────────────────────────────────────────╯ - Key(key='a'character='a'name='a'is_printable=True) - Key(key='b'character='b'name='b'is_printable=True) - - - - - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ClearQuit - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + Textual Keys + ╭────────────────────────────────────────────────────────────────────────────╮ + Press some keys! + + To quit the app press ctrl+ctwice or press the Quit button below. + ╰────────────────────────────────────────────────────────────────────────────╯ + Key(key='a'character='a'name='a'is_printable=True) + Key(key='b'character='b'name='b'is_printable=True) + + + + + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ClearQuit + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -16800,136 +17139,136 @@ font-weight: 700; } - .terminal-513592180-matrix { + .terminal-1675990519-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-513592180-title { + .terminal-1675990519-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-513592180-r1 { fill: #c5c8c6 } - .terminal-513592180-r2 { fill: #e3e3e3 } - .terminal-513592180-r3 { fill: #ff0000 } - .terminal-513592180-r4 { fill: #e1e1e1 } - .terminal-513592180-r5 { fill: #dde8f3;font-weight: bold } - .terminal-513592180-r6 { fill: #ddedf9 } + .terminal-1675990519-r1 { fill: #c5c8c6 } + .terminal-1675990519-r2 { fill: #e3e3e3 } + .terminal-1675990519-r3 { fill: #e1e1e1 } + .terminal-1675990519-r4 { fill: #ff0000 } + .terminal-1675990519-r5 { fill: #dde8f3;font-weight: bold } + .terminal-1675990519-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DialogIssueApp + DialogIssueApp - - - - DialogIssueApp - - - - - - ─────────────────────────────────────── - - - - - - This should not cause a scrollbar to ap - - - - - - ─────────────────────────────────────── - - - - -  D  Toggle the dialog  + + + + DialogIssueApp + + + + + + ─────────────────────────────────────── + + + + + + This should not cause a scrollbar to ap + + + + + + ─────────────────────────────────────── + + + + +  D  Toggle the dialog  @@ -17116,143 +17455,144 @@ font-weight: 700; } - .terminal-4197777529-matrix { + .terminal-4055437288-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4197777529-title { + .terminal-4055437288-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4197777529-r1 { fill: #7ae998 } - .terminal-4197777529-r2 { fill: #e76580 } - .terminal-4197777529-r3 { fill: #1e1e1e } - .terminal-4197777529-r4 { fill: #121212 } - .terminal-4197777529-r5 { fill: #c5c8c6 } - .terminal-4197777529-r6 { fill: #0a180e;font-weight: bold } - .terminal-4197777529-r7 { fill: #f5e5e9;font-weight: bold } - .terminal-4197777529-r8 { fill: #e2e2e2 } - .terminal-4197777529-r9 { fill: #008139 } - .terminal-4197777529-r10 { fill: #780028 } - .terminal-4197777529-r11 { fill: #e1e1e1 } - .terminal-4197777529-r12 { fill: #23568b } - .terminal-4197777529-r13 { fill: #14191f } + .terminal-4055437288-r1 { fill: #7ae998 } + .terminal-4055437288-r2 { fill: #e76580 } + .terminal-4055437288-r3 { fill: #1e1e1e } + .terminal-4055437288-r4 { fill: #121212 } + .terminal-4055437288-r5 { fill: #c5c8c6 } + .terminal-4055437288-r6 { fill: #4ebf71;font-weight: bold } + .terminal-4055437288-r7 { fill: #f5e5e9;font-weight: bold } + .terminal-4055437288-r8 { fill: #e2e2e2 } + .terminal-4055437288-r9 { fill: #0a180e;font-weight: bold } + .terminal-4055437288-r10 { fill: #008139 } + .terminal-4055437288-r11 { fill: #780028 } + .terminal-4055437288-r12 { fill: #e1e1e1 } + .terminal-4055437288-r13 { fill: #23568b } + .terminal-4055437288-r14 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - AcceptDeclineAcceptDecline - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - AcceptAccept - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - DeclineDecline - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - 00 - - 10000001000000 + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + AcceptDeclineAcceptDecline + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + AcceptAccept + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + DeclineDecline + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + 00 + + 10000001000000 @@ -17927,135 +18267,135 @@ font-weight: 700; } - .terminal-2423395429-matrix { + .terminal-543315859-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2423395429-title { + .terminal-543315859-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2423395429-r1 { fill: #c5c8c6 } - .terminal-2423395429-r2 { fill: #e3e3e3 } - .terminal-2423395429-r3 { fill: #e1e1e1 } - .terminal-2423395429-r4 { fill: #dde8f3;font-weight: bold } - .terminal-2423395429-r5 { fill: #ddedf9 } + .terminal-543315859-r1 { fill: #c5c8c6 } + .terminal-543315859-r2 { fill: #e3e3e3 } + .terminal-543315859-r3 { fill: #e1e1e1 } + .terminal-543315859-r4 { fill: #dde8f3;font-weight: bold } + .terminal-543315859-r5 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ModalApp + ModalApp - - - - ModalApp - Hello - - - - - - - - - - - - - - - - - - - - - -  ⏎  Open Dialog  + + + + ModalApp + Hello + + + + + + + + + + + + + + + + + + + + + +  ⏎  Open Dialog  @@ -18722,136 +19062,136 @@ font-weight: 700; } - .terminal-1829927563-matrix { + .terminal-4119903855-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1829927563-title { + .terminal-4119903855-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1829927563-r1 { fill: #c5c8c6 } - .terminal-1829927563-r2 { fill: #e3e3e3 } - .terminal-1829927563-r3 { fill: #e1e1e1 } - .terminal-1829927563-r4 { fill: #004578 } - .terminal-1829927563-r5 { fill: #e0e8ee;font-weight: bold } - .terminal-1829927563-r6 { fill: #e2e3e3 } - .terminal-1829927563-r7 { fill: #ddedf9 } + .terminal-4119903855-r1 { fill: #c5c8c6 } + .terminal-4119903855-r2 { fill: #e3e3e3 } + .terminal-4119903855-r3 { fill: #e1e1e1 } + .terminal-4119903855-r4 { fill: #004578 } + .terminal-4119903855-r5 { fill: #ddedf9;font-weight: bold } + .terminal-4119903855-r6 { fill: #e2e3e3 } + .terminal-4119903855-r7 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + OptionListApp - - - - OptionListApp - - - - ────────────────────────────────────────────────────── - Aerilon - Aquaria - Canceron - Caprica - Gemenon - Leonis - Libran - Picon - Sagittaron - Scorpia - Tauron - Virgon - - - ────────────────────────────────────────────────────── - - - + + + + OptionListApp + + + + ────────────────────────────────────────────────────── + Aerilon + Aquaria + Canceron + Caprica + Gemenon + Leonis + Libran + Picon + Sagittaron + Scorpia + Tauron + Virgon + + + ────────────────────────────────────────────────────── + + + @@ -18882,139 +19222,139 @@ font-weight: 700; } - .terminal-2055091312-matrix { + .terminal-3443619924-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2055091312-title { + .terminal-3443619924-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2055091312-r1 { fill: #c5c8c6 } - .terminal-2055091312-r2 { fill: #e3e3e3 } - .terminal-2055091312-r3 { fill: #e1e1e1 } - .terminal-2055091312-r4 { fill: #004578 } - .terminal-2055091312-r5 { fill: #e0e8ee;font-weight: bold } - .terminal-2055091312-r6 { fill: #e2e3e3 } - .terminal-2055091312-r7 { fill: #42464b } - .terminal-2055091312-r8 { fill: #777a7e } - .terminal-2055091312-r9 { fill: #14191f } - .terminal-2055091312-r10 { fill: #ddedf9 } + .terminal-3443619924-r1 { fill: #c5c8c6 } + .terminal-3443619924-r2 { fill: #e3e3e3 } + .terminal-3443619924-r3 { fill: #e1e1e1 } + .terminal-3443619924-r4 { fill: #004578 } + .terminal-3443619924-r5 { fill: #ddedf9;font-weight: bold } + .terminal-3443619924-r6 { fill: #e2e3e3 } + .terminal-3443619924-r7 { fill: #42464b } + .terminal-3443619924-r8 { fill: #777a7e } + .terminal-3443619924-r9 { fill: #14191f } + .terminal-3443619924-r10 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + OptionListApp - - - - OptionListApp - - - - ────────────────────────────────────────────────────── - Aerilon - Aquaria - ──────────────────────────────────────────────────── - Canceron - Caprica - ──────────────────────────────────────────────────── - Gemenon - ──────────────────────────────────────────────────── - Leonis - Libran - ────────────────────────────────────────────────────▅▅ - Picon - ──────────────────────────────────────────────────── - Sagittaron - ────────────────────────────────────────────────────── - - - + + + + OptionListApp + + + + ────────────────────────────────────────────────────── + Aerilon + Aquaria + ──────────────────────────────────────────────────── + Canceron + Caprica + ──────────────────────────────────────────────────── + Gemenon + ──────────────────────────────────────────────────── + Leonis + Libran + ────────────────────────────────────────────────────▅▅ + Picon + ──────────────────────────────────────────────────── + Sagittaron + ────────────────────────────────────────────────────── + + + @@ -19045,140 +19385,140 @@ font-weight: 700; } - .terminal-1395459687-matrix { + .terminal-589190207-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1395459687-title { + .terminal-589190207-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1395459687-r1 { fill: #c5c8c6 } - .terminal-1395459687-r2 { fill: #e3e3e3 } - .terminal-1395459687-r3 { fill: #e1e1e1 } - .terminal-1395459687-r4 { fill: #004578 } - .terminal-1395459687-r5 { fill: #e0e8ee;font-weight: bold;font-style: italic; } - .terminal-1395459687-r6 { fill: #e2e3e3 } - .terminal-1395459687-r7 { fill: #e0e8ee;font-weight: bold } - .terminal-1395459687-r8 { fill: #14191f } - .terminal-1395459687-r9 { fill: #e2e3e3;font-style: italic; } - .terminal-1395459687-r10 { fill: #e2e3e3;font-weight: bold } - .terminal-1395459687-r11 { fill: #ddedf9 } + .terminal-589190207-r1 { fill: #c5c8c6 } + .terminal-589190207-r2 { fill: #e3e3e3 } + .terminal-589190207-r3 { fill: #e1e1e1 } + .terminal-589190207-r4 { fill: #004578 } + .terminal-589190207-r5 { fill: #ddedf9;font-weight: bold;font-style: italic; } + .terminal-589190207-r6 { fill: #e2e3e3 } + .terminal-589190207-r7 { fill: #ddedf9;font-weight: bold } + .terminal-589190207-r8 { fill: #14191f } + .terminal-589190207-r9 { fill: #e2e3e3;font-style: italic; } + .terminal-589190207-r10 { fill: #e2e3e3;font-weight: bold } + .terminal-589190207-r11 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + OptionListApp - - - - OptionListApp - - - - ────────────────────────────────────────────────────── -                   Data for Aerilon                   - ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ - Patron God   Population    Capital City   ▂▂ - ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ - Demeter      1.2 Billion   Gaoth           - └───────────────┴────────────────┴─────────────────┘ -                   Data for Aquaria                   - ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ - Patron God    Population   Capital City    - ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ - Hermes        75,000       None            - └────────────────┴───────────────┴─────────────────┘ -                  Data for Canceron                   - ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ - ────────────────────────────────────────────────────── - - - + + + + OptionListApp + + + + ────────────────────────────────────────────────────── +                   Data for Aerilon                   + ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ + Patron God   Population    Capital City   ▂▂ + ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ + Demeter      1.2 Billion   Gaoth           + └───────────────┴────────────────┴─────────────────┘ +                   Data for Aquaria                   + ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ + Patron God    Population   Capital City    + ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ + Hermes        75,000       None            + └────────────────┴───────────────┴─────────────────┘ +                  Data for Canceron                   + ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ + ────────────────────────────────────────────────────── + + + @@ -19209,134 +19549,135 @@ font-weight: 700; } - .terminal-2380819869-matrix { + .terminal-1891986877-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2380819869-title { + .terminal-1891986877-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2380819869-r1 { fill: #e1e9ef;font-weight: bold } - .terminal-2380819869-r2 { fill: #c5c8c6 } - .terminal-2380819869-r3 { fill: #e4e5e6 } - .terminal-2380819869-r4 { fill: #4f5459 } - .terminal-2380819869-r5 { fill: #cc555a } + .terminal-1891986877-r1 { fill: #ddedf9;font-weight: bold } + .terminal-1891986877-r2 { fill: #e1e9ef;font-weight: bold } + .terminal-1891986877-r3 { fill: #c5c8c6 } + .terminal-1891986877-r4 { fill: #e4e5e6 } + .terminal-1891986877-r5 { fill: #4f5459 } + .terminal-1891986877-r6 { fill: #cc555a } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + OptionListApp - - - - OneOneOne - TwoTwoTwo - ──────────────────────────────────────────────────────────────────────────────── - ThreeThreeThree - - - - - - - - - - - - - - - - - - - + + + + OneOneOne + TwoTwoTwo + ──────────────────────────────────────────────────────────────────────────────── + ThreeThreeThree + + + + + + + + + + + + + + + + + + + @@ -19367,136 +19708,136 @@ font-weight: 700; } - .terminal-3980370474-matrix { + .terminal-1392305496-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3980370474-title { + .terminal-1392305496-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3980370474-r1 { fill: #ffff00 } - .terminal-3980370474-r2 { fill: #e3e3e3 } - .terminal-3980370474-r3 { fill: #c5c8c6 } - .terminal-3980370474-r4 { fill: #e1e1e1 } - .terminal-3980370474-r5 { fill: #dde8f3;font-weight: bold } - .terminal-3980370474-r6 { fill: #ddedf9 } + .terminal-1392305496-r1 { fill: #ffff00 } + .terminal-1392305496-r2 { fill: #e3e3e3 } + .terminal-1392305496-r3 { fill: #c5c8c6 } + .terminal-1392305496-r4 { fill: #e1e1e1 } + .terminal-1392305496-r5 { fill: #dde8f3;font-weight: bold } + .terminal-1392305496-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Layers + Layers - - - - ──────────────────────────────────Layers - It's full of stars! My God! It's full of sta - - This should float over the top - - - ────────────────────────────────── - - - - - - - - - - - - - - - - -  T  Toggle Screen  + + + + ──────────────────────────────────Layers + It's full of stars! My God! It's full of sta + + This should float over the top + + + ────────────────────────────────── + + + + + + + + + + + + + + + + +  T  Toggle Screen  @@ -19526,136 +19867,136 @@ font-weight: 700; } - .terminal-1053593998-matrix { + .terminal-3727479996-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1053593998-title { + .terminal-3727479996-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1053593998-r1 { fill: #ffff00 } - .terminal-1053593998-r2 { fill: #e3e3e3 } - .terminal-1053593998-r3 { fill: #c5c8c6 } - .terminal-1053593998-r4 { fill: #ddeedd } - .terminal-1053593998-r5 { fill: #dde8f3;font-weight: bold } - .terminal-1053593998-r6 { fill: #ddedf9 } + .terminal-3727479996-r1 { fill: #ffff00 } + .terminal-3727479996-r2 { fill: #e3e3e3 } + .terminal-3727479996-r3 { fill: #c5c8c6 } + .terminal-3727479996-r4 { fill: #ddeedd } + .terminal-3727479996-r5 { fill: #dde8f3;font-weight: bold } + .terminal-3727479996-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Layers + Layers - - - - ──────────────────────────────────Layers - It's full of stars! My God! It's full of sta - - This should float over the top - - - ────────────────────────────────── - - - - - - - - - - - - - - - - -  T  Toggle Screen  + + + + ──────────────────────────────────Layers + It's full of stars! My God! It's full of sta + + This should float over the top + + + ────────────────────────────────── + + + + + + + + + + + + + + + + +  T  Toggle Screen  @@ -19685,142 +20026,142 @@ font-weight: 700; } - .terminal-700023403-matrix { + .terminal-1570661136-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-700023403-title { + .terminal-1570661136-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-700023403-r1 { fill: #c5c8c6 } - .terminal-700023403-r2 { fill: #eae3e5 } - .terminal-700023403-r3 { fill: #e8e0e7 } - .terminal-700023403-r4 { fill: #efe9e4 } - .terminal-700023403-r5 { fill: #ede6e6 } - .terminal-700023403-r6 { fill: #efeedf } - .terminal-700023403-r7 { fill: #e9eee5 } - .terminal-700023403-r8 { fill: #e2edeb } - .terminal-700023403-r9 { fill: #e4eee8;font-weight: bold } - .terminal-700023403-r10 { fill: #dfebed;font-weight: bold } - .terminal-700023403-r11 { fill: #dfe9ed } - .terminal-700023403-r12 { fill: #e3e6eb;font-weight: bold } - .terminal-700023403-r13 { fill: #e6e3e9 } + .terminal-1570661136-r1 { fill: #c5c8c6 } + .terminal-1570661136-r2 { fill: #eae3e5 } + .terminal-1570661136-r3 { fill: #e8e0e7 } + .terminal-1570661136-r4 { fill: #efe9e4 } + .terminal-1570661136-r5 { fill: #ede6e6 } + .terminal-1570661136-r6 { fill: #efeedf } + .terminal-1570661136-r7 { fill: #e9eee5 } + .terminal-1570661136-r8 { fill: #e2edeb } + .terminal-1570661136-r9 { fill: #e4eee8;font-weight: bold } + .terminal-1570661136-r10 { fill: #dfebed;font-weight: bold } + .terminal-1570661136-r11 { fill: #dfe9ed } + .terminal-1570661136-r12 { fill: #e3e6eb;font-weight: bold } + .terminal-1570661136-r13 { fill: #e6e3e9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - PlaceholderApp + PlaceholderApp - - - - - Placeholder p2 here! - This is a custom label for p1. - #p4 - #p3#p5Placeholde - r - - Lorem ipsum dolor sit  - 26 x 6amet, consectetur 27 x 6 - adipiscing elit. Etiam  - feugiat ac elit sit amet  - - - Lorem ipsum dolor sit amet,  - consectetur adipiscing elit. Etiam 40 x 6 - feugiat ac elit sit amet accumsan.  - Suspendisse bibendum nec libero quis  - gravida. Phasellus id eleifend ligula. - Nullam imperdiet sem tellus, sed  - vehicula nisl faucibus sit amet. Lorem ipsum dolor sit amet,  - Praesent iaculis tempor ultricies. Sedconsectetur adipiscing elit. Etiam  - lacinia, tellus id rutrum lacinia, feugiat ac elit sit amet accumsan.  - sapien sapien congue mauris, sit amet Suspendisse bibendum nec libero quis  + + + + + Placeholder p2 here! + This is a custom label for p1. + #p4 + #p3#p5Placeholde + r + + Lorem ipsum dolor sit  + 26 x 6amet, consectetur 27 x 6 + adipiscing elit. Etiam  + feugiat ac elit sit amet  + + + Lorem ipsum dolor sit amet,  + consectetur adipiscing elit. Etiam 40 x 6 + feugiat ac elit sit amet accumsan.  + Suspendisse bibendum nec libero quis  + gravida. Phasellus id eleifend ligula. + Nullam imperdiet sem tellus, sed  + vehicula nisl faucibus sit amet. Lorem ipsum dolor sit amet,  + Praesent iaculis tempor ultricies. Sedconsectetur adipiscing elit. Etiam  + lacinia, tellus id rutrum lacinia, feugiat ac elit sit amet accumsan.  + sapien sapien congue mauris, sit amet Suspendisse bibendum nec libero quis  @@ -20006,135 +20347,135 @@ font-weight: 700; } - .terminal-1426024135-matrix { + .terminal-230009450-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1426024135-title { + .terminal-230009450-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1426024135-r1 { fill: #c5c8c6 } - .terminal-1426024135-r2 { fill: #4ebf71 } - .terminal-1426024135-r3 { fill: #e1e1e1 } - .terminal-1426024135-r4 { fill: #dde8f3;font-weight: bold } - .terminal-1426024135-r5 { fill: #ddedf9 } + .terminal-230009450-r1 { fill: #c5c8c6 } + .terminal-230009450-r2 { fill: #e1e1e1 } + .terminal-230009450-r3 { fill: #4ebf71 } + .terminal-230009450-r4 { fill: #dde8f3;font-weight: bold } + .terminal-230009450-r5 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - IndeterminateProgressBar + IndeterminateProgressBar - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:-- - - - - - - - - - - - -  S  Start  + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:-- + + + + + + + + + + + +  S  Start  @@ -20164,136 +20505,137 @@ font-weight: 700; } - .terminal-1998155485-matrix { + .terminal-3162092160-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1998155485-title { + .terminal-3162092160-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1998155485-r1 { fill: #c5c8c6 } - .terminal-1998155485-r2 { fill: #b93c5b } - .terminal-1998155485-r3 { fill: #1e1e1e } - .terminal-1998155485-r4 { fill: #e1e1e1;text-decoration: underline; } - .terminal-1998155485-r5 { fill: #dde8f3;font-weight: bold } - .terminal-1998155485-r6 { fill: #ddedf9 } + .terminal-3162092160-r1 { fill: #c5c8c6 } + .terminal-3162092160-r2 { fill: #e1e1e1 } + .terminal-3162092160-r3 { fill: #b93c5b } + .terminal-3162092160-r4 { fill: #1e1e1e } + .terminal-3162092160-r5 { fill: #e1e1e1;text-decoration: underline; } + .terminal-3162092160-r6 { fill: #dde8f3;font-weight: bold } + .terminal-3162092160-r7 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - StyledProgressBar + StyledProgressBar - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:-- - - - - - - - - - - - -  S  Start  + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:-- + + + + + + + + + + + +  S  Start  @@ -20323,136 +20665,136 @@ font-weight: 700; } - .terminal-836496735-matrix { + .terminal-1630089489-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-836496735-title { + .terminal-1630089489-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-836496735-r1 { fill: #c5c8c6 } - .terminal-836496735-r2 { fill: #fea62b } - .terminal-836496735-r3 { fill: #323232 } - .terminal-836496735-r4 { fill: #e1e1e1 } - .terminal-836496735-r5 { fill: #dde8f3;font-weight: bold } - .terminal-836496735-r6 { fill: #ddedf9 } + .terminal-1630089489-r1 { fill: #c5c8c6 } + .terminal-1630089489-r2 { fill: #e1e1e1 } + .terminal-1630089489-r3 { fill: #fea62b } + .terminal-1630089489-r4 { fill: #323232 } + .terminal-1630089489-r5 { fill: #dde8f3;font-weight: bold } + .terminal-1630089489-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - IndeterminateProgressBar + IndeterminateProgressBar - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07 - - - - - - - - - - - -  S  Start  + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07 + + + + + + + + + + + +  S  Start  @@ -20482,137 +20824,138 @@ font-weight: 700; } - .terminal-1783624548-matrix { + .terminal-1532901142-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1783624548-title { + .terminal-1532901142-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1783624548-r1 { fill: #c5c8c6 } - .terminal-1783624548-r2 { fill: #004578 } - .terminal-1783624548-r3 { fill: #152939 } - .terminal-1783624548-r4 { fill: #1e1e1e } - .terminal-1783624548-r5 { fill: #e1e1e1;text-decoration: underline; } - .terminal-1783624548-r6 { fill: #dde8f3;font-weight: bold } - .terminal-1783624548-r7 { fill: #ddedf9 } + .terminal-1532901142-r1 { fill: #c5c8c6 } + .terminal-1532901142-r2 { fill: #e1e1e1 } + .terminal-1532901142-r3 { fill: #004578 } + .terminal-1532901142-r4 { fill: #152939 } + .terminal-1532901142-r5 { fill: #1e1e1e } + .terminal-1532901142-r6 { fill: #e1e1e1;text-decoration: underline; } + .terminal-1532901142-r7 { fill: #dde8f3;font-weight: bold } + .terminal-1532901142-r8 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - StyledProgressBar + StyledProgressBar - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07 - - - - - - - - - - - -  S  Start  + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07 + + + + + + + + + + + +  S  Start  @@ -20642,136 +20985,136 @@ font-weight: 700; } - .terminal-2036756687-matrix { + .terminal-3440292978-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2036756687-title { + .terminal-3440292978-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2036756687-r1 { fill: #c5c8c6 } - .terminal-2036756687-r2 { fill: #323232 } - .terminal-2036756687-r3 { fill: #b93c5b } - .terminal-2036756687-r4 { fill: #e1e1e1 } - .terminal-2036756687-r5 { fill: #dde8f3;font-weight: bold } - .terminal-2036756687-r6 { fill: #ddedf9 } + .terminal-3440292978-r1 { fill: #c5c8c6 } + .terminal-3440292978-r2 { fill: #e1e1e1 } + .terminal-3440292978-r3 { fill: #323232 } + .terminal-3440292978-r4 { fill: #b93c5b } + .terminal-3440292978-r5 { fill: #dde8f3;font-weight: bold } + .terminal-3440292978-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - IndeterminateProgressBar + IndeterminateProgressBar - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:-- - - - - - - - - - - - -  S  Start  + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:-- + + + + + + + + + + + +  S  Start  @@ -20801,137 +21144,138 @@ font-weight: 700; } - .terminal-4086988071-matrix { + .terminal-4046569674-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4086988071-title { + .terminal-4046569674-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4086988071-r1 { fill: #c5c8c6 } - .terminal-4086988071-r2 { fill: #fea62b } - .terminal-4086988071-r3 { fill: #004578 } - .terminal-4086988071-r4 { fill: #1e1e1e } - .terminal-4086988071-r5 { fill: #e1e1e1;text-decoration: underline; } - .terminal-4086988071-r6 { fill: #dde8f3;font-weight: bold } - .terminal-4086988071-r7 { fill: #ddedf9 } + .terminal-4046569674-r1 { fill: #c5c8c6 } + .terminal-4046569674-r2 { fill: #e1e1e1 } + .terminal-4046569674-r3 { fill: #fea62b } + .terminal-4046569674-r4 { fill: #004578 } + .terminal-4046569674-r5 { fill: #1e1e1e } + .terminal-4046569674-r6 { fill: #e1e1e1;text-decoration: underline; } + .terminal-4046569674-r7 { fill: #dde8f3;font-weight: bold } + .terminal-4046569674-r8 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - StyledProgressBar + StyledProgressBar - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:-- - - - - - - - - - - - -  S  Start  + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:-- + + + + + + + + + + + +  S  Start  @@ -20961,135 +21305,135 @@ font-weight: 700; } - .terminal-1586716314-matrix { + .terminal-1484676870-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1586716314-title { + .terminal-1484676870-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1586716314-r1 { fill: #c5c8c6 } - .terminal-1586716314-r2 { fill: #737373 } - .terminal-1586716314-r3 { fill: #e1e1e1;font-weight: bold } - .terminal-1586716314-r4 { fill: #323232 } - .terminal-1586716314-r5 { fill: #0178d4 } - .terminal-1586716314-r6 { fill: #e1e1e1 } + .terminal-1484676870-r1 { fill: #c5c8c6 } + .terminal-1484676870-r2 { fill: #737373 } + .terminal-1484676870-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-1484676870-r4 { fill: #474747 } + .terminal-1484676870-r5 { fill: #0178d4 } + .terminal-1484676870-r6 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - QuicklyChangeTabsApp + QuicklyChangeTabsApp - + - - - onetwothree - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - three - - - - - - - - - - - - - - - - - - + + + onetwothree + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + three + + + + + + + + + + + + + + + + + + @@ -21444,137 +21788,137 @@ font-weight: 700; } - .terminal-2779683141-matrix { + .terminal-1869274227-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2779683141-title { + .terminal-1869274227-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2779683141-r1 { fill: #c5c8c6 } - .terminal-2779683141-r2 { fill: #e3e3e3 } - .terminal-2779683141-r3 { fill: #008000 } - .terminal-2779683141-r4 { fill: #ffff00 } - .terminal-2779683141-r5 { fill: #e1e1e1 } - .terminal-2779683141-r6 { fill: #dde8f3;font-weight: bold } - .terminal-2779683141-r7 { fill: #ddedf9 } + .terminal-1869274227-r1 { fill: #c5c8c6 } + .terminal-1869274227-r2 { fill: #e3e3e3 } + .terminal-1869274227-r3 { fill: #008000 } + .terminal-1869274227-r4 { fill: #ffff00 } + .terminal-1869274227-r5 { fill: #e1e1e1 } + .terminal-1869274227-r6 { fill: #dde8f3;font-weight: bold } + .terminal-1869274227-r7 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - VerticalRemoveApp + VerticalRemoveApp - - - - VerticalRemoveApp - ────────────────────────────────────────────────────────────────────────────── - ──────────────────── - This is a test label - ──────────────────── - ────────────────────────────────────────────────────────────────────────────── - - - - - - - - - - - - - - - - - -  A  Add  D  Delete  + + + + VerticalRemoveApp + ────────────────────────────────────────────────────────────────────────────── + ──────────────────── + This is a test label + ──────────────────── + ────────────────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + + + + + +  A  Add  D  Delete  @@ -21604,135 +21948,299 @@ font-weight: 700; } - .terminal-3992644605-matrix { + .terminal-1316892474-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3992644605-title { + .terminal-1316892474-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3992644605-r1 { fill: #c5c8c6 } - .terminal-3992644605-r2 { fill: #e3e3e3 } - .terminal-3992644605-r3 { fill: #e1e1e1 } - .terminal-3992644605-r4 { fill: #dde8f3;font-weight: bold } - .terminal-3992644605-r5 { fill: #ddedf9 } + .terminal-1316892474-r1 { fill: #c5c8c6 } + .terminal-1316892474-r2 { fill: #e3e3e3 } + .terminal-1316892474-r3 { fill: #e1e1e1 } + .terminal-1316892474-r4 { fill: #dde8f3;font-weight: bold } + .terminal-1316892474-r5 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ModalApp + ModalApp - - - - ModalApp - B - - - - - - - - - - - - - - - - - - - - - -  A  Push screen A  + + + + ModalApp + B + + + + + + + + + + + + + + + + + + + + + +  A  Push screen A  + + + + + ''' +# --- +# name: test_scroll_to + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollOffByOne + + + + + + + + + + X27 + X28 + X29 + X30 + X31 + X32 + X33▄▄ + X34 + X35 + X36 + X37 + X38 + X39▂▂ + X40 + X41 + X42 + X43 + X44 + X45 + X46 + X47 + X48 + X49 + X50 + @@ -22075,134 +22583,134 @@ font-weight: 700; } - .terminal-2749576739-matrix { + .terminal-1647606097-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2749576739-title { + .terminal-1647606097-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2749576739-r1 { fill: #c5c8c6 } - .terminal-2749576739-r2 { fill: #e3e3e3 } - .terminal-2749576739-r3 { fill: #ff0000 } - .terminal-2749576739-r4 { fill: #dde2e8 } - .terminal-2749576739-r5 { fill: #ddedf9 } + .terminal-1647606097-r1 { fill: #c5c8c6 } + .terminal-1647606097-r2 { fill: #e3e3e3 } + .terminal-1647606097-r3 { fill: #ff0000 } + .terminal-1647606097-r4 { fill: #dde2e8 } + .terminal-1647606097-r5 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollViewTester + ScrollViewTester - - - - ScrollViewTester -  1 ────────────────────────────────────────────────────────────────────────── - Welcome to line 980 - Welcome to line 981 - Welcome to line 982 - Welcome to line 983 - Welcome to line 984 - Welcome to line 985 - Welcome to line 986 - Welcome to line 987 - Welcome to line 988 - Welcome to line 989 - Welcome to line 990 - Welcome to line 991 - Welcome to line 992 - Welcome to line 993 - Welcome to line 994 - Welcome to line 995 - Welcome to line 996 - Welcome to line 997 - Welcome to line 998 - Welcome to line 999 - ────────────────────────────────────────────────────────────────────────────── + + + + ScrollViewTester +  1 ────────────────────────────────────────────────────────────────────────── + Welcome to line 980 + Welcome to line 981 + Welcome to line 982 + Welcome to line 983 + Welcome to line 984 + Welcome to line 985 + Welcome to line 986 + Welcome to line 987 + Welcome to line 988 + Welcome to line 989 + Welcome to line 990 + Welcome to line 991 + Welcome to line 992 + Welcome to line 993 + Welcome to line 994 + Welcome to line 995 + Welcome to line 996 + Welcome to line 997 + Welcome to line 998 + Welcome to line 999 + ────────────────────────────────────────────────────────────────────────────── @@ -22233,136 +22741,136 @@ font-weight: 700; } - .terminal-4198692207-matrix { + .terminal-1161182100-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4198692207-title { + .terminal-1161182100-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4198692207-r1 { fill: #c5c8c6 } - .terminal-4198692207-r2 { fill: #e3e3e3 } - .terminal-4198692207-r3 { fill: #e1e1e1 } - .terminal-4198692207-r4 { fill: #1e1e1e } - .terminal-4198692207-r5 { fill: #121212 } - .terminal-4198692207-r6 { fill: #787878 } - .terminal-4198692207-r7 { fill: #a8a8a8 } + .terminal-1161182100-r1 { fill: #c5c8c6 } + .terminal-1161182100-r2 { fill: #e3e3e3 } + .terminal-1161182100-r3 { fill: #e1e1e1 } + .terminal-1161182100-r4 { fill: #1e1e1e } + .terminal-1161182100-r5 { fill: #0178d4 } + .terminal-1161182100-r6 { fill: #787878 } + .terminal-1161182100-r7 { fill: #a8a8a8 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectApp + SelectApp - - - - SelectApp - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Select - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - + + + + SelectApp + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + @@ -22393,140 +22901,140 @@ font-weight: 700; } - .terminal-1874975621-matrix { + .terminal-2035490498-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1874975621-title { + .terminal-2035490498-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1874975621-r1 { fill: #c5c8c6 } - .terminal-1874975621-r2 { fill: #e3e3e3 } - .terminal-1874975621-r3 { fill: #e1e1e1 } - .terminal-1874975621-r4 { fill: #1e1e1e } - .terminal-1874975621-r5 { fill: #0178d4 } - .terminal-1874975621-r6 { fill: #787878 } - .terminal-1874975621-r7 { fill: #a8a8a8 } - .terminal-1874975621-r8 { fill: #121212 } - .terminal-1874975621-r9 { fill: #ddedf9;font-weight: bold } - .terminal-1874975621-r10 { fill: #85beea;font-weight: bold } - .terminal-1874975621-r11 { fill: #e2e3e3 } + .terminal-2035490498-r1 { fill: #c5c8c6 } + .terminal-2035490498-r2 { fill: #e3e3e3 } + .terminal-2035490498-r3 { fill: #e1e1e1 } + .terminal-2035490498-r4 { fill: #1e1e1e } + .terminal-2035490498-r5 { fill: #0178d4 } + .terminal-2035490498-r6 { fill: #787878 } + .terminal-2035490498-r7 { fill: #a8a8a8 } + .terminal-2035490498-r8 { fill: #121212 } + .terminal-2035490498-r9 { fill: #ddedf9;font-weight: bold } + .terminal-2035490498-r10 { fill: #85beea;font-weight: bold } + .terminal-2035490498-r11 { fill: #e2e3e3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectApp + SelectApp - - - - SelectApp - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Select - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Select - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total  - obliteration. - I will face my fear. - I will permit it to pass over me and through me. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + SelectApp + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total  + obliteration. + I will face my fear. + I will permit it to pass over me and through me. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + @@ -22557,136 +23065,299 @@ font-weight: 700; } - .terminal-2181889025-matrix { + .terminal-4010426174-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2181889025-title { + .terminal-4010426174-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2181889025-r1 { fill: #c5c8c6 } - .terminal-2181889025-r2 { fill: #e3e3e3 } - .terminal-2181889025-r3 { fill: #e1e1e1 } - .terminal-2181889025-r4 { fill: #1e1e1e } - .terminal-2181889025-r5 { fill: #0178d4 } - .terminal-2181889025-r6 { fill: #e2e2e2 } - .terminal-2181889025-r7 { fill: #a8a8a8 } + .terminal-4010426174-r1 { fill: #c5c8c6 } + .terminal-4010426174-r2 { fill: #e3e3e3 } + .terminal-4010426174-r3 { fill: #e1e1e1 } + .terminal-4010426174-r4 { fill: #1e1e1e } + .terminal-4010426174-r5 { fill: #0178d4 } + .terminal-4010426174-r6 { fill: #e2e2e2 } + .terminal-4010426174-r7 { fill: #a8a8a8 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectApp + SelectApp - - - - I must not fear. - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - I must not fear. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - + + + + I must not fear. + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + I must not fear. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_select_rebuild + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectRebuildApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + This + Should + Be + What + Goes + Into + The + Snapshit + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + @@ -22717,136 +23388,136 @@ font-weight: 700; } - .terminal-932889121-matrix { + .terminal-2914557706-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-932889121-title { + .terminal-2914557706-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-932889121-r1 { fill: #e1e1e1 } - .terminal-932889121-r2 { fill: #c5c8c6 } - .terminal-932889121-r3 { fill: #e1e1e1;font-weight: bold } - .terminal-932889121-r4 { fill: #1e1e1e } - .terminal-932889121-r5 { fill: #0178d4 } - .terminal-932889121-r6 { fill: #e2e3e3 } - .terminal-932889121-r7 { fill: #e3e8e8 } + .terminal-2914557706-r1 { fill: #e1e1e1 } + .terminal-2914557706-r2 { fill: #c5c8c6 } + .terminal-2914557706-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-2914557706-r4 { fill: #1e1e1e } + .terminal-2914557706-r5 { fill: #0178d4 } + .terminal-2914557706-r6 { fill: #e2e3e3 } + .terminal-2914557706-r7 { fill: #e3e8e8 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SwitchApp + SwitchApp - - - - - - - - Example switches - - - ▔▔▔▔▔▔▔▔ - off:      - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - on:       - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - focused:  - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - custom:   - ▁▁▁▁▁▁▁▁ - - - - + + + + + + + + Example switches + + + ▔▔▔▔▔▔▔▔ + off:      + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + on:       + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + focused:  + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + custom:   + ▁▁▁▁▁▁▁▁ + + + + @@ -22877,140 +23548,141 @@ font-weight: 700; } - .terminal-1328081937-matrix { + .terminal-3875007613-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1328081937-title { + .terminal-3875007613-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1328081937-r1 { fill: #c5c8c6 } - .terminal-1328081937-r2 { fill: #737373 } - .terminal-1328081937-r3 { fill: #e1e1e1;font-weight: bold } - .terminal-1328081937-r4 { fill: #323232 } - .terminal-1328081937-r5 { fill: #0178d4 } - .terminal-1328081937-r6 { fill: #121212 } - .terminal-1328081937-r7 { fill: #0053aa } - .terminal-1328081937-r8 { fill: #dde8f3;font-weight: bold } - .terminal-1328081937-r9 { fill: #e1e1e1 } - .terminal-1328081937-r10 { fill: #ddedf9 } + .terminal-3875007613-r1 { fill: #c5c8c6 } + .terminal-3875007613-r2 { fill: #737373 } + .terminal-3875007613-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-3875007613-r4 { fill: #474747 } + .terminal-3875007613-r5 { fill: #0178d4 } + .terminal-3875007613-r6 { fill: #121212 } + .terminal-3875007613-r7 { fill: #0053aa } + .terminal-3875007613-r8 { fill: #dde8f3;font-weight: bold } + .terminal-3875007613-r9 { fill: #e1e1e1 } + .terminal-3875007613-r10 { fill: #323232 } + .terminal-3875007613-r11 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TabbedApp + TabbedApp - + - - - LetoJessicaPaul - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - Lady Jessica - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Bene Gesserit and concubine of Leto, and mother of Paul and Alia. - - - - PaulAlia - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - First child - - - - - - -  L  Leto  J  Jessica  P  Paul  + + + LetoJessicaPaul + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Lady Jessica + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Bene Gesserit and concubine of Leto, and mother of Paul and Alia. + + + + PaulAlia + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + First child + + + + + + +  L  Leto  J  Jessica  P  Paul  @@ -23670,133 +24342,133 @@ font-weight: 700; } - .terminal-1336653930-matrix { + .terminal-3137592172-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1336653930-title { + .terminal-3137592172-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1336653930-r1 { fill: #e2e3e3 } - .terminal-1336653930-r2 { fill: #1a1000;font-weight: bold } - .terminal-1336653930-r3 { fill: #c5c8c6 } - .terminal-1336653930-r4 { fill: #008139 } + .terminal-3137592172-r1 { fill: #e2e3e3 } + .terminal-3137592172-r2 { fill: #211505;font-weight: bold } + .terminal-3137592172-r3 { fill: #c5c8c6 } + .terminal-3137592172-r4 { fill: #fea62b;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TreeApp + TreeApp - - - - ▼ Dune - └── ▼ Characters - ├── Paul - ├── Jessica - └── Chani - - - - - - - - - - - - - - - - - - + + + + ▼ Dune + ┗━━ ▼ Characters + ┣━━ Paul + ┣━━ Jessica + ┗━━ Chani + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/snapshot_apps/dock_scroll2.py b/tests/snapshot_tests/snapshot_apps/dock_scroll2.py new file mode 100644 index 000000000..fe2a1b234 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/dock_scroll2.py @@ -0,0 +1,33 @@ +from textual.app import App +from textual.widgets import Header, Label, Footer + + +# Same as dock_scroll.py but with 2 labels +class TestApp(App): + BINDINGS = [("ctrl+q", "app.quit", "Quit")] + CSS = """ + + Label { + border: solid red; + } + Footer { + height: 4; + } + """ + + def compose(self): + text = ( + "this is a sample sentence and here are some words".replace(" ", "\n") * 2 + ) + yield Header() + yield Label(text) + yield Label(text) + yield Footer() + + def on_mount(self): + self.dark = False + + +if __name__ == "__main__": + app = TestApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py b/tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py new file mode 100644 index 000000000..f9a5a00fd --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py @@ -0,0 +1,17 @@ +from textual.app import App, ComposeResult +from textual.widgets import Checkbox, Footer + + +class ScrollOffByOne(App): + def compose(self) -> ComposeResult: + for number in range(1, 100): + yield Checkbox(str(number)) + yield Footer() + + def on_mount(self) -> None: + self.query_one("Screen").scroll_end() + + +app = ScrollOffByOne() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css b/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css index 50ea9edff..f135a09c5 100644 --- a/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css +++ b/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css @@ -19,6 +19,6 @@ #horizontal { width: auto; - height: auto; + height: 4; background: darkslateblue; -} \ No newline at end of file +} diff --git a/tests/snapshot_tests/snapshot_apps/scroll_to.py b/tests/snapshot_tests/snapshot_apps/scroll_to.py new file mode 100644 index 000000000..9dd21a3fc --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/scroll_to.py @@ -0,0 +1,19 @@ +from textual.app import App, ComposeResult +from textual.widgets import Checkbox, Footer + + +class ScrollOffByOne(App): + """Scroll to item 50.""" + + def compose(self) -> ComposeResult: + for number in range(1, 100): + yield Checkbox(str(number), id=f"number-{number}") + yield Footer() + + def on_ready(self) -> None: + self.query_one("#number-50").scroll_visible() + + +app = ScrollOffByOne() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/select_rebuild.py b/tests/snapshot_tests/snapshot_apps/select_rebuild.py new file mode 100644 index 000000000..190db3b4b --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/select_rebuild.py @@ -0,0 +1,21 @@ +"""Test https://github.com/Textualize/textual/issues/2557""" + +from textual.app import App, ComposeResult +from textual.widgets import Select, Button + + +class SelectRebuildApp(App[None]): + + def compose(self) -> ComposeResult: + yield Select[int]((("1", 1), ("2", 2))) + yield Button("Rebuild") + + def on_button_pressed(self): + self.query_one(Select).set_options(( + ("This", 0), ("Should", 1), ("Be", 2), + ("What", 3), ("Goes", 4), ("Into",5), + ("The", 6), ("Snapshit", 7) + )) + +if __name__ == "__main__": + SelectRebuildApp().run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index ea5e32153..bdedbced3 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -1,5 +1,4 @@ from pathlib import Path -import sys import pytest @@ -78,8 +77,7 @@ def test_switches(snap_compare): def test_input_and_focus(snap_compare): press = [ - "tab", - *"Darren", # Focus first input, write "Darren" + *"Darren", # Write "Darren" "tab", *"Burns", # Focus second input, write "Burns" ] @@ -88,7 +86,7 @@ def test_input_and_focus(snap_compare): def test_buttons_render(snap_compare): # Testing button rendering. We press tab to focus the first button too. - assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab", "tab"]) + assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab"]) def test_placeholder_render(snap_compare): @@ -189,7 +187,7 @@ def test_content_switcher_example_initial(snap_compare): def test_content_switcher_example_switch(snap_compare): assert snap_compare( WIDGET_EXAMPLES_DIR / "content_switcher.py", - press=["tab", "tab", "enter", "wait:500"], + press=["tab", "enter", "wait:500"], terminal_size=(50, 50), ) @@ -203,9 +201,11 @@ def test_option_list(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_options.py") assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_tables.py") + def test_option_list_build(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "option_list.py") + def test_progress_bar_indeterminate(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_isolated_.py", press=["f"]) @@ -313,7 +313,7 @@ def test_programmatic_scrollbar_gutter_change(snap_compare): def test_borders_preview(snap_compare): - assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["tab", "enter"]) + assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["enter"]) def test_colors_preview(snap_compare): @@ -377,9 +377,7 @@ def test_disabled_widgets(snap_compare): def test_focus_component_class(snap_compare): - assert snap_compare( - SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab", "tab"] - ) + assert snap_compare(SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab"]) def test_line_api_scrollbars(snap_compare): @@ -440,7 +438,7 @@ def test_modal_dialog_bindings_input(snap_compare): # Check https://github.com/Textualize/textual/issues/2194 assert snap_compare( SNAPSHOT_APPS_DIR / "modal_screen_bindings.py", - press=["enter", "tab", "h", "!", "left", "i", "tab"], + press=["enter", "h", "!", "left", "i", "tab"], ) @@ -457,6 +455,23 @@ def test_dock_scroll(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "dock_scroll.py", terminal_size=(80, 25)) +def test_dock_scroll2(snap_compare): + # https://github.com/Textualize/textual/issues/2525 + assert snap_compare(SNAPSHOT_APPS_DIR / "dock_scroll2.py", terminal_size=(80, 25)) + + +def test_dock_scroll_off_by_one(snap_compare): + # https://github.com/Textualize/textual/issues/2525 + assert snap_compare( + SNAPSHOT_APPS_DIR / "dock_scroll_off_by_one.py", terminal_size=(80, 25) + ) + + +def test_scroll_to(snap_compare): + # https://github.com/Textualize/textual/issues/2525 + assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_to.py", terminal_size=(80, 25)) + + def test_auto_fr(snap_compare): # https://github.com/Textualize/textual/issues/2220 assert snap_compare(SNAPSHOT_APPS_DIR / "auto_fr.py", terminal_size=(80, 25)) @@ -493,3 +508,11 @@ def test_quickly_change_tabs(snap_compare): def test_fr_unit_with_min(snap_compare): # https://github.com/Textualize/textual/issues/2378 assert snap_compare(SNAPSHOT_APPS_DIR / "fr_with_min.py") + + +def test_select_rebuild(snap_compare): + # https://github.com/Textualize/textual/issues/2557 + assert snap_compare( + SNAPSHOT_APPS_DIR / "select_rebuild.py", + press=["space", "escape", "tab", "enter", "tab", "space"], + ) diff --git a/tests/test_app.py b/tests/test_app.py index 54bde8221..9cbb82fd0 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,5 +1,5 @@ from textual.app import App, ComposeResult -from textual.widgets import Button +from textual.widgets import Button, Input def test_batch_update(): @@ -20,6 +20,7 @@ def test_batch_update(): class MyApp(App): def compose(self) -> ComposeResult: + yield Input() yield Button("Click me!") diff --git a/tests/test_on.py b/tests/test_on.py index 7812cd616..740af6a94 100644 --- a/tests/test_on.py +++ b/tests/test_on.py @@ -36,7 +36,7 @@ async def test_on_button_pressed() -> None: app = ButtonApp() async with app.run_test() as pilot: - await pilot.press("tab", "enter", "tab", "enter", "tab", "enter") + await pilot.press("enter", "tab", "enter", "tab", "enter") await pilot.pause() assert pressed == [ diff --git a/tests/test_paste.py b/tests/test_paste.py index 774ad5038..6cfc3951f 100644 --- a/tests/test_paste.py +++ b/tests/test_paste.py @@ -1,5 +1,6 @@ from textual import events from textual.app import App +from textual.widgets import Input async def test_paste_app(): @@ -16,3 +17,29 @@ async def test_paste_app(): assert len(paste_events) == 1 assert paste_events[0].text == "Hello" + + +async def test_empty_paste(): + """Regression test for https://github.com/Textualize/textual/issues/2563.""" + + paste_events = [] + + class MyInput(Input): + def on_paste(self, event): + super()._on_paste(event) + paste_events.append(event) + + class PasteApp(App): + def compose(self): + yield MyInput() + + def key_p(self): + self.query_one(MyInput).post_message(events.Paste("")) + + app = PasteApp() + async with app.run_test() as pilot: + app.set_focus(None) + await pilot.press("p") + assert app.query_one(MyInput).value == "" + assert len(paste_events) == 1 + assert paste_events[0].text == "" diff --git a/tests/test_screens.py b/tests/test_screens.py index 6825c101c..bd9dfba3c 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -6,6 +6,7 @@ import pytest from textual.app import App, ScreenStackError from textual.screen import Screen +from textual.widgets import Button, Input, Label skip_py310 = pytest.mark.skipif( sys.version_info.minor == 10 and sys.version_info.major == 3, @@ -150,3 +151,73 @@ async def test_screens(): screen2.remove() screen3.remove() await app._shutdown() + + +async def test_auto_focus(): + class MyScreen(Screen[None]): + def compose(self): + yield Button() + yield Input(id="one") + yield Input(id="two") + + class MyApp(App[None]): + pass + + app = MyApp() + async with app.run_test(): + await app.push_screen(MyScreen()) + assert isinstance(app.focused, Button) + app.pop_screen() + + MyScreen.AUTO_FOCUS = None + await app.push_screen(MyScreen()) + assert app.focused is None + app.pop_screen() + + MyScreen.AUTO_FOCUS = "Input" + await app.push_screen(MyScreen()) + assert isinstance(app.focused, Input) + assert app.focused.id == "one" + app.pop_screen() + + MyScreen.AUTO_FOCUS = "#two" + await app.push_screen(MyScreen()) + assert isinstance(app.focused, Input) + assert app.focused.id == "two" + + # If we push and pop another screen, focus should be preserved for #two. + MyScreen.AUTO_FOCUS = None + await app.push_screen(MyScreen()) + assert app.focused is None + app.pop_screen() + assert app.focused.id == "two" + + +async def test_auto_focus_skips_non_focusable_widgets(): + class MyScreen(Screen[None]): + def compose(self): + yield Label() + yield Button() + + class MyApp(App[None]): + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test(): + assert app.focused is not None + assert isinstance(app.focused, Button) + + +async def test_dismiss_non_top_screen(): + class MyApp(App[None]): + async def key_p(self) -> None: + self.bottom, top = Screen(), Screen() + await self.push_screen(self.bottom) + await self.push_screen(top) + + app = MyApp() + async with app.run_test() as pilot: + await pilot.press("p") + with pytest.raises(ScreenStackError): + app.bottom.dismiss() diff --git a/tests/test_widget_child_moving.py b/tests/test_widget_child_moving.py index 520ef7810..f2d40a4aa 100644 --- a/tests/test_widget_child_moving.py +++ b/tests/test_widget_child_moving.py @@ -42,22 +42,18 @@ async def test_move_child_to_outside() -> None: pilot.app.screen.move_child(child, before=Widget()) -@pytest.mark.xfail( - strict=True, reason="https://github.com/Textualize/textual/issues/1743" -) async def test_move_child_before_itself() -> None: """Test moving a widget before itself.""" + async with App().run_test() as pilot: child = Widget(Widget()) await pilot.app.mount(child) pilot.app.screen.move_child(child, before=child) -@pytest.mark.xfail( - strict=True, reason="https://github.com/Textualize/textual/issues/1743" -) async def test_move_child_after_itself() -> None: """Test moving a widget after itself.""" + # Regression test for https://github.com/Textualize/textual/issues/1743 async with App().run_test() as pilot: child = Widget(Widget()) await pilot.app.mount(child) diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index 95025bf37..51765b99e 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -39,6 +39,7 @@ async def test_radio_sets_initial_state(): async def test_click_sets_focus(): """Clicking within a radio set should set focus.""" async with RadioSetApp().run_test() as pilot: + pilot.app.set_focus(None) assert pilot.app.screen.focused is None await pilot.click("#clickme") assert pilot.app.screen.focused == pilot.app.query_one("#from_buttons") @@ -72,8 +73,6 @@ async def test_radioset_same_button_mash(): async def test_radioset_inner_navigation(): """Using the cursor keys should navigate between buttons in a set.""" async with RadioSetApp().run_test() as pilot: - assert pilot.app.screen.focused is None - await pilot.press("tab") for key, landing in ( ("down", 1), ("up", 0), @@ -88,8 +87,6 @@ async def test_radioset_inner_navigation(): == pilot.app.query_one("#from_buttons").children[landing] ) async with RadioSetApp().run_test() as pilot: - assert pilot.app.screen.focused is None - await pilot.press("tab") assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_buttons") await pilot.press("tab") assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_strings") @@ -101,8 +98,6 @@ async def test_radioset_inner_navigation(): async def test_radioset_breakout_navigation(): """Shift/Tabbing while in a radioset should move to the previous/next focsuable after the set itself.""" async with RadioSetApp().run_test() as pilot: - assert pilot.app.screen.focused is None - await pilot.press("tab") assert pilot.app.screen.focused is pilot.app.query_one("#from_buttons") await pilot.press("tab") assert pilot.app.screen.focused is pilot.app.query_one("#from_strings")