# UI

> Guide window, hub and options

Page: https://polestar.guide/docs/contributors/architecture/ui (Polestar docs > Contributors > Architecture)

## Options registry

Any module adds settings to the options window without touching UI files:

```lua
Polestar.RegisterOptions({
  page = "Quests", icon = "icon-accept", order = 30,      -- sidebar page (created on first use), its title as shown
  key = "quests",                                           -- optional: the page's stable key (default: made from page)
  tab = "Map", section = "Markers",                         -- tab row entry and section header
  controls = {
    { kind = "toggle",   key = "qol.mapObjectives", label = "Objective markers", help = "One line." },
    { kind = "slider",   key = "qol.markerScale", label = "Marker size", min = 0.5, max = 2, step = 0.05, format = "%.2f\195\151", -- example key
      release = true },                                                 -- optional: write when the drag ends
    { kind = "dropdown", key = "gear.spec", label = "Weights", choices = { { value = "auto", label = "Automatic" }, ... } }, -- example key (the real one is gear.spec.<CLASS>)
      -- choices may be a function (choices that come and go); get()/set(v) may stand in for the path
    { kind = "color",    key = "qol.markerColor", label = "Marker colour", -- example key
      shown = function() return r, g, b end, source = function() return "Scheme" end, linked = { "other.key" } }, -- example key
      -- shown: the colour in effect while the key holds none; source: where it comes from;
      -- linked: paths a cancelled pick restores too. Right-click resets (sliders too).
    { kind = "button",   label = "Reset lockouts", help = "One line.", onClick = function() end },
    { kind = "binding",  action = "POLESTAR_NEXT", label = "Next step", help = "One line." }, -- a key binding row (UI/GamepadGuide.lua)
  },
})
```

Controls read and write `Polestar.Get/Set`; `disabled = function() return bool end` is optional;
registered labels and help are searchable. Pages sort by `order`; Recorder and
About always close the list. A page or tab with no available control is left
out of the sidebar and tab row. Every row resets to its default on right-click.

The built-in pages are grouped by what the player is doing: **Guide** (10:
Window, Arrow & routes, Automation), **Appearance** (20: Colour & type,
Windows, Arrow), **Map** (30), **Questing** (40: Quests, Rewards & gear,
Talents, Buttons), **Discovery** (50: Lookup, Dungeons, Rares), **Quality of Life** (60:
Travel, Social, Vendors, Trackers), **Community guides** (70, a custom page:
`UI/Packs.lua`), **Gamepad** (75, registered by `UI/GamepadGuide.lua`:
Navigation, Key bindings), Recorder, About. The options window
declares every tab and section of the module-filled pages, in order, and the
modules register their controls into them by name (`page = L["Questing"]`,
`tab = L["Rewards & gear"]`, `section = L["Gear advice"]`): Quests, QoL,
Gear and Talents fill Questing; Lookup, Dungeons and Rares fill Discovery; QoL and Maps
(flight timer, instances) fill Quality of Life. On the **Map** page (Guide &
quests: Guide, Quest markers; Rares & travel: Rares and treasures, Dungeons and
travel; Zones: Zones, Coordinates) Nav, Quests, Rares and Maps place every map
and minimap setting.

A page is opened by its key (`/polestar options <key>`, `UI.ShowOptions(key)`,
`Options.Select`), not its title: the built-in pages give English keys
(`guide`, `appearance`, `map`, `questing`, `discovery`, `qualityoflife`,
`guidepacks`, `recorder`, `about`) so a translated title does not move them;
a registered page without `key` takes one made from its title. Old page keys
(`guidewindow`, `navigation`, `automation`, `quests`, `gear`, `lookup`,
`dungeons`, `rares`, …) reach the page and tab that now hold their settings
(`ALIASES` in UI/Options.lua); a page's own key wins over an alias.

The sidebar's page list is its own scroll area (`f.NavScroll`, `W.ShellNav`)
from 12px under the search to the footer bar's line, so the version and stage
chip below it are never crowded, whatever the page count, font or menu scale;
the selected row is scrolled into view. The hub shares it. Its items are
`{ key, label, icon, count, dot }`: `dot` puts a warning dot at the row's
right edge instead of the count (a page spec's `dot = function() end`, the
Community guides page while a pack has problems), and `{ heading = true, label }`
is a muted heading over the rows after it, not a choice (the menu's "Community
guides").

The settings window is a rail shell like the hub (`W.Shell` with `rail`): it
shows the hub's sections (`UI.Picker.RailItems()`, and at its foot
`UI.Picker.FootItems()`) with Options marked, and a
section reopens the hub there. Switching between the two places the window
being opened at the other's top-left corner (`UI.Picker.TakePlace`), so the
rail does not move.

## Guide window, guide menu and zone art

Reading flow: `docs/DESIGN.md` ("Intent" and "The Guide Window").

- **Guide window** (`UI/GuideFrame.lua`): tab strip, a slim navigator (prev,
  step number, next, Skip, the bug that opens the report window, menu), one
  focus block from the step view's `groups`/`focus` (the person to talk to
  now with only their goals, or a field step's objectives), counters right
  after the goal text (in text, success once complete; the accent is kept
  for the current goal's glyph, the arrow's target mark, tabs, primary
  buttons and the progress fill), a "Then: <next
  person>" line with "Person n of N", tips folded to one line (an opened tip
  shows the step's `image` under it: the texture in its pack's addon folder,
  as wide as the text column, its aspect kept, at most 160 high; none for a
  pack without an addon folder, or a file the client cannot load), and the
  guide's progress bar with percent. Up next is off by default (`ui.upcoming = 0`).
  Transparent mode fades the whole navigator (step number with its chevrons)
  until hovered and keeps the progress track as a faint ink line.
- **Hub** (`UI/Picker.lua`, `UI.ShowPicker(opts)`; the window the guide menu
  grew into, `docs/SUITE.md` "UI"). A section rail (`W.Shell` with
  `rail = true`, `W.ShellRail`) at the window's left: the emblem, then one
  button per section from the 160 line (glyph over label, 56px rows; eight fit
  at 680 tall), Options at its foot. The sidebar holds the wordmark, the hub
  search and the active section's index (`W.ShellNav`). Guides is the built-in
  section; every other module registers:

  ```lua
  UI.RegisterSection({
    key = "Dungeons", label = L["Dungeons"], icon = "icon-dungeon", order = 30,
    build = function(parent, hub) end,  -- once; parent spans the list and details panes
    render = function() end,            -- every hub render while shown (W.ShellTitle/W.ShellTabs on hub)
    nav = function() return items, activeKey end, -- its index in the sidebar ({ key, label, icon, count })
    onNav = function(key) end,           -- set the state, then UI.Picker.Render()
    search = function(text) end, enter = function() end, step = function(delta) end, -- optional
    searchPlaceholder = "...",           -- optional; or a list of wordings, longest first: the first that fits the box shows
  })
  ```

  `UI.Picker.ShowSection(key)` shows one ("Guides" returns to the guide
  list), `P.Section()` names it, `P.Query()` is the search text. A section's
  top-level divisions go in `nav`, not in a tab row; `W.ShellTabs` on the hub
  is for views inside one division, and the section's frame starts under it.
  A section registered with `foot = true` sits at the rail's foot, above
  Options, instead of among the sections (`P.FootItems(onOptions)`: Options,
  then each foot section, from the bottom up).
- **Import** (`UI/Import.lua`, a foot section, order 90; `/polestar import`
  and `SHOW_IMPORT` open it). The two ways to bring someone else's guides
  into Polestar side by side (an import string for one small guide, a
  community addon for a route or a set), then a single-line box: what is
  pasted is read at once (`Import.Preview`) and shown before anything is
  saved ("Title · by Author · N steps · levels a–b · faction · license"),
  with Import (Update when an older version is imported; nothing for the
  same or a newer one, with a line saying so) and Cancel, or the reason it
  is refused. Enter imports. Below, "Your imported guides" (title, "By
  author · version · license", why a stored guide did not register,
  Delete with a second click to confirm), then "Want more than one guide?"
  (community addons in three steps and the GUIDE_PACKS.md link).
- **Guide menu** (the Guides section): three panes. Left: the categories with
  counts. Middle: Home (continue and suggested), Browse, Current (open tabs),
  Recent tabs over one line per guide,
  `Title (min–max)`, in folders (a guide's `folder` when set, else leveling by
  faction and band, other guides by category); search also matches `folder`
  and `keywords`. The faction filter (`ui.pickerFaction`, default this
  character's) on the player's own faction lists only the guides this
  character can follow (`Guides.GuideMatches`: no other race's or class's
  guides); the other faction and All list every guide. Right: the
  selected guide's zone art, facts (led by a guide's `reward` tiles and
  `why`, "Guide data"), progress, pitch, pros and cons, a favourite
  star, "Load guide" and "Open in new tab". Selecting never loads; the buttons,
  a double-click or Enter do.
  Its search also lists Codex matches under the guides (`UI.LookupRows`,
  `UI.LookupCount`, `UI.LookupTotal` from `UI/Lookup.lua`): an "In the
  Codex" folder with up to three per kind and a "Look up everything" line;
  picking one opens the Lookup section at it.
- **Lookup** (`UI/Lookup.lua`, the hub section "Lookup", order 20;
  `UI.ShowLookup({ query, kind, id })` and `SHOW_LOOKUP(query, kind, id)` open
  the hub at it). Search only, never browsing: the hub's search box drives it
  (`Search.Query`, run when typing pauses for 0.15 s; the section's `search`,
  `enter`, `step` hooks); an empty search lists the recent lookups
  (`CharState().ui.recentLookups`, newest first, at most 10, kept when the
  player opens a page) and a hint. Sidebar (its `nav`): Everything and the
  kinds the search found, with counts; a kind filters the results only.
  Left: the results grouped by kind (five per kind under Everything with a
  "Show all" line; under one kind pages of 50, `opts.offset` for "Show more"),
  one line each (item icon or kind glyph in the gutter, the name in its
  quality colour lifted to 4.5:1 or its subject colour, a muted "No longer
  obtainable" / "No known source" chip for `status`, the subtitle right);
  only the rows in view are drawn, and only they get a subtitle
  (`Search.Subtitle`). Lookup has no tabs, so no tab row: it comes back only
  to say why the Codex is not there to search ("Loading the Codex…"
  re-queries on `CODEX_LOADED`); Enter is named in each row's tooltip. Right: the selection
  on a band (the zone art of its best located source), facts, the ranked
  sources of `Lookup.Sources` (asked once per selection; label, detail,
  travel, chance / price / standing; ones this character cannot use dimmed
  with `whyText`), a "Guide" button on the selected source, and pinned
  "Guide me" ("Open guide" when `Lookup.CuratedGuide` has one) calling
  `Lookup.Guide`; nothing guides to an unobtainable entity. Guide results
  open in a new tab or among the guides; dungeons also open under Dungeons
  ("Open dungeon", `Dungeons.Open(key)`). Without the Codex pack
  (`Search.Status`) it says what is missing and still searches guides.
  Up/Down walk the results, Enter acts, Escape closes the hub.
- **Dungeons** (`UI/Dungeons.lua`, the hub section "Dungeons", order 30;
  `SHOW_DUNGEON(key)` from `Polestar.Dungeons.Open(key)` opens the hub at a
  page, nil at Home). A dungeon quest journal: dungeons grouped by level, a
  page per dungeon (`docs/DESIGN.md`, "Intent"). Sidebar (`nav`): For your
  level, All dungeons, 10–20 … 50–60, Raids, with counts; the faction filter
  (`CharState().ui.dungeonsFaction`, default this character's) as segments at
  the right of the tab row. Home: two cards per line in level order, the zone
  painting centred on the entrance (`List()` carries it; Home never decodes a
  page), name, levels, zone, quests for the filter, Forever and Your level
  chips; only the cards in view get their painting, the rest as they scroll
  in. The hub search filters the cards by name; Enter opens the first. A page:
  the painting in the header band with the title drawn on it (the hub's own
  Title, Subtitle and Band are faded/hidden while a page shows and restored
  on Home and when the section hides), a back chevron and the actions, right
  to left from the band's corner with the primary first, three at most (by
  priority; "Show on map" gives way first). The primary is "Hand in the
  quests" while `Dungeons.HandinPlan` holds a finished quest (`DS.HandIn` ->
  `Dungeons.HandIn`; the tooltip counts them and names what is not finished),
  then "Pick up the quests" while `Dungeons.PickupPlan` holds quests to take
  (`DS.PickUp` -> `Dungeons.PickUp`: the guide opens in a new tab and the hub
  steps aside; the tooltip counts the quests and those in the log), else
  "Guide me to the entrance" (`Dungeons.GuideTo`). After the primary: the
  other of the two guides ("Hand in the quests" is shown while any of the
  page's quests is in the log; with none finished its tooltip and, clicked,
  chat say what is open, `Dungeons.HandinText`), "Guide me to the entrance"
  and "Show on map" (`Dungeons.ShowOnMap`); with nothing to pick up or hand
  in, "Nothing to pick up" (shown while the page has quests for this side)
  says why in its tooltip and, clicked, in chat (`DS.NothingText`); no guide
  is built. Tabs Quests n, Attunement (Keys when each is a key; only on pages
  with one for the filter's side), Bosses n, Map. Quests: a block per quest planned from the
  data (the objective measured on one shared string) so only the blocks in
  view are drawn (`DS.PaintQuests` on scroll): the title in the client's
  difficulty colour (`GetQuestDifficultyColor`, else `QuestDifficultyColors`
  by level gap) lifted to 4.5:1, the state from `Compat.IsQuestCompleted`,
  `IsOnQuest`, `IsQuestReadyForTurnIn` and the level, level, requirement,
  classes, picked up inside, shareable; Objective, Starts/Ends with pins
  (`Dungeons.GuideToPoint`); the Starts line (pin and text) is one link to
  `Dungeons.GiverTarget`: the giver of the first prerequisite still to do
  (its turn-in when that one is in the log), else the quest's giver, its
  tooltip saying which; the Chain (`Dungeons.Quest` per id, a pin each),
  Choose one / You get item tiles (quality border; hover `GameTooltip:
  SetHyperlink`, where Gear adds its upgrade line; modified clicks through
  `HandleModifiedItemClick`), XP, money and reputation chips. Bosses: kill
  order, name, level, each tied quest ("Kill it for", "Drops X for", "Drops X,
  which starts", "Part of"), a line opening the quest. Attunement
  (`Dungeons.Attunements`, `Dungeons.AttunementState`): per entry a head
  (state glyph, the name in quest or item colour, a chip: Done, "Not in your
  bags" for a key whose chain is done, "n of m done"; the facts: kind, who
  needs it, level, side; our note; the item tile with "You have it" or "Not
  in your bags or bank"; "Guide me through it" while something is left, which
  opens `Dungeons.Attune` in a new tab as the pick-up guide does) and a row per
  step of the filter's side (state glyph, the quest in its difficulty colour
  or what drops it, its state, giver and zone or chance and instance; a click
  sets the arrow there); the other side's entries carry no state. Map: the entrance and
  the quest givers on its zone painting. Blocks, rows, tiles, chips and cards
  are pooled; quest states refresh on QUEST_ACCEPTED, QUEST_REMOVED,
  QUEST_TURNED_IN and PLAYER_LEVEL_UP, the Attunement tab also on
  BAG_UPDATE_DELAYED and UPDATE_FACTION. `UI.DungeonsSection` exposes
  `Open`, `Home`, `SetBand`, `SetTab`, `SetFaction`, `ShowQuest`, `Attune`, `Shown`
  (`shown.attune`: per entry its id, done, chip, guide, owned and step states)
  for tests and the preview.
- **Zone art** (`UI/ZoneArt.lua`): `UI.ZoneArt.Create(parent, width, height
  [, fadeColor])` and `UI.ZoneArt.Paint(art, uiMapID, focusX, focusY, index) ->
  painted, reveal`. The zone's world map painting from the player's own
  client, drawn the way the world map draws it: the base layer
  (`C_Map.GetMapArtLayers` / `GetMapArtLayerTextures`, layer 1, tiles
  row-major), then every overlay over it, so the zone shows whole, without
  fog of war. The client only lists the overlays this character has explored
  (`C_MapExplorationInfo.GetExploredMapTextures`), so the full list is
  generated: `UI/ZoneArtOverlays.lua` (`maps overlays`, below) registers
  `ZoneArt.RegisterOverlays(build, { [uiMapArtID] = { tileWidth, tileHeight,
  { textureWidth, textureHeight, offsetX, offsetY, fileDataID, ... }, ... } })`.
  It is drawn only when `C_Map.GetMapArtID(uiMapID)` is the art it was built
  for (and the tile size matches); otherwise the explored overlays are
  (`reveal` "explored", "full" with the table). Overlays split into tiles,
  the last column and row cropped from a power-of-two file, exactly as
  `MapExplorationDataProvider` places them, at the same desaturation, above
  the base. The place (`focusX/Y`, live-frame percent) lands mid-band
  horizontally and vertically mid-way between the top shade and the bottom
  fade (`art.psFocusY`), where nothing covers it; near an edge the painting
  zooms in up to `ZA.REFRAME` times so the place can stay there, and it never
  shows past the painting's edges. Only the base and overlay tiles the band
  shows get a texture, from per-band pools. Used by the route choice cards
  and the menu; the night band stands in without the art.
- **Community guides in the menu.** Our categories (Leveling, Professions, …)
  list only our guides; All, Favourites, Current, Recent and search cover every
  shown guide. A "Community guides" heading in the sidebar (a `W.ShellNav` heading
  row), present only while a shown pack has guides, lists one entry per such
  pack (`pack:<id>`, "<title> · <author>"), its guides grouped as ours are.
  Pack guides carry their pack's title in rows and results; the details pane
  has a credits line for every guide ("By <author> · <pack title> <version> ·
<license>", plus the pack's `source`; ours "By Polestar"). The empty states
name both Polestar Guides and community addons.
- **Community guides page** (`UI/Packs.lua`, Options order 70, `/polestar
packs`): one row per `Guides.Packs()` record: title, credits, guide count,
website as a `W.Link` (the address without its scheme; the click opens
`UI.ShowCopy("<title> website", <address>)`), source, and a `W.Switch` (`Guides.SetPackShown`) or
the status text ("turned off in the AddOns list" with **Enable and reload**,
"not loaded[: <reason>]", "needs Polestar <v> or newer" (or "needs a newer
Polestar"), "broken: registered
no guides", "refused: <reason>"); with problems a warning chip, **Show**
(the list) and **Copy report** (`P.Report`: pack, versions, client build,
every problem). Then **Suggest guides from** (`behavior.suggestFrom`,
`Guides.SuggestSources()`), what a community addon is, and "Read more:" with a
`W.Link` to docs/GUIDE_PACKS.md opening `UI.ShowCopy("Community guides,
explained", <address>)` (`ns.DocsURL()` with `GUIDE_PACKS.md` for
`PACKS.md`: the TOC's `X-Website` + `/blob/HEAD/docs/GUIDE_PACKS.md`, else
"docs/GUIDE_PACKS.md in Polestar's source"). While `Guides.ProblemCount() > 0` the page's sidebar row carries
the warning dot (a page spec's `dot`). Nothing goes to chat.
- **Welcome** (`UI/Welcome.lua`, `SHOW_WELCOME`): centred, four pages under
a starlit hero, with step dots, Skip, Back and Next in the footer: Hello,
<name> (what Polestar does, then the guide window, the arrow and
`/polestar guides`); Set up (quest automation, the guide at login, the
colour scheme as cards that apply live); Route style (`nav.routeStyle`,
the setting Options' Route style dropdown writes, as three cards, Fastest,
Balanced with its Recommended tag and Roads, each saying what it does: a
click sets it live and Nav/Walk.lua searches again, right-click or the
gamepad's Y puts Balanced back, and the pick follows the setting however
it changes; a line on what every style keeps you away from); Start (the guide
`Guides.Suggest()` picks on its zone painting, with its level range and
pitch; the beta note while `ns.routesBeta`; the Discord invite in a copy
box; what community guides are; then Start guide, or Choose a guide without
one, and Browse guides). Once
per new character (`PolestarCharDB.welcomeSeen`: 0 for a character with no
saved `PolestarCharDB` as Polestar loaded, `ns.IsNewCharacter()`; set seen
silently for one with saved data and no flag), 2 s after login, never in
combat, during a cinematic or over the route choice; a due welcome that never opened stays due
for the next login, and one that opened is marked on the character as it
opens (a /reload or logout with it up does not repeat it); one the intro
cinematic closes comes back on its page when the cinematic ends. Set up and Route style are in it only while the account has not had its
welcome (`PolestarDB.welcomeSeen`); the page set is fixed when a showing
starts. Any close marks the account (and the character). About's "Show the
welcome again" and `/polestar welcome` open it with every page.
- **Copy window** (`UI/Copy.lua`, frame `PolestarCopy`, `UI.ShowCopy(title,
text)` or `SHOW_COPY`): a small window with the band, a title, a read-only
`W.CopyBox` holding the text, selected and focused, and how to copy it; ✕
and Escape close it. `/polestar where` uses it. `opts` (`UI.ShowCopy(title,
text, opts)`): `icon` and `iconColor` put a mark before the title, `hint`
replaces the line above the box; the Polestar Discord's invite
(`UI.ShowDiscord`) comes with Discord's mark.
- **Reporting a problem** (`UI/Report.lua`, frame `PolestarReport`,
`UI.ShowReport()` or `SHOW_REPORT`): the guide window's bug (in the
navigator while a guide is open), its menu's "Report a problem" and
`/polestar report` open it on the step on screen. The player types what
went wrong (250 letters); the report is `UI.Report.Text(description,
details, spot)`: their words after "**What went wrong:**", then as bold
lines under it the right spot when one is marked ("**Right spot:**") and
the other addons that are loaded, names only, quest, map and guide addons
first, then A to Z ("**Addons:**", left out when there are none), then
`UI.Report.Details()` in a code block, one fact a line, taken when the
window opens: Polestar's version and stage, the client build and locale;
race, class, level (XP %) and faction (never the name or realm); the guide
(title, id, pack and version); "Step n of N: title", with "the guide is at
step m" while another step is looked at, or "skipped"; each goal (the first
six, then "... k more goals") as `[>]` the arrow's, `[x]` done, `[-]`
skipped, `[n/a]` not for this character or `[ ]` open, its line and count,
`quest`, `npc`, `item`, `object` ids, `map <id> @x,y` as a guide writes it
(`live` unless in the Era frame) and `spot i of n` for the arrow's spawn
spot; the arrow's target and distance (and "your own waypoint" with its
position when the player set it); where the player stands (zone, subzone,
`map <id> @x,y live`); "Routes:" with the route style (`nav.routeStyle`:
`fastest`, `balanced` or `roads`, in English) and only the routing settings
away from their defaults (`real paths off`, `travel planner off`, `handed
to Shortest Path Forever`), after Where so the bug bot still finds Arrow and
Where by position and reads this line by its style word; other packs that
are on; and the errors
(`Polestar.Errors()`, plus with !BugGrabber its errors this session in
Polestar's files, `BugGrabber:GetDB()`) by their path from the AddOns
folder, and the calls the game blocked (`Polestar.Blocked()`) as "blocked
in <addon>: <function> ()": the newest three of both together, by
`seq` (!BugGrabber's count as the newest), each counted. Above the code
block, under the player's words, bold lines for the right spot when one
is marked and the other addons that are loaded (quest, map and guide
addons first, then A to Z). Every line is cut at 100 bytes, and when the
report is still longer than one Discord message (2000 characters) the
Addons line is shortened, then dropped, never a line the bot reads. The
Discord bot (`tools/discord-bot`) takes the code block for the details
and everything above it, the bold lines included, as the description it
quotes in the issue.
"Mark the right spot" hides the window behind a bar (`PolestarMark`) while
the player walks to where the step should have sent them; "Mark here"
keeps `ns.WhereLines()` as the right spot, and "Clear mark" takes it out.
Under it, the way to send the report: Discord's mark, "Report it on
Discord", three numbered lines (press Copy report then Ctrl+C, open #bugs
and press New Post, paste and post: the bot files it), "Copy report",
which shows the report box under it selected, and "Get the invite", which
opens the invite (`Polestar.DiscordURL`) in the copy window. Last, for a
player with a GitHub account, "Open on GitHub" opens in the copy window a
link to a new issue on Polestar's GitHub,
`UI.Report.IssueURL(description, details, spot)`, whose query fills the
issue form's fields by their ids: a "Wrong step" issue (`wrong-step.yml`:
title, guide, step, character, the right spot, the report) when the
player marked a spot, a "Bug report" (`bug-report.yml`) otherwise, both
with the version and the addons. The report in the issue leaves out the
Addons line and, in a Wrong step issue, the Right spot line: each has its
own field. The whole link stays under `UI.Report.LINK_MAX` (GitHub
refuses one much longer): the report loses goals after the arrow's, then
is cut, and the addons field lists as many as fit after a line that
counts them. Copy report and Open on GitHub wait until the description
holds `UI.Report.DESC_MIN` characters. The game opens no links and has no
clipboard for addons, so every text to take away is selected for Ctrl+C.
A gamepad starts on "Copy report", or on "Mark the right spot" while it
waits. The details, the issue's fields and the right spot are taken when
the window opens or the spot is marked, so the guide moving on changes
neither. What the player typed and the right spot stay while the window
is reopened on the same step. The hub's and the settings' sidebars carry
the Discord's mark at the end of the version line
(`UI.Report.SidebarLink(shell)`), which opens the invite like
`/polestar discord` (`UI.ShowDiscord`, `SHOW_DISCORD`).
- **Per-character UI state** in `CharState().ui`: `recentGuides` (ids, newest
first, at most 10), `favoriteGuides` (id -> true), `pickerSelected` (id).

## Journey panel

`UI/JourneyPanel.lua` (`UI.JourneyPanel`; adapted from Shortest Path
Forever's journey tracker with Cillian Berragan's permission) shows the
travel route's steps in the guide window, right under the focus they lead
to and above "Then:": a header ("Journey", the remaining time and distance,
a chevron; a click collapses it, `nav.journeyOpen`), "To <destination>"
(the target's label, in the place colour), then one row per leg from the
current one on (at most eight, then "3 more steps"): the leg's number in
the route, its glyph (by `leg.kind`, or `leg.step` for a walk's chokepoint:
lift, gate, bridge, cave, pass), its caption (`leg.label`; without one the
kind's words and where it ends, never "Unknown") and its time. Boats,
zeppelins, the tram and lifts add a line, "average wait 0:15 · ride 0:10"
("leaves in" once a ride timed the loop; "on board · 0:42" while riding).
A walk whose engine path enters the other faction's towns (`leg.hubs`) adds
"Hostile town: Southshore" (names joined by commas) in the warning colour.
The current row wears the accent glyph and the 2px selection bar; a click
on it opens the skip menu (`Nav.prompts.OpenMenu`).

Data: `Nav.GetRoute()` (legs, current index), `Nav.travel.Active()` (more
than one leg: a lone walk is the arrow's own distance and time, so the panel
stays away), `Nav.travel.Progress()` (a current walk's seconds and yards
left along its path; any other current leg, a flight, hearth or teleport,
counts its planned time down from when it became current, stamped on every
route event even while the window is closed), `leg.seconds`, `leg.yards` and
`ns.Transit.NextDeparture(kind, id, at, side)` asked at the time the route
reaches the dock; standing at the dock (60 yd), `Transit.NextArrival` too,
so a visit leaving within the boarding window shows its own wait rather than
the next loop. A departure line appears only where a departure is known.
Times are whole seconds rounded up, unreadable numbers count 0, and the
header adds up the rows. Steps are repainted on `ROUTE_CHANGED`,
`ROUTE_ADVANCED` and settings; a 1 s `C_Timer` ticker, running only while
the panel is on screen, changes the times alone (`SetFormattedText`), so the
window is laid out on a tick only when a departure line comes or goes.
`GuideFrame.Layout` places it (`JourneyPanel.Layout`
returns its height); never in compact mode. Off with `nav.journeySteps`
(Options, Arrow & routes, Routes, "Show the journey"); without Nav neither
setting exists. Non-secure frames only. Tests: `tests/ui-journey.test.js`;
render: `docs/preview/journey.svg`.

## Gamepad (`UI/Gamepad.lua`, `UI/GamepadGuide.lua`)

Ported from the translated names database (@moust4ki), GPL-3.0, and rebuilt on the Theme.
No Polestar window takes a button from the game while it is merely shown:
a window holds the gamepad only while it is the newest one open, out of
combat, with a gamepad active (the game's gamepad interface, or
`gamepad.withMouse` on with the keyboard and mouse) and `gamepad.navigate` on (focus mode needs
only a gamepad), through a frame that reads the buttons first
(`PolestarPadInput`, `OnGamePadButtonDown`), so neither the game's gamepad
interface nor another addon's overrides (Easy Controller, ConsolePort) go
before it; any other button goes on to the game. With the keyboard and
mouse interface, temporary override bindings (`SetOverrideBindingClick` on
the hidden buttons `PolestarPad<KEY>`) sit under the frame for a client where
the frame gets nothing. With Forever's gamepad interface on
(`Polestar.GamepadUI()`, see [Gamepad interface](https://polestar.guide/docs/contributors/architecture/core#gamepad-interface)) none
is set: the game's own bindings on those buttons stay as its binding stack
left them, and the frame alone reads the window's buttons. Both are cleared
when the window closes. At `PLAYER_REGEN_DISABLED` (before the lockdown:
nothing is set in a fight) the window lets go first, then a binding row's
capture ends, each step in its own `pcall`, so an error in a row's callback
cannot leave the frame shown or a binding set for the fight; until
`PLAYER_REGEN_ENABLED` no window takes the gamepad, then the newest open one
takes it again. A button already down when a window takes the gamepad stays
the game's until released (`G.heldKeys`); a press that opened the window
stays Polestar's.

A window that shows by itself waits to be asked: the welcome at login (or
back after a fight or the route choice) and the route choice the engine
offers (`spec.unasked()`) take no button, unless a Polestar window held the
gamepad as they came. The focus binding (`POLESTAR_FOCUS`) gives the newest
waiting window the gamepad (`G.TakeWaiting`) before it starts focus mode,
and the waiting window's hint bar shows that binding's button when it has
one. Opened by the player (a binding, a chord, a click, A in another
window), every window takes the gamepad at once.

```lua
Polestar.Gamepad.Register(frame, spec)  -- navigation while `frame` is shown (the contract every window uses)
-- spec = { unasked = fn -> bool (this showing came by itself: wait for the focus binding),
--          rows = fn -> { widget, ... } (default G.Collect(frame): every shown, enabled, clickable
--          widget, the outermost one), initial = fn(rows) -> index, back = fn (B, Start; default Hide),
--          prevTab / nextTab = fn (LB / RB), reset = fn(w) -> handled (Y; default: a setting row's
--          right-click), x = fn(w) (X), activate = fn(w) -> handled (A; default w:Click()),
--          key = fn(key, w, isRepeat) -> handled, onFocus = fn(w),
--          close = button | false (where B's chip sits; default frame.Close),
--          actionLabel / xLabel / resetLabel = fn(w) -> text, backLabel, tabsLabel = text }
Polestar.Gamepad.IsActive() -> bool        -- Polestar.GamepadUI(); with the keyboard and mouse only with gamepad.withMouse on (off by default) and C_GamePad / GamePadEnable with a device plugged in
Polestar.Gamepad.Style() -> "xbox" | "playstation"  -- gamepad.glyphs, "auto" reads C_GamePad's labelStyle ("Shapes")
Polestar.Gamepad.ButtonName(button), KeyName(key) -> text   -- "A" / "Cross", "LT + A", "LB + D-pad up", "Ctrl + N"
Polestar.Gamepad.Glyph(parent[, name]) -> frame             -- frame:SetKeys(key or { button, ... }) -> width
Polestar.Gamepad.BindingKeys(action), ShownKeys(action[, pad]), KeyText(action), Bind(action, key), Unbind(action[, pad])
Polestar.Gamepad.WhoUses(action, key) -> { kind = "binding"|"addon"|"gamepad", name, action } | nil
Polestar.Gamepad.Capture(opts)                              -- a key, a button or a chord for a binding row
Polestar.Gamepad.StartFocus(), StopFocus(), ToggleFocus(), IsFocusing()   -- the guide window's focus mode
Polestar.Gamepad.TakeWaiting() -> bool                      -- the newest window waiting to be asked takes the gamepad
Polestar.Gamepad.HasWindow() -> bool                        -- a Polestar window holds the gamepad (for other gamepad addons)
```

- **Navigation.** Where widgets have places (a client; the test and preview
  stubs place none) the D-pad goes to the nearest widget that way (its
  distance plus twice how far it sits off the line), scrolling a list on at
  its edge; without, it walks `rows` in order. Left / right step a slider
  (`row.Slider`'s wheel handler) or a dropdown's choices instead. The D-pad
  repeats while held (0.4 s, then every 0.12 s). Until the player presses a
  button the focus follows `spec.initial` (a window fills its page after it
  shows); LB / RB start the new page on it again. Registered: the options
  (LB / RB: `Options.StepTab`, every page's tabs in order), the hub (LB / RB:
  `Picker.StepSection`, the rail's sections; X loads the focused guide), the
  shared menu (`PolestarMenu`, B closes it and the window under it has the
  gamepad back where it was), the welcome (LB / RB its pages), the route
  choice, the report and copy windows.
- **The look** (docs/DESIGN.md, "Gamepad"): one focus ring (`PolestarPadRing`,
  the focused widget's child: the hover wash under a 2px accent ring at 90%),
  one hint bar under the window holding the gamepad (`PolestarPadHints`, the
  panel skin, clamped to the screen, wrapping to the window's width, 300 at
  least) and one B chip left of its close button (`PolestarPadBadge`; the
  bar then leaves B out: each action shown once). The bar lists the D-pad,
  what A, X and Y do on the focused widget, LB / RB and B. `gamepad.hints`
  turns the bar and the chip off. Without a gamepad none of it exists.
- **Chips** (`G.Glyph`): a 20px circle per face button (Xbox's letter in
  Caption, PlayStation's shape: `pad-cross`, `pad-circle`, `pad-square`,
  `pad-triangle` in `Media/UI`) and D-pad direction (the chevrons), a capsule
  for the rest ("LB", "Ctrl"), "+" between the buttons of a combination; the
  chip wash, the name in text.
- **Focus mode** (`POLESTAR_FOCUS`, `TOGGLE_FOCUS`; a window waiting to be
  asked takes the gamepad first): `G.Open` of the guide
  window with its own spec: rows are its widgets (tabs, navigator, banner,
  person, goal lines, tip, up next), the first open goal first; A on a goal
  line aims the arrow (`rowClick`), or on the quest item's line (the item
  button `QoL.Buttons.item` shows the goal's `item`, or a `use` goal) is
  redirected to the secure binding `CLICK PolestarQuestItemButton`
  (`G.Redirect`, an override binding: not with the gamepad interface, where
  A clicks that line like any other); X runs the line's `OnEnter` (its
  tooltip) and again its `OnLeave`; LB / RB `Engine.Prev/Next`; Y sets
  `ui.compact` and ends the mode; B ends it; hiding the window or
  `GAMEPAD_COMBAT` ends it.
- **Bindings and chords.** Bindings.xml: `POLESTAR_TOGGLE`, `_NEXT`,
  `_PREV`, `_FOCUS`, `_MENU` (`TOGGLE_PICKER`), `_OPTIONS`
  (`TOGGLE_OPTIONS`), `_REPORT`, and the quest buttons' `CLICK` bindings;
  `Polestar.Bindings` runs them. Nothing is bound unasked. A chord
  (`"PADLSHOULDER+PADDUP"`) is Polestar's own, in
  `PolestarDB.gamepad.chords[action]` (account-wide, outside the settings,
  so "Reset all settings" keeps it), never bound: while chords exist and a
  gamepad is active, the driver reads their first buttons (`IsKeyDown`,
  the chords parsed once per `GAMEPAD_BINDINGS_CHANGED`) every 0.05 s, and
  every frame while one is held; then, out of combat, `PolestarPadChords`
  reads the gamepad first and keeps the second button from the game; any
  other button goes on. A chord does nothing in a fight (`chordsFree`, and
  `runChord` itself: its window could not take the gamepad, and the button
  reached the game as well), while a Polestar window holds the gamepad, a
  binding row waits, the chat has the keyboard or a game window the
  gamepad's focus (`GamepadMode.FrameControlsManager.focusedFrame`, the
  field read, never a call), nor with another shoulder or trigger held
  (LB + RB + Up is not RB + Up). With the gamepad interface, LB and RB are
  its targeting modifiers (held, the D-pad and face buttons pick, mark and
  assist; both held, the D-pad pages the bars and opens the quest tracker
  or the chat): `G.WhoUses` names any chord on them as the game's. The
  quest buttons' secure clicks take no chord: a combination is the game's,
  with a button it makes a modifier (`GamePadEmulateShift` / `Ctrl` /
  `Alt`). Polestar never changes the game's gamepad settings.
- **Binding rows**: the registry's `binding` control (`G.BindingRow`). A (a
  click) captures (`G.Capture`): a gamepad button on its release, two
  buttons as a chord, a keyboard key with its modifiers; B tapped cancels, B
  held a second binds B, Escape cancels, so do a fight and 20 s idle. A key
  someone else has (`G.WhoUses`: another binding or chord, another addon's
  override, Easy Controller's own chord read from its saved settings, the
  game's gamepad interface) is named in danger and bound by a second A within
  5 s. Y (right-click) unbinds what the row shows: the gamepad's keys while a
  gamepad is active, else the keyboard's.
- Messages: `GAMEPAD_BINDINGS_CHANGED`, `GAMEPAD_CHANGED(active)`
  (`GAME_PAD_CONNECTED` / `DISCONNECTED` / `ACTIVE_CHANGED`),
  `GAMEPAD_COMBAT` (the gamepad went back at `PLAYER_REGEN_DISABLED`).

---

Next page: [Rares](https://polestar.guide/docs/contributors/architecture/rares.md) · Every docs page: https://polestar.guide/llms.txt · All docs in one file: https://polestar.guide/llms-full.txt
