Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ concurrency:

jobs:
native:
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.9.0
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.10.0
permissions:
contents: write
pull-requests: write
Expand Down
21 changes: 15 additions & 6 deletions claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ the comment there about not caching "nothing staged" asks for.
by which colour object, so a draw that changes little lays out little. An enlarged picture below
its own size is drawn from a copy at that size (`reduced`), made on the work queue in the fitted
copy's slot, except in a capture, past its own size, or when both panes name one picture. None
of this head can be compiled or run from Windows: CI's `macos-14` job is the first build, and
of this head can be compiled or run from Windows: the macOS leg of CI's `unix` job is the first build, and
its OSX baselines come from that job's `received-*` artifacts. That job only captures, so it
exercises none of the clip test and none of `reduced`. A picture or a scaled copy that lands
during a draw clipped to less than the window is owed a whole redraw, which `takeFinished`
Expand Down Expand Up @@ -621,7 +621,7 @@ the comment there about not caching "nothing staged" asks for.
`native/CMakeLists.txt`, `PlatformHeads`, `PlatformHeads.Core`, `PlatformHeads.Linux` (the
Linux tray's) and `PlatformHeads.WindowsForms` in `Directory.Packages.props`, which move
together, and the `PlatformHeads` package
in `native/swift/Package.swift`, all at 0.9.0. So is the native runtime under both heads' drawing: the C++
in `native/swift/Package.swift`, all at 0.10.0. So is the native runtime under both heads' drawing: the C++
one `deview.cpp` calls as `ph::` (the window, GLFW's input as it happened, which frames are
built and drawn, the renderer, the fonts and fontconfig, and the label helpers) and the Swift
`PlatformHeadsMac` the AppKit head imports (`HeadWindow`, `EventPump`, `TerminateGuard`,
Expand Down Expand Up @@ -692,9 +692,18 @@ the comment there about not caching "nothing staged" asks for.
`TrayMenuItem`s built from what the tracker holds, an action for each by id, and tips kept beside
the items since a Linux tray's menu cannot show one. Ids name the entry acted on by its key, so a
click from a menu built before a change acts on that entry or on nothing. It comes in two halves,
`Fixed` and `Tracked`, because the head's `MenuBuilder` keeps the fixed items and builds the
tracked ones on every open, making each a ToolStrip item with PlatformHeads' `TrayMenuItems`,
and setting the tips and the submenus' direction through its options' `Style`. What only a head
`Fixed` and `Tracked`. Every head shows it the same way, by `TrayTurns`: a model of both halves
handed to the tray every tenth of a second, and a click done on the menus it was chosen from.
The Windows head's tray is PlatformHeads.WindowsForms' `NotifyIconTray`, which makes the menu
from the last model as it opens, and its turns run from a WinForms timer on the message loop,
where the icon's events are raised. It used to build the tracked half itself on every open, and
a menu can now be a turn behind the tracker, which the ids make safe. `MenuBuilder` is only
how it looks: the options the tray is made with, the tips and the submenus' direction through
`Style`, the font and the direction through `Theme`, and either button opening it
(`LeftClickOpensMenu`). Hot keys are registered against the icon's window
(`NotifyIconTray.Handle`), and its balloons are `TrayNotificationKind.Warning`. `MenuBuilderTest`
shows the menu through `MenuTray`, which makes it as `NotifyIconTray` does with no icon in the
notification area. What only a head
can do (its windows, exit) is handed in as `TrayActions`, with the core's `DiffToolLauncher` for
a move's diff tool, which brings the tool forward only on Windows and elsewhere ends it, where it
may, and starts it again. Two seams
Expand Down Expand Up @@ -902,7 +911,7 @@ the comment there about not caching "nothing staged" asks for.
`async Task Main` lands on a method the runtime does not start, and the thread came up MTA.
- `SessionEndWindow`, PlatformHeads.WindowsForms', is a hidden top level window that hears
`WM_ENDSESSION`, which neither the notify icon nor a message filter does. So are
`MenuOnOpening`, which builds the menu's tracked items on every open beside its fixed ones and
`NotifyIconTray`, and `MenuOnOpening` under it, which builds the menu's items on every open and
disposes them at the next, and `IssueLink` and `LinkOpener` under `IssueLauncher` and
`LinkLauncher`: BuildMonitor's tray shares them, and a fix to any of them goes there. An owning tray stages its queue from inside the message
(`OwnedInlineHost.SessionEnding`) and refuses patches from then on, because a logoff never comes
Expand Down
2 changes: 1 addition & 1 deletion native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ include(FetchContent)
FetchContent_Declare(
platformheads
GIT_REPOSITORY https://github.com/SimonCropp/PlatformHeads.git
GIT_TAG 0.9.0
GIT_TAG 0.10.0
GIT_SHALLOW ON)
FetchContent_MakeAvailable(platformheads)
include(${platformheads_SOURCE_DIR}/cmake/PlatformHeads.cmake)
Expand Down
2 changes: 1 addition & 1 deletion native/swift/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let package = Package(
dependencies: [
// What every AppKit head does underneath its drawing, shared with BuildMonitor. The sixth
// pin of PlatformHeads, moved with the other five.
.package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.9.0")
.package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.10.0")
],
targets: [
// Exists only to import the ABI. Swift does not guarantee struct layout, so the structs
Expand Down
74 changes: 15 additions & 59 deletions src/DiffEngineTray.Core/TrayLoop.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
/// <summary>
/// The loop of a head whose tray is a model and no window: the menu built from the tracker and
/// handed to the tray, and what was chosen on it done, on the loop's own thread.
/// The loop of a head whose tray is a model: the menu built from the tracker and handed to the
/// tray, and what was chosen on it done, on the loop's own thread.
/// <para>
/// Built every turn rather than as the menu opens, which is how the Windows head builds it. A
/// StatusNotifierItem's menu is the panel's to show, and the panel's question before it shows one,
/// dbusmenu's AboutToShow, PlatformHeads answers itself with "nothing changed" and tells the app
/// nothing, so the tray has to hold the current layout before anyone opens it. A turn that changes
/// nothing hands the tray a model equal to the one it holds, which it sends nowhere.
/// Built every turn rather than as the menu opens. A StatusNotifierItem's menu is the panel's to
/// show, and the panel's question before it shows one, dbusmenu's AboutToShow, PlatformHeads
/// answers itself with "nothing changed" and tells the app nothing, so the tray has to hold the
/// current layout before anyone opens it. A turn that changes nothing hands the tray a model equal
/// to the one it holds, which it sends nowhere. The Windows head built its menu as it opened, and
/// takes the same turns now, from a timer on its message loop (<see cref="TrayTurns" />), so a
/// menu can be a turn behind the tracker, which the ids make safe: see <see cref="TrayTurns.Turn" />.
/// </para>
/// </summary>
static class TrayLoop
{
/// <summary>
/// How long a click waits to be done, and how stale the menu can be.
/// </summary>
static readonly TimeSpan turn = TimeSpan.FromMilliseconds(100);
public static readonly TimeSpan Turn = TimeSpan.FromMilliseconds(100);

/// <param name="tray">
/// Null where there is nowhere to show the menu, as on a GNOME without the AppIndicator
Expand All @@ -29,11 +31,11 @@ public static async Task Run(ITray<TrayIcon>? tray, Tracker tracker, TrayActions
return;
}

var shown = Show(tray, tracker, head, icon);
var turns = new TrayTurns(tray, tracker, head, icon);
while (!cancel.IsCancellationRequested)
{
await Wait(turn, cancel);
shown = Turn(tray, shown, tracker, head, icon);
await Wait(Turn, cancel);
turns.Turn();
}
}

Expand All @@ -45,57 +47,11 @@ public static async Task Run(ITray<TrayIcon>? tray, Tracker tracker, TrayActions
/// </summary>
public static void RunPumped(ITray<TrayIcon> tray, Action pump, Tracker tracker, TrayActions head, Func<TrayIcon> icon, Cancel cancel)
{
var shown = Show(tray, tracker, head, icon);
var turns = new TrayTurns(tray, tracker, head, icon);
while (!cancel.IsCancellationRequested)
{
pump();
shown = Turn(tray, shown, tracker, head, icon);
}
}

/// <summary>
/// What was chosen on the menus last shown done, and the menus shown again.
/// </summary>
static (TrayMenu Fixed, TrayMenu Tracked) Turn(
ITray<TrayIcon> tray,
(TrayMenu Fixed, TrayMenu Tracked) shown,
Tracker tracker,
TrayActions head,
Func<TrayIcon> icon)
{
if (tray.Poll().ClickedItem is { } id)
{
Invoke(shown.Fixed, shown.Tracked, id);
}

return Show(tray, tracker, head, icon);
}

static (TrayMenu Fixed, TrayMenu Tracked) Show(ITray<TrayIcon> tray, Tracker tracker, TrayActions head, Func<TrayIcon> icon)
{
var fixedItems = TrayMenu.Fixed(head);
var tracked = TrayMenu.Tracked(tracker, head);
tray.Apply(new(icon(), "DiffEngineTray", [.. fixedItems.Items, .. tracked.Items]));
return (fixedItems, tracked);
}

/// <summary>
/// On the menus the item was shown from. Its id names what it acts on, so one built since
/// would act on the same entry, but an item the tracker has since dropped is in no other.
/// </summary>
static void Invoke(TrayMenu fixedItems, TrayMenu tracked, string id)
{
try
{
if (!fixedItems.Invoke(id))
{
tracked.Invoke(id);
}
}
catch (Exception exception)
{
// Thrown out of here, it would end the loop, and with it the tray
ExceptionHandler.Handle($"Failed to do what the menu item {id} does", exception);
turns.Turn();
}
}

Expand Down
72 changes: 72 additions & 0 deletions src/DiffEngineTray.Core/TrayTurns.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/// <summary>
/// The turns of <see cref="TrayLoop" />, for whichever loop runs them: the menus last shown, so a
/// click is done on the menu it was chosen from and a tip is the one for the item shown. The
/// Windows head runs them from a timer on its message loop, where the tray's events are raised.
/// </summary>
sealed class TrayTurns
{
readonly ITray<TrayIcon> tray;
readonly Tracker tracker;
readonly TrayActions head;
readonly Func<TrayIcon> icon;
TrayMenu fixedItems;
TrayMenu tracked;

/// <summary>
/// Shows the menus at once, so the tray holds them before anyone can open it.
/// </summary>
public TrayTurns(ITray<TrayIcon> tray, Tracker tracker, TrayActions head, Func<TrayIcon> icon)
{
this.tray = tray;
this.tracker = tracker;
this.head = head;
this.icon = icon;
(fixedItems, tracked) = Show();
}

/// <summary>
/// What was chosen on the menus last shown done, and the menus shown again. Done on those
/// menus: an item's id names what it acts on, so one built since would act on the same entry,
/// but an item the tracker has since dropped is in no other.
/// </summary>
public void Turn()
{
if (tray.Poll().ClickedItem is { } id)
{
Invoke(id);
}

(fixedItems, tracked) = Show();
}

/// <summary>
/// The tip of an item on the menus last shown, which a tray whose menus can show one asks as
/// it builds them.
/// </summary>
public string? Tooltip(string id) =>
fixedItems.Tooltip(id) ?? tracked.Tooltip(id);

(TrayMenu Fixed, TrayMenu Tracked) Show()
{
var shownFixed = TrayMenu.Fixed(head);
var shownTracked = TrayMenu.Tracked(tracker, head);
tray.Apply(new(icon(), "DiffEngineTray", [.. shownFixed.Items, .. shownTracked.Items]));
return (shownFixed, shownTracked);
}

void Invoke(string id)
{
try
{
if (!fixedItems.Invoke(id))
{
tracked.Invoke(id);
}
}
catch (Exception exception)
{
// Thrown out of here, it would end the loop, and with it the tray
ExceptionHandler.Handle($"Failed to do what the menu item {id} does", exception);
}
}
}
Binary file not shown.
Binary file not shown.
Loading
Loading