User reported wanting to mark text in-game and copy it out (item names,
coordinates, NPC dialogue, etc). ImGui doesn't natively let you select
across multiple TextColored widgets, but a read-only multi-line
InputText is fully click-drag selectable + Ctrl+C copyable. This
commit adds a "Copy mode" toggle to ChatPanel that swaps the chat
tail's render path between the colored-line view and a single
selectable text region.
New IPanelRenderer primitive:
void TextMultilineReadOnly(string id, string content, Vector2 size);
ImGui maps this to InputTextMultiline with the ReadOnly flag — same
selection + Ctrl+C UX a user expects from any text-input widget.
FakePanelRenderer records the call for tests. The future D.2b
custom retail-look backend implements its own equivalent (likely
the same widget pattern with retail font/skin).
ChatPanel rendering:
· A "Copy mode (select text to Ctrl+C)" Checkbox at the top of
the panel toggles _copyMode.
· Off (default) — current per-line render with colored combat
entries. Visually unchanged from before.
· On — the chat tail becomes a single TextMultilineReadOnly
widget holding every visible line joined with newlines. Loses
per-line color, gains arbitrary-span text selection.
· Footer (separator + input field) renders identically in both
modes so the user can still type while in copy mode.
Existing ChatPanelLayoutTests's footer-separator probe was using
IndexOf("Separator") — which now matches the new pre-tail separator
between the Checkbox and the chat tail. Switched to LastIndexOf
which still pins the footer separator (between EndChild and
InputTextSubmit). Behaviour and intent unchanged.
DisplaySettingsTests' With_expression test was still asserting the
old "1920x1080" Default.Resolution; updated to the new "1280x720"
that the previous wire-up commit introduced (the earlier commit
forgot this one).
dotnet build green (0 warnings); dotnet test 1,309 / 1,309 green
(243 Core.Net + 393 UI.Abstractions + 673 Core).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
282 lines
12 KiB
C#
282 lines
12 KiB
C#
using System.Numerics;
|
|
|
|
namespace AcDream.UI.Abstractions;
|
|
|
|
/// <summary>
|
|
/// Drawing primitives exposed to panels. The <b>only</b> API panels use to
|
|
/// emit pixels. The ImGui backend maps these straight onto ImGui calls; the
|
|
/// later custom retail-look backend will map the same primitives onto its
|
|
/// own retained-mode toolkit using retail dat-sourced fonts / sprites.
|
|
///
|
|
/// <para>
|
|
/// Keep this interface small and retail-friendly. If a widget requires a
|
|
/// feature the custom backend couldn't express with dat assets, don't add
|
|
/// it — find a different widget shape that both backends can satisfy.
|
|
/// </para>
|
|
/// </summary>
|
|
public interface IPanelRenderer
|
|
{
|
|
/// <summary>
|
|
/// Begin a top-level window. Matches retail's root <c>UiPanel</c> +
|
|
/// ImGui's <c>Begin</c>. Returns <c>false</c> if the window is collapsed
|
|
/// — the caller must still call <see cref="End"/> to balance.
|
|
/// </summary>
|
|
bool Begin(string title);
|
|
|
|
/// <summary>Close the most recent <see cref="Begin"/>.</summary>
|
|
void End();
|
|
|
|
/// <summary>Draw a single line of text. No formatting / markdown.</summary>
|
|
void Text(string text);
|
|
|
|
/// <summary>Keep the next widget on the same line as the previous one.</summary>
|
|
void SameLine();
|
|
|
|
/// <summary>Horizontal rule separator.</summary>
|
|
void Separator();
|
|
|
|
/// <summary>
|
|
/// A filled progress bar.
|
|
/// <paramref name="fraction"/> is clamped by the backend to [0, 1].
|
|
/// <paramref name="width"/> is the pixel width of the full bar.
|
|
/// <paramref name="overlay"/> is optional text (e.g. <c>"54%"</c>) rendered on top.
|
|
/// </summary>
|
|
void ProgressBar(float fraction, float width, string? overlay = null);
|
|
|
|
// -- Phase I.1 widget extensions ----------------------------------
|
|
|
|
/// <summary>
|
|
/// Draw a single line of text in the supplied RGBA color.
|
|
/// Used for combat-event color-coding (damage red, heal green) and
|
|
/// any other surface where a glance distinguishes severity.
|
|
/// </summary>
|
|
void TextColored(Vector4 rgba, string text);
|
|
|
|
/// <summary>
|
|
/// Open / close section grouping inside a window. Returns
|
|
/// <c>true</c> when the section is currently expanded — only render
|
|
/// the section's contents in that branch.
|
|
/// </summary>
|
|
/// <param name="defaultOpen">If the user has never toggled the
|
|
/// header before, start in this state.</param>
|
|
bool CollapsingHeader(string label, bool defaultOpen = true);
|
|
|
|
/// <summary>
|
|
/// Push an expandable nested node. Pair every successful
|
|
/// <c>true</c> return with a matching <see cref="TreePop"/>;
|
|
/// when this returns <c>false</c> do NOT call <see cref="TreePop"/>.
|
|
/// </summary>
|
|
bool TreeNode(string label);
|
|
|
|
/// <summary>Pop a previously-pushed <see cref="TreeNode"/>.</summary>
|
|
void TreePop();
|
|
|
|
/// <summary>
|
|
/// A boolean toggle. Returns <c>true</c> on the frame the user
|
|
/// flipped the box (and updates <paramref name="value"/> in place);
|
|
/// returns <c>false</c> the rest of the time.
|
|
/// </summary>
|
|
bool Checkbox(string label, ref bool value);
|
|
|
|
/// <summary>
|
|
/// A clickable button. Returns <c>true</c> on the single frame the
|
|
/// user clicked it; <c>false</c> every other frame.
|
|
/// </summary>
|
|
bool Button(string label);
|
|
|
|
/// <summary>
|
|
/// A drop-down selector. Returns <c>true</c> on the frame the user
|
|
/// changed the selection (and updates <paramref name="selectedIndex"/>
|
|
/// to the new value); <c>false</c> otherwise.
|
|
/// </summary>
|
|
bool Combo(string label, ref int selectedIndex, string[] items);
|
|
|
|
/// <summary>
|
|
/// A horizontal slider clamped to <paramref name="min"/> /
|
|
/// <paramref name="max"/>. Returns <c>true</c> on frames where the
|
|
/// user dragged the value (and updates <paramref name="value"/>);
|
|
/// <c>false</c> when idle.
|
|
/// </summary>
|
|
bool SliderFloat(string label, ref float value, float min, float max);
|
|
|
|
/// <summary>
|
|
/// Time-series line graph (e.g. fps history). The active window is
|
|
/// <paramref name="values"/>[<paramref name="offset"/>..
|
|
/// <paramref name="offset"/>+<paramref name="count"/>] interpreted
|
|
/// as a ring buffer.
|
|
/// </summary>
|
|
/// <param name="label">Header text shown above the plot.</param>
|
|
/// <param name="values">Sample buffer (kept by the caller).</param>
|
|
/// <param name="count">Number of valid samples in the ring.</param>
|
|
/// <param name="offset">Index of the oldest sample in
|
|
/// <paramref name="values"/>.</param>
|
|
/// <param name="overlay">Optional centered overlay text (e.g.
|
|
/// <c>"60 fps"</c>).</param>
|
|
/// <param name="min">Optional fixed lower bound; null = autoscale.</param>
|
|
/// <param name="max">Optional fixed upper bound; null = autoscale.</param>
|
|
/// <param name="size">Optional pixel size; null = backend default.</param>
|
|
void PlotLines(
|
|
string label,
|
|
float[] values,
|
|
int count,
|
|
int offset = 0,
|
|
string? overlay = null,
|
|
float? min = null,
|
|
float? max = null,
|
|
Vector2? size = null);
|
|
|
|
/// <summary>
|
|
/// Begin a multi-column table. Pair every call with <see cref="EndTable"/>.
|
|
/// Inside the table, advance to each cell with
|
|
/// <see cref="TableNextColumn"/> before emitting widgets for it.
|
|
/// </summary>
|
|
void BeginTable(string id, int columns);
|
|
|
|
/// <summary>Move to the next cell of the active table.</summary>
|
|
void TableNextColumn();
|
|
|
|
/// <summary>Close the most recent <see cref="BeginTable"/>.</summary>
|
|
void EndTable();
|
|
|
|
/// <summary>
|
|
/// A single-line text input that submits on Enter. Returns
|
|
/// <c>true</c> on the frame the user pressed Enter; in that case
|
|
/// <paramref name="submitted"/> is the value entered and the
|
|
/// implementation clears <paramref name="buffer"/> for the next
|
|
/// frame. On every other frame returns <c>false</c> and
|
|
/// <paramref name="submitted"/> is null; the implementation may
|
|
/// still mutate <paramref name="buffer"/> as the user types.
|
|
/// </summary>
|
|
/// <param name="maxLen">Maximum characters the buffer may grow to.</param>
|
|
bool InputTextSubmit(string label, ref string buffer, int maxLen, out string? submitted);
|
|
|
|
/// <summary>Insert a small vertical gap.</summary>
|
|
void Spacing();
|
|
|
|
/// <summary>Reserve invisible space of the given size, useful for
|
|
/// layout-only padding.</summary>
|
|
void Dummy(Vector2 size);
|
|
|
|
/// <summary>
|
|
/// Draw text that wraps at the current content region width.
|
|
/// Use for descriptions, error messages, anything multi-line.
|
|
/// </summary>
|
|
void TextWrapped(string text);
|
|
|
|
// -- Phase J Tier 3 — scrollable child region for chat-style layouts --
|
|
|
|
/// <summary>
|
|
/// Open a scrollable nested region inside the current window.
|
|
/// <paramref name="size"/> follows ImGui semantics: 0 means "fill
|
|
/// available", a positive value is absolute pixels, a negative
|
|
/// value means "fill available minus this amount". Pass
|
|
/// <c>(0, -footerHeight)</c> to reserve space at the bottom for
|
|
/// fixed elements (separator + input field) so the inner content
|
|
/// scrolls but the footer stays put across window resizes.
|
|
/// </summary>
|
|
bool BeginChild(string id, Vector2 size, bool border = false);
|
|
|
|
/// <summary>Close the most recent <see cref="BeginChild"/>.</summary>
|
|
void EndChild();
|
|
|
|
/// <summary>
|
|
/// Approximate single-line widget height including ImGui's frame
|
|
/// padding + item spacing. Panels use this to compute footer
|
|
/// reservations for <see cref="BeginChild"/> (e.g. one input
|
|
/// field + spacing). Backend-friendly because it returns a
|
|
/// scalar that the custom retail-look toolkit can also expose.
|
|
/// </summary>
|
|
float FrameHeightWithSpacing();
|
|
|
|
/// <summary>
|
|
/// Scroll the current scroll region so that the given fractional
|
|
/// vertical position is visible, where 0 = top and 1 = bottom.
|
|
/// Typical usage: call with <c>1.0f</c> at the bottom of a
|
|
/// chat-tail render block to keep the latest line visible when
|
|
/// new entries arrive. No-op when no new content was emitted —
|
|
/// callers are expected to skip the call when they don't want
|
|
/// to force a scroll.
|
|
/// </summary>
|
|
void SetScrollHereY(float ratio);
|
|
|
|
/// <summary>
|
|
/// Request keyboard focus for the NEXT widget rendered. Used by
|
|
/// <c>ChatPanel</c> when Tab fires <c>ToggleChatEntry</c> — the
|
|
/// chat input gets focused programmatically so the user can begin
|
|
/// typing without clicking the field.
|
|
/// </summary>
|
|
void SetKeyboardFocusHere();
|
|
|
|
// -- Phase K.3 — top-of-screen main menu bar -------------------------
|
|
|
|
/// <summary>
|
|
/// Open the top-of-screen main menu bar. Returns true if the bar is
|
|
/// visible (top-level menus go inside that branch). Always pair with
|
|
/// <see cref="EndMainMenuBar"/> when the call returned true.
|
|
/// </summary>
|
|
bool BeginMainMenuBar();
|
|
|
|
/// <summary>Close the menu bar opened by <see cref="BeginMainMenuBar"/>.</summary>
|
|
void EndMainMenuBar();
|
|
|
|
/// <summary>
|
|
/// Open a top-level menu within a menu bar. Returns true if the menu
|
|
/// is open — the caller emits <see cref="MenuItem"/> entries inside
|
|
/// that branch, then calls <see cref="EndMenu"/>.
|
|
/// </summary>
|
|
bool BeginMenu(string label);
|
|
|
|
/// <summary>Close the menu opened by <see cref="BeginMenu"/>.</summary>
|
|
void EndMenu();
|
|
|
|
/// <summary>
|
|
/// A clickable menu item with optional shortcut hint (e.g.
|
|
/// <c>"F11"</c>) drawn right-aligned. Returns true on the single
|
|
/// frame the user clicks the item; false otherwise.
|
|
/// </summary>
|
|
bool MenuItem(string label, string? shortcut = null);
|
|
|
|
// -- Tab bar (Settings panel + future tabbed surfaces) ---------------
|
|
|
|
/// <summary>
|
|
/// Open a tab bar inside the current window. Returns <c>true</c>
|
|
/// when the bar is visible — only emit <see cref="BeginTabItem"/>
|
|
/// calls inside that branch. Always pair with
|
|
/// <see cref="EndTabBar"/> when the call returned true. Retail had
|
|
/// tab bars in the Options UIs (<c>gmGameplayOptionsUI</c> etc), so
|
|
/// this primitive must be expressible by the future custom
|
|
/// retail-look backend.
|
|
/// </summary>
|
|
bool BeginTabBar(string id);
|
|
|
|
/// <summary>Close the tab bar opened by <see cref="BeginTabBar"/>.</summary>
|
|
void EndTabBar();
|
|
|
|
/// <summary>
|
|
/// Begin a single tab inside an open <see cref="BeginTabBar"/>.
|
|
/// Returns <c>true</c> when the tab is the currently selected one
|
|
/// — only render this tab's content in that branch. Always pair
|
|
/// with <see cref="EndTabItem"/> when the call returned true.
|
|
/// </summary>
|
|
bool BeginTabItem(string label);
|
|
|
|
/// <summary>Close the tab opened by <see cref="BeginTabItem"/>.</summary>
|
|
void EndTabItem();
|
|
|
|
/// <summary>
|
|
/// Render a read-only multi-line text region the user can
|
|
/// <b>select</b> with click+drag and copy with <c>Ctrl+C</c>.
|
|
/// Matches the typical "click into a textbox to grab text" UX —
|
|
/// chat panels, log viewers, etc. use this to make text
|
|
/// extractable without the user having to alt-tab + retype.
|
|
///
|
|
/// <para>
|
|
/// The widget is sized to <paramref name="size"/>; pass
|
|
/// <c>(0, 0)</c> for "fill the current content region" semantics
|
|
/// (matches ImGui defaults). <paramref name="id"/> is the ImGui
|
|
/// stable identifier — typically <c>"##chatcopy"</c> or similar
|
|
/// hidden-label form.
|
|
/// </para>
|
|
/// </summary>
|
|
void TextMultilineReadOnly(string id, string content, Vector2 size);
|
|
}
|