fl_panel 1.1.0
fl_panel: ^1.1.0 copied to clipboard
A docking layout for Flutter: a tree of splits, panels and tab groups you can resize, dock and persist, with ordinary Flutter widgets inside every panel.
1.1.0 #
The host's own words can be translated, and its menus can be read without reading English. Additive: a host that changes nothing sees exactly what 1.0.0 showed.
Added #
PanelLocalizations, the words the host draws — its menu labels — in the pattern of Flutter'sMaterialLocalizations.PanelLocalizations.of(context)answers the one a delegate in scope provides, orDefaultPanelLocalizations(English) when none does, so nothing has to be set up. A host translates by extending the default and listing a delegate for it inlocalizationsDelegates; extending rather than implementing means a string added later shows in English instead of breaking the build. Strings the host already sets, such as tab titles throughtitleOf, are not in it. Pinned by every default label comes from the localizations, and keeps its id and says what the delegate in scope says intest/localizations_test.dart.DefaultPanelLocalizations.delegate, which answers the English for every locale, for a host that lists its delegates explicitly.PanelMenuRequest.localizations(const DefaultPanelLocalizations()).PanelMenus.defaultEntriesstays pure and reads its labels from the request, so a test can still assert a menu without pumping one;PanelHostfills it fromPanelLocalizations.of.PanelMenuEntry.id(null) andPanelMenuEntryId, one per built-in entry, submenu sides included. A builder that keeps, drops or relabels a default matched its English label until now, and would have stopped matching the day the label was translated; it matches the id instead, andcopyWithkeeps it. Pinned by a builder finds a default by id, whatever it says intest/localizations_test.dart.
1.0.0 #
The API is stable from here. Nothing about the file format changed — the version is still 1 and a layout saved by 0.1.0 loads as it was — so what follows is names, and four things a host could not do.
Renamed #
PanelAppisPanelWorkspace. The*Appname read as a widget at the root of an application, likeMaterialApp, and this is neither: it is every window of the layout. Everything that said "app" follows —PanelController(workspace:),controller.workspace,replaceWorkspace,PanelJson.encodeWorkspace/decodeWorkspace.PanelTab.formsisallowedForms, beside theallows(form)that already answered from it. The JSON key is stillforms.DockPolicy.takesFocusiscanBeFocusedLeaf(true). It never meant Flutter's keyboard focus — the content of a leaf answering false still takes the keyboard — and now says what it decides in the package's own words:focusedLeafId,focusedLeaf,focusLeaf.
| 0.1.0 | 1.0.0 |
|---|---|
PanelApp(windows: …) |
PanelWorkspace(windows: …) |
PanelController(app: …), controller.app |
PanelController(workspace: …), controller.workspace |
controller.replaceApp(…) |
controller.replaceWorkspace(…) |
PanelJson.encodeApp/decodeApp |
PanelJson.encodeWorkspace/decodeWorkspace |
PanelTab(forms: …), tab.forms |
PanelTab(allowedForms: …), tab.allowedForms |
bool takesFocus(LeafNode leaf) |
bool canBeFocusedLeaf(LeafNode leaf) |
PanelDecorations(onTabSecondaryTap: …) |
PanelMenus(build: …) returning [], see below |
DockResolver(edgeBand: …, centreFraction: …) |
DockResolver(zones: DockZones(…)) |
Added #
PanelTab.metadataValue<T>(key): the value as aT, or null when it is absent or something else. Metadata comes back from a file, so a wrong type is a stale or hand-edited layout and reads as missing rather than throwing under a host. Anintis accepted as adouble, because1.0comes back as1from anything that is not the Dart VM.PanelHost.defaultTitlereads through it. metadata reads typed intest/model_test.dart.PanelTheme.copyWith, so a host overrides one field of a theme it was handed;clearTabStyleSpec(false) is how a spec is taken away, andwithTabStyleis that. switching style drops a spec made for the old one intest/chrome_test.dart.DockZones(edgeBand24,centreFraction0.5) andPanelController.dockZones. The two numbers were fields onDockResolver, which the controller built with the defaults, so no host could change them — and a touch screen wants wider bands than a mouse. the drop zones a host sets are the ones a drag resolves with intest/controller_test.dart.
Removed #
PanelDecorations.onTabSecondaryTap. It predates the context menus and survived so a host with its own chip menu did not suddenly show two. That host now returns no entries fromPanelMenus.buildand opens its own atPanelMenuRequest.globalPosition— the same way on a strip, a header and a divider as on a chip, which the decoration never covered. a right click reaches the menu builder with the position intest/chrome_test.dart.TabSlotandStripSlotfrom the barrel.StripScope.tabSlotandstripSlotare how a chrome applies them and nothing else constructs one.
0.1.0 #
The first cut: the tree, the solver, the controller, the host.
PanelApp→PanelWindow→LayoutNode→PanelTab: an immutable tree of splits, single panels and tab groups, edited by the pure functions inLayoutTree, which normalises after every edit so no caller sees a split of one, an empty group or a same-axis nesting. Splits are n-ary and flatten same-axis children rather than binary, so((A|B)|C)and(A|(B|C))— which draw the same and resize differently — are one tree. A tab holds acontentIdand metadata, never a widget, so the tree serialises and moves between windows without asking anybody.test/model_test.dart.PanelSolver:PanelExtent.flex(weight)children share whatPanelExtent.fixed(pixels)ones leave;PanelTab.minWidth/minHeightare the only hard constraint, held by taking from siblings with slack, scaled together when even they do not fit.resizetrades pixels between two neighbours and nobody else moves.test/layout_test.dart.DockResolver: five zones over a leaf (centre joins, edges split), a band along the window's edges splits the root, a strip hit inserts at the index. Every candidate is checked by running the edit, so a drop the policy refuses is never offered.DockPolicycarries the two affinity levels:canTakeFormfromPanelTab.forms,canJoin/canSplitfor the host's metadata rules. Default permissive. affinity intest/model_test.dart.PanelController: aChangeNotifierover the app;dock,open,close,activate,resize,moveToWindow; the drag session (beginDrag,updateDrag,commitDrag);toJson/loadwith an integerversion(PanelApp.version, 1) refused when newer and aTabResolverthat drops what this build cannot show.onSettledfires once per gesture, never per frame.PanelHost: one window as one flatStack, a keyedPositionedper tab, chrome above, preview on top — content is never reparented, so its state survives a move. Strips and headers drag; dividers resize withDragStartBehavior.downso they track the pointer from the first pixel.test/panel_host_test.dart.PanelChrome: the contract for everything drawn that is not content, withStripScope.tabSlot/stripSlotas the one rule custom chrome keeps to stay a drop target.DefaultPanelChromedraws threePanelTabStyles —attached,blended(aBlendedChipPainterwith Chrome's shoulders, the bottom left open so the active tab and its content are one surface),floating— from aPanelTabStyleSpecper style, per theme or per group throughPanelHost.tabStyleOf.PanelDecorationshangs an icon, an unsaved dot, a strip button and a context menu on the default chrome. styles, decorations and custom chrome intest/chrome_test.dart.- Chips share a strip between
PanelTheme.tabMinWidth(96) andtabMaxWidth(200) and the strip scrolls once they would shrink below the floor — by wheel, never by pointer drag, which already means "move". shrink then scroll intest/chrome_test.dart. PanelWindow.focusedLeafId, persisted: whereopenputs content (falling back to the first leaf that accepts, then the right edge) and whatnextTab/previousTab/closeActiveact on; the focused group's strip shows the full accent.PanelController.focus(tabId, keyboard:)activates, reveals the chip in a scrolled strip and, withkeyboard, hands the content the focus.test/controller_test.dartfocus,test/chrome_test.dartfocus reveals a clipped chip.PanelController.events, synchronous:TabOpened,TabClosed,TabMovedfrom one placement diff per commit, plusTabActivated,LeafFocused,TabUpdated,LayoutReplaced.closeGuardasked before any close, from the × or a verb;closeOthers,closeToTheRight,closeLeafask per tab.updateTabfor a title or a flag.test/controller_test.dart.TabGroup.persistent(false): a group that stays in the tree when its last tab closes — an IDE's editor area, where documents come and go but the place they open into is part of the layout. The rule is window-wide: an empty persistent group is removed as soon as the tree keeps another persistent group, so only the last editor area ever stands empty, and a tab dragged out of a persistent group makes a persistent group, so an editor area split in two is two editor areas that fold back into one as they empty.removeTabempties rather than removes,joinkeeps the flag, and an empty one cannot be dragged: its strip is a drop target, not a handle.LeafNode.activeTabis nullable for it, andPanelHost.emptyLeafBuilderdraws what goes where its content would. The file format carriespersistent. a persistent group survives its last tab, an emptied editor area folds away while another remains and a tab dragged out of an editor area makes another editor area intest/model_test.dart; an empty persistent group draws its placeholder and takes a drop intest/panel_host_test.dart.- A removed child hands its room to the sibling before it — after it when
it was first — rather than to every sibling in proportion, so closing the
right-hand editor group widens the group beside it and does not nudge the
file tree along too; a fixed neighbour keeps its pixels and the room goes
to the nearest flex one. In
LayoutTree.replace. a removed child hands its room to its neighbour intest/model_test.dart. DockPolicy.takesFocus(leaf)(true): which leaves may be the window's focused leaf — whereopenputs content, what the keyboard verbs act on, whose strip shows the full accent. An IDE answers no for its tool panels, so a click in the file tree never makes the tree where the next document opens, and the accent stays on the editor group it will open into.PanelController.focusedLeaffalls back to the first leaf that does. focus policy intest/controller_test.dart.- Context menus, in the shape of
fl_nodes_v2's:PanelHost.contextMenus(const PanelMenus(); null for none) opens a Material menu on a right click on a chip, a strip's background, a single panel's header or a divider. The entries arePanelMenuEntrydata built byPanelMenus.defaultEntriesfrom the controller's verbs — close verbs and a Split submenu on a chip, Close all and Move to edge on a strip or a header, Equalise and Swap on a divider, each greyed when it would not apply — andPanelMenus.buildrewrites them with the defaults in hand. APanelDecorations.onTabSecondaryTapkeeps winning on chips. Two verbs came with them:PanelController.equaliseandswap, overLayoutTree.equalise/swap, andcanDockas the dry run a menu greys by; a leaf moved to the window edge it already sits on is a no-op.test/menus_test.dart. PanelTab.closable(true): off for content an application always shows. The chrome draws no close glyph on its chip or header, andPanelController.closerefuses without asking the guard, so the group verbs skip it andcloseLeafkeeps its leaf; it still moves. In the file format asclosable. closable intest/controller_test.dart, an unclosable tab draws no glyph intest/chrome_test.dart.PanelDecorations.wrapTab: a wrapper around the whole chip, inside the drop slot — a tooltip, a spotlight target — because a host with a tour needs to point at a tab and neithertabLeadingnortabTrailingis the tab. Same test as above.package:fl_panel/model.dartexports the Flutter-free half;test/flutter_free_test.dartkeeps it so.example/: an IDE-shaped demo with a metadata policy and save/restore.