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
9 changes: 8 additions & 1 deletion native/include/deview.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ typedef struct DeviewScreen {
const DeviewQueueItem* queue;
int32_t queueCount;

/*
* How many entries are pending in total, which is not queueCount: that is the visible slice,
* sized to the body and with the members of folded groups left out. A head that counted the
* slice reported "Pending (16)" beside "inline 1 of 30", and folding a group lowered it.
*/
int32_t pendingCount;

int32_t titleOffset;
int32_t titleLength;
int32_t subtitleOffset;
Expand Down Expand Up @@ -215,7 +222,7 @@ typedef struct DeviewInput {
* described. A widened array element, so an older library reads every pane after the first at
* the wrong offset — this is the bump that matters most to honour.
*/
#define DEVIEW_VERSION 6
#define DEVIEW_VERSION 7

/*
* The Swift implementation imports this header for the struct layouts, because Swift does not
Expand Down
82 changes: 76 additions & 6 deletions native/src/deview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ struct State
ImGuiContext* context = nullptr;
DeviewInput input{};

/* Whether the last screen carried a context menu, which is what makes Escape and a click
* outside it a dismissal rather than what they would otherwise mean. */
bool menuOpen = false;

/* What a wheel message left over. A notch is 1.0, and a touchpad sends fractions of one:
* truncating each frame's value on its own threw all of them away, so a touchpad scrolled
* nothing at all. */
float scrollRemainder = 0.0f;

/*
* The queue column, owned here rather than by the table.
*
Expand Down Expand Up @@ -384,6 +393,7 @@ void UpdateTexture(ImTextureData* texture)
void RenderTriangles(
unsigned int count,
unsigned int indexStart,
unsigned int vertexOffset,
const ImVector<ImDrawIdx>& indices,
const ImVector<ImDrawVert>& vertices,
ImTextureID textureId)
Expand All @@ -400,7 +410,12 @@ void RenderTriangles(
{
for (unsigned int corner = 0; corner < 3; corner++)
{
const ImDrawVert& vertex = vertices[indices[indexStart + index + corner]];
/* Plus the command's own vertex offset. ImDrawIdx is sixteen bits, so a draw list
* that runs past 65535 vertices - a maximised 4K window of dense long lines gets
* there - is split by ImGui into commands whose indices restart from a base recorded
* here. Without adding it the indices wrapped and the panes drew scrambled, and in a
* release build, with IM_ASSERT compiled out, nothing said so. */
const ImDrawVert& vertex = vertices[vertexOffset + indices[indexStart + index + corner]];
const ImColor colour = ImColor(vertex.col);
rlColor4f(colour.Value.x, colour.Value.y, colour.Value.z, colour.Value.w);
rlTexCoord2f(vertex.uv.x, vertex.uv.y);
Expand Down Expand Up @@ -448,6 +463,7 @@ void RenderDrawData(ImDrawData* drawData)
RenderTriangles(
command.ElemCount,
command.IdxOffset,
command.VtxOffset,
commands->IdxBuffer,
commands->VtxBuffer,
command.GetTexID());
Expand Down Expand Up @@ -695,6 +711,10 @@ void DrawPaneImage(const DeviewScreen* screen, const DeviewPane& pane, const Pan

void BuildFrame(const DeviewScreen* screen)
{
/* Read by the input pass, which has no screen of its own: Escape means dismiss while one of
* these is up, and quit otherwise. */
state.menuOpen = screen->menuCount > 0;

const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
Expand Down Expand Up @@ -785,7 +805,7 @@ void BuildFrame(const DeviewScreen* screen)
if (index < screen->queueCount)
{
const DeviewQueueItem& item = screen->queue[index];
const std::string label = Copy(screen, item.labelOffset, item.labelLength);
std::string label = Copy(screen, item.labelOffset, item.labelLength);
if (item.flags & DEVIEW_QUEUE_HEADER)
{
/* A heading is dimmed like the subtitle, and never carries the selection.
Expand All @@ -805,9 +825,14 @@ void BuildFrame(const DeviewScreen* screen)
else
{
const bool selected = (item.flags & DEVIEW_QUEUE_SELECTED) != 0;
if (item.flags & DEVIEW_QUEUE_FAILED)
const bool failed = (item.flags & DEVIEW_QUEUE_FAILED) != 0;
if (failed)
{
ImGui::PushStyleColor(ImGuiCol_Text, RowColour(DEVIEW_ROW_REMOVED));
/* The marker the other three heads and docs/viewer.md show. Colour
* alone says nothing to a reader who cannot tell this red from the
* one a removed line is drawn in, or from any other. */
label += " !";
}

ImGui::PushID(index);
Expand All @@ -817,7 +842,7 @@ void BuildFrame(const DeviewScreen* screen)
}

ImGui::PopID();
if (item.flags & DEVIEW_QUEUE_FAILED)
if (failed)
{
ImGui::PopStyleColor();
}
Expand Down Expand Up @@ -981,9 +1006,22 @@ void BuildFrame(const DeviewScreen* screen)
ImGui::PopID();
}

/* Asked before End, which is what makes it about this window. A click anywhere else is a
* dismissal: the menu used to float until a row, a button or a key was hit, contrary to
* what docs/viewer.md says of it. A right click elsewhere opens the next menu, and the
* managed side ignores a dismissal that arrives with one of those. */
const bool overMenu = ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);

ImGui::End();
ImGui::PopStyleVar();
ImGui::PopStyleColor();

if (!overMenu &&
(ImGui::IsMouseClicked(ImGuiMouseButton_Left) ||
ImGui::IsMouseClicked(ImGuiMouseButton_Right)))
{
state.input.menuClosed = 1;
}
}

for (int index = 0; index < screen->buttonCount; index++)
Expand Down Expand Up @@ -1070,7 +1108,11 @@ int32_t deview_init(
/* No MSAA. ImGui draws axis aligned quads with pre-antialiased glyph textures, so multisampling
* buys nothing visually, and it is a real source of difference between a GPU and the software
* rasteriser the pixel snapshots are pinned to. */
unsigned int flags = FLAG_WINDOW_RESIZABLE;
/* ALWAYS_RUN because WindowShouldClose waits on events while the window is minimised, and
* that call is inside deview_present: without it the managed loop stops being pumped the
* moment the window is minimised, so a snapshot arriving after that is accepted by the
* listener and never shown. */
unsigned int flags = FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_ALWAYS_RUN;
if (hidden != 0)
{
flags |= FLAG_WINDOW_HIDDEN;
Expand All @@ -1090,6 +1132,10 @@ int32_t deview_init(
ImGui::SetCurrentContext(state.context);
ImGuiIO& io = ImGui::GetIO();
io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures;
/* Declared, so ImGui splits a long draw list into commands with a vertex offset rather than
* refusing to let one grow past what a sixteen bit index can address. RenderTriangles applies
* the offset. */
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset;
io.IniFilename = nullptr;
io.LogFilename = nullptr;
ApplyStyle();
Expand Down Expand Up @@ -1164,8 +1210,25 @@ void deview_poll_input(DeviewInput* input)
if (state.initialised)
{
state.input.key = ReadKey();

/* Escape with a menu up dismisses the menu. It reached the managed side as quit, which
* closes the menu and then runs the command, so Esc-to-dismiss closed the viewer - and on
* Linux there is no tray to open it again from, so the queue went to staging. */
if (state.menuOpen &&
state.input.key == DEVIEW_KEY_QUIT &&
IsKeyPressed(KEY_ESCAPE))
{
state.input.key = DEVIEW_KEY_NONE;
state.input.menuClosed = 1;
}

/* Whole notches, keeping the fraction. A touchpad sends a fraction of one per frame and
* truncating each frame on its own threw every one of them away. */
const Vector2 wheel = GetMouseWheelMoveV();
state.input.scrollDelta = static_cast<int32_t>(wheel.y);
state.scrollRemainder += wheel.y;
const int32_t notches = static_cast<int32_t>(state.scrollRemainder);
state.input.scrollDelta = notches;
state.scrollRemainder -= static_cast<float>(notches);
MeasureGrid();
}

Expand Down Expand Up @@ -1252,6 +1315,13 @@ void deview_focus(void)
}

ClearWindowState(FLAG_WINDOW_HIDDEN);
/* A minimised window stays minimised through SetWindowFocused, so a focus for a new snapshot
* left it in the taskbar. */
if (IsWindowMinimized())
{
RestoreWindow();
}

SetWindowFocused();
}

Expand Down
6 changes: 6 additions & 0 deletions native/swift/Sources/Deview/Frame.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ struct Frame {
var subtitle = ""
var status = ""
var queue: [QueueItem] = []

/// Everything pending, which `queue` is not: that is the visible slice, sized to the body and
/// with the members of folded groups left out.
var pendingCount: Int32 = 0

var buttons: [Button] = []
var left = Pane()
var right = Pane()
Expand Down Expand Up @@ -63,6 +68,7 @@ struct Frame {
frame.title = string(screen, screen.titleOffset, screen.titleLength)
frame.subtitle = string(screen, screen.subtitleOffset, screen.subtitleLength)
frame.status = string(screen, screen.statusOffset, screen.statusLength)
frame.pendingCount = screen.pendingCount

if let items = screen.queue {
for index in 0 ..< Int(screen.queueCount) {
Expand Down
7 changes: 4 additions & 3 deletions native/swift/Sources/Deview/Renderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,10 @@ final class Renderer {

let headerTop = firstRule + Renderer.gap
if hasQueue {
// Entries only: the rows include group headings, which are not pending anything.
let pending = frame.queue.filter { !$0.header }.count
text("Pending (\(pending))", in: rect(top: headerTop, left: Renderer.padding, width: queue, height: line, size), Palette.text, context)
// The count the managed side carries, not one derived from `queue`: that is the
// visible slice, so thirty pending in a sixteen row body read as "Pending (16)" beside
// "inline 1 of 30", and folding a group lowered it further.
text("Pending (\(frame.pendingCount))", in: rect(top: headerTop, left: Renderer.padding, width: queue, height: line, size), Palette.text, context)
}

text(frame.left.header, in: rect(top: headerTop, left: panesLeft, width: half, height: line, size), Palette.text, context)
Expand Down
40 changes: 34 additions & 6 deletions native/swift/Sources/Deview/ViewerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,28 @@ final class ViewerView: NSView, NSViewToolTipOwner {
///
/// The text is answered on demand below rather than stored here, so a row whose label changed
/// under a resting cursor still reads correctly.
/// Rebuilt only when the regions themselves changed. AppKit times its tooltip delay from the
/// moment the cursor enters a tracking rectangle, and this runs on every frame, so removing
/// and re-adding the rectangle under a resting cursor restarted that delay before it could
/// ever elapse - which is to say queue tooltips never appeared on macOS at all.
func refreshToolTips() {
let wanted = layout.queueItems.enumerated()
.filter { $0.offset < model.queue.count && !model.queue[$0.offset].tooltip.isEmpty }
.map(\.element)
guard wanted != toolTipRects else {
return
}

toolTipRects = wanted
removeAllToolTips()
for (index, bounds) in layout.queueItems.enumerated()
where index < model.queue.count && !model.queue[index].tooltip.isEmpty {
for bounds in wanted {
_ = addToolTip(bounds, owner: self, userData: nil)
}
}

/// What the tips are registered on, so an unchanged frame can leave them alone.
private var toolTipRects: [NSRect] = []

/// Composed by the managed side, so this only finds the row under the cursor.
func view(_ view: NSView, stringForToolTip tag: NSView.ToolTipTag, point: NSPoint, userData: UnsafeMutableRawPointer?) -> String {
guard let index = layout.queueItems.firstIndex(where: { $0.contains(point) }),
Expand Down Expand Up @@ -132,12 +146,26 @@ final class ViewerView: NSView, NSViewToolTipOwner {
super.rightMouseDown(with: event)
}

/// Accumulated, because a trackpad delivers many small deltas between two polls and the
/// managed side amplifies whatever it is given.
/// A notch of a wheel, in the points a precise device reports one movement of it as.
///
/// AppKit reports a wheel in lines and a trackpad in points, and rounding both to an integer
/// number of notches treated them as the same thing: an ordinary flick of a trackpad reads as
/// tens of points, so it arrived as tens of notches and the managed side then multiplied it
/// by three. Slow movement rounded to nothing at all.
private static let pointsPerNotch = 16.0

/// What is left over between events, because a trackpad delivers many small deltas between
/// two polls and dropping each one on its own is what made slow movement do nothing.
private var scrollRemainder = 0.0

override func scrollWheel(with event: NSEvent) {
let notches = Int32(event.scrollingDeltaY.rounded())
scrollRemainder += event.hasPreciseScrollingDeltas
? event.scrollingDeltaY / ViewerView.pointsPerNotch
: event.scrollingDeltaY
let notches = scrollRemainder.rounded(.towardZero)
scrollRemainder -= notches
if notches != 0 {
Runtime.shared.input.scrollDelta += notches
Runtime.shared.input.scrollDelta += Int32(notches)
}
}

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion src/DiffEngineViewer/Native/Deview.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ static unsafe partial class Deview
/// Must match DEVIEW_VERSION in native/include/deview.h. Bumped whenever the structs change,
/// so a stale native library is reported rather than read as garbage.
/// </summary>
public const int ExpectedVersion = 6;
public const int ExpectedVersion = 7;

[LibraryImport(library, EntryPoint = "deview_version")]
public static partial int Version();
Expand Down
5 changes: 5 additions & 0 deletions src/DiffEngineViewer/Native/DeviewStructs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ unsafe struct DeviewScreen
public int ButtonCount;
public DeviewQueueItem* Queue;
public int QueueCount;

/// <summary>
/// Everything pending, rather than the visible slice <see cref="QueueCount" /> counts.
/// </summary>
public int PendingCount;
public int TitleOffset;
public int TitleLength;
public int SubtitleOffset;
Expand Down
3 changes: 3 additions & 0 deletions src/DiffEngineViewer/Native/ScreenPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ sealed class ScreenPayload
int statusOffset;
int statusLength;
int menuRow;
int pendingCount;

public void Build(Screen screen)
{
Expand All @@ -26,6 +27,7 @@ public void Build(Screen screen)
queue.Clear();
menu.Clear();
menuRow = -1;
pendingCount = screen.PendingCount;

(titleOffset, titleLength) = Add(screen.Title);
(subtitleOffset, subtitleLength) = Add(screen.Subtitle);
Expand Down Expand Up @@ -142,6 +144,7 @@ unsafe DeviewScreen Native(
ButtonCount = buttons.Count,
Queue = queuePtr,
QueueCount = queue.Count,
PendingCount = pendingCount,
TitleOffset = titleOffset,
TitleLength = titleLength,
SubtitleOffset = subtitleOffset,
Expand Down
Loading