Skip to content

Release 0.3.0: the Basic editor, a turning display preview and the Media Source picture as designed - #11

Merged
Misiu merged 7 commits into
mainfrom
basic_editor
Nov 10, 2026
Merged

Misiu merged 7 commits into
mainfrom
basic_editor

Conversation

@Misiu

@Misiu Misiu commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Summary

Release 0.3.0. The main change is a second editor, Basic, beside the full one.

The Basic editor

  • A dashboard has a mode, chosen when it is created and kept: advanced (the editor there
    is, and the default, so every stored dashboard stays as it was) or basic. There is no
    conversion between the two.
  • A Basic dashboard is a grid cut into regions with one widget in each. A region is any
    rectangle of cells, up to the whole grid, and the regions cover every cell once. The backend
    derives each widget's frame from its region (basic_layout.py, through the SDK grid), so the
    rendering, the Media Source, sending to a device and export are the same pipeline and
    everything is still ODL drawn by odl-renderer.
  • Layout tab: columns, rows, gap and outer padding; one click on a cell and one on another make
    a region, a double click removes it, regions are lettered in the order they were made.
    Widgets tab: widgets only in the library, properties on the right, no tree and no arrange bar.
    Only empty regions are outlined, and a click beside the regions lets go of the selection.
  • The same canvas, library and properties panel as Advanced, with the differences passed in as
    properties instead of a second copy.
  • New opendisplay_studio/layout_frames command and layoutLimits in bootstrap, so the panel
    never lays a grid out itself.

New dashboard window and the list

  • The window starts with the editor, then where the display comes from, the display and the
    name, and a preview of the display with two arrows that turn it a quarter at a time (a lean
    back, a swing past and a settle; none with reduced motion).
  • One drawing of a display (ods-display-preview) serves the window and the cards of the list,
    which now show each dashboard as its display, mark it Basic or Advanced and are no taller than
    their content.

Media Source picture as designed

  • New switch in Display settings, off by default. Off: the picture is turned for the display as
    before. On: it keeps the orientation of the designer, which suits a preview or a card; to
    show it with opendisplay.upload_image give the dashboard's rotation as rotation (the text
    under the switch names it). What the editor sends to a device is always turned for it.
  • Not stored as EXIF: the OpenDisplay action calls ImageOps.exif_transpose on every image, so
    an orientation tag would turn the picture back before it reaches the device.

Fixes

  • A turned text that starts outside the display lost its first letters in the Media Source and
    in what is sent to a device, while the editor showed it whole. A dashboard is now always drawn
    once on the editor's picture and the margin is cut off afterwards; a new test compares the
    two, pixel for pixel, at every rotation (twelve of its cases failed before the change).
  • The cells of an SDK grid stopped short of the edge of their box by the width of the gaps. The
    weather widget moved by a few pixels; its golden pictures and the widgets page are regenerated.
  • The menu of a dashboard had no icon for Display settings (mdi:monitor-cog is not in Home
    Assistant's set); it has the cog now.

Notes for review

  • schemaVersion stays 1; mode, layout, region and display.mediaAsDesigned are optional
    and default to the old behaviour.
  • The bundle is rebuilt. The Windows snapshot of the new dashboard window is updated here; the
    Linux one is updated from the CI run where it differs.
  • tests/test_images.py::test_a_camera_entity_is_drawn_from_its_current_picture fails in my
    local environment with or without this change (no camera component); CI has the full set.

Test plan

  • ruff check, ruff format --check, mypy, pytest (2236 pass locally, the camera test aside)
  • npm run lint, npm test (729), npm run build, npm run test:e2e (369 pass locally)
  • CI
  • Media Source picture, both settings, on a real display

The SDK split a box into equal cells without counting the gaps between them, so the
last cell stopped short of the edge: three columns with a gap of 8 in 800 px ended at
792 px. The shares are now cut from what is left after the gaps, so the cells and the
gaps add up to the box exactly, and a cell is never less than one pixel.

Widgets that lay themselves out with gaps get the extra pixels back. Only the weather
widget moved: its golden pictures, its preview and the pictures of the widgets page
are regenerated. The Basic editor builds on this, because the frame of a region is
taken from the same grid.
A dashboard now has a mode, chosen when it is created and kept: Advanced (the editor
there is, and the default, so every stored dashboard stays as it was) or Basic. There
is no conversion between the two. A Basic dashboard is a grid cut into regions with one
widget in each; nothing is dragged, resized or aligned. It goes through the same
pipeline as an Advanced one (compile, preview, Media Source, delivery, export), so
everything is still ODL drawn by odl-renderer.

Model and backend
- Dashboard `mode` ("basic" | "advanced", default advanced) and, for Basic, a `layout`
  with `columns`, `rows`, `gap` and `regions`. A region is a rectangle of cells (row,
  column, rowSpan, columnSpan) of any size up to the whole grid; the regions cover every
  cell exactly once. A widget item carries the `region` it fills.
- `basic_layout.py` validates the layout and sets the frame of each widget from its
  region, through the SDK grid, so the layout is the one place that says where a widget
  is. Advanced dashboards may not carry a layout or a region; a Basic one holds widgets
  only, one to a region. schemaVersion stays 1, the new fields are optional.
- `bootstrap` sends the limits of a grid (`layoutLimits`) and the new
  `opendisplay_studio/layout_frames` command returns where the cells and the regions of a
  layout are on a display, so the panel never lays out a grid itself.

Choosing the editor and the display (new dashboard dialog)
- The dialog starts with the editor (Advanced by default), then where the display comes
  from (a device, a predefined display, a custom size), the display and the name side by
  side, and a preview of the display with two arrows that turn it a quarter at a time.
  The turn is animated: a lean back, a swing past and a settle (rotation-motion.ts).
  The preview bar shows the resolution with the turn and the colors of the palette.
- Padding and snap size are offered only for Advanced; Basic keeps its padding in the
  layout panel and has nothing to snap to. The Rotation select is gone, the arrows replace it.
- One drawing of a display (ods-display-preview) serves the dialog and the cards of the
  list: the proportions of the resolution, the housing turned as the display is set up,
  its light and button where they are on the device. The list marks each dashboard Basic
  or Advanced on the picture, with the colors of its palette on the left and the
  resolution on the right, and the card is no taller than its content.

The Basic editor
- The same canvas, library and properties panel as Advanced, with the differences passed
  in as properties instead of a second copy: regions on the canvas instead of elements,
  a library with widgets only, no structure tree, no arrange bar, and the zoom bar kept.
  Commands that move, copy or regroup elements are disabled for a Basic dashboard.
- Layout tab: columns, rows, gap and outer padding. A click on one cell and a click on
  another join the cells between them into a region; one that would cut another in two is
  refused. A double click removes a region. Regions are lettered A, B, C ... in the order
  they were made, with their size in cells. Resizing the grid keeps the regions that still
  fit and fills the rest with cells.
- Widgets tab: a widget goes into a region by dragging, or into the selected region (or
  the first empty one) by a click. Only an empty region is drawn with a border; a region
  with a widget is not, and the selected one is outlined. A click beside the regions lets
  go of the selection, so what is left is the picture as it will be rendered.
- Pure logic lives in grid-layout.ts (regions, lettering, hit testing) and basic-ops.ts
  (dashboard changes), with unit tests.

Also
- The dev harness learns the layout command, a basic preview and the new limits.
- New files are added to the tsconfig allow-list; the bundle is rebuilt.
- README gets a section on the two editors with plain screenshots, and the picture of the
  new dashboard dialog is replaced. ROADMAP gets phase 12, CLAUDE.md the rule that a
  dashboard keeps its mode.

Tests
- Backend: validation (any rectangles, a region as wide as the grid, a gap in the tiling
  refused), frames of a layout, compile and render of a Basic dashboard, the command.
- Frontend unit: region operations, lettering, placing a widget, hit testing, display
  shape, rotation motion. E2E: choosing the editor, the grid, joining and removing
  regions, filling them, saving, reopening, the marks in the list, selection and borders,
  the preview and its turn. The Windows snapshot of the dialog is updated; the Linux one
  has to be updated where Linux runs.
- Known: tests/test_images.py::test_a_camera_entity_is_drawn_from_its_current_picture
  fails in this environment, with or without this change.
The editor draws a dashboard on a picture with a margin around the display and shows
the display part of it. The Media Source and the picture sent to a device were drawn
on the display alone. The renderer draws an element that hangs out of the picture only
as far as the picture goes, and it turns a text after drawing it, so a turned text that
started outside the display lost its first letters in the output while the editor
showed it whole. The rotation of the display had nothing to do with it.

A dashboard is now always drawn once, on the picture with the margin, and the margin
is cut off afterwards when the picture is for a display; the editor keeps it. The
display is turned after the cut, as before. Delivered pictures have the size they had.

A new test renders seven layouts at all four rotations (a text hanging out of each
edge, a turned text partly outside, a rectangle beyond the display, a text inside, and
the dashboard that showed the fault: a band, a turned text and an agenda) and compares
what the editor shows with what the display receives, turned back, pixel for pixel.
Twelve of its cases failed before the change. CLAUDE.md records the rule.
A dashboard that is turned for its display (rotation 90, say) is shown by the editor in
the orientation you design it in, but the Media Source gave the picture turned for the
display, so a preview in the media browser or a card in Lovelace looked sideways or, for
a portrait picture, ran far below the dialog that shows it: Home Assistant draws an image
at the full width of that dialog and has no height limit.

Display settings has a new switch, "Media Source picture as designed", off by default.
Left off, nothing changes: the picture is turned for the display and goes to
opendisplay.upload_image without a rotation. Switched on, the Media Source gives the
picture in the orientation of the designer, which suits a preview or a card. It is not
turned for the display then, so to show it with opendisplay.upload_image the rotation of
the dashboard has to be given as `rotation`; the text under the switch names the value.
The OpenDisplay action turns pictures by their EXIF orientation itself, so the
orientation is not stored in the picture, which would turn it back before it reaches the
device.

What the editor sends to a device is always turned for it and goes with rotation 0,
whatever the switch says; a test checks both settings.

- `display.mediaAsDesigned` (boolean, default false) in the dashboard; a dashboard stored
  without it keeps being turned. The Media Source reads it; nothing else does.
- The display settings form shows the switch and, under it, what rotation to give. The
  harness form shows helper texts so the text can be seen and tested.
- Texts in English, Polish and German.
- Tests: the setting (default, stored without it, only a boolean), the Media Source at
  each rotation in both settings, sending from the editor with both settings; the form
  data and the helper in unit tests; the switch and the text in e2e.
The menu of a dashboard in the list had no icon for Display settings: it asked for
mdi:monitor-cog, which Home Assistant does not ship in its set of icons. It now uses
mdi:cog-outline, the cog that the Settings button of the list already has. The harness
draws the same glyph for it. The bundle is rebuilt.

A band of a dial, the colored range of the gauge and the thermometer, is drawn by the
renderer as a thick arc. Its ends lie along the radius of the dial, parallel to the
tick line of the value they start and end at; near three o'clock the radius is level,
so the end of a range that starts there looks flat. A new test measures, with the real
renderer, the angles a band leaves ink at and checks them against the angles it was
asked for, for ranges at the top, the sides and the bottom of a dial and across zero.
Bump the version to 0.3.0 where it is written: the manifest, the integration constant,
the panel package and its lock file, pyproject.toml (which was stale at 0.2.0 too) and
the version the development harness reports, and the test that checks the constant.
The changelog gets the entry of the release: the Basic editor, the rebuilt New dashboard
window and its turning preview, the dashboards of the list drawn as their displays, the
Media Source picture as designed, and the fixes to turned text that hangs out of the
display and to the cells of a grid. The notes of the release are read from that entry.
The window has a new layout (the editor, the display with its turning preview) and is
670 px tall now. This is the picture the Linux job of CI took of it, which is the one
that run compares with; the Windows snapshot was updated with the change itself.
@Misiu
Misiu merged commit fc989d0 into main Nov 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant