feat(ui): assemble MainWindow from the components layer

Top-level chat window composes HonorificHeader, Sidebar, MessageList,
InputBar and StatusBar in the layout from the master spec: header row,
horizontal body (sidebar + main area with messages + input), status
strip pinned to the bottom. Component types are fully qualified through
the Ui.Components prefix so the v1.5.6 Ui.StatusBar type cannot shadow
the new layer through parent-namespace resolution before it is removed.
Toggle is a new-shadow on Window.Toggle so the open path also writes
Config.MainWindowOpen; OnClose covers the close path through the base
behaviour. InputBar.Height is now public so the layout math can reach
it from outside the components folder.
This commit is contained in:
2026-05-23 19:52:37 +02:00
parent 0fc2512f3e
commit 576cd6dafd
3 changed files with 129 additions and 1 deletions
+7
View File
@@ -145,6 +145,13 @@ internal static class PluginHostFactory
sp.GetRequiredService<ThemeRegistry>(), sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<FontManager>() sp.GetRequiredService<FontManager>()
)); ));
services.AddSingleton(sp => new Ui.Windows.MainWindow(
sp.GetRequiredService<Ui.Components.HonorificHeader>(),
sp.GetRequiredService<Ui.Components.Sidebar>(),
sp.GetRequiredService<Ui.Components.MessageList>(),
sp.GetRequiredService<Ui.Components.InputBar>(),
sp.GetRequiredService<Ui.Components.StatusBar>()
));
services.AddSingleton(sp => new Integrations.FailedTellNotifier( services.AddSingleton(sp => new Integrations.FailedTellNotifier(
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>() sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
)); ));
+1 -1
View File
@@ -16,7 +16,7 @@ namespace HellionChat.Ui.Components;
// picker overlay. // picker overlay.
internal sealed class InputBar internal sealed class InputBar
{ {
private const float Height = 32f; public const float Height = 32f;
private const float PillHeight = 22f; private const float PillHeight = 22f;
private const float PillPaddingX = 8f; private const float PillPaddingX = 8f;
private const int BufferCapacity = 500; private const int BufferCapacity = 500;
+121
View File
@@ -0,0 +1,121 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
namespace HellionChat.Ui.Windows;
// Top-level chat window assembled from the components layer. Layout from
// top to bottom: honorific header, horizontal body with sidebar + main
// area (messages + input bar), and the status strip pinned to the
// bottom. The window-level theme push stays on the global plugin draw
// path for now — this window only composes content.
//
// Components are fully qualified through the Ui.Components prefix so the
// old Ui.StatusBar type (still alive until the cleanup block removes it)
// cannot shadow the new layer through parent-namespace resolution.
internal sealed class MainWindow : Window
{
private const float DefaultWidth = 620f;
private const float DefaultHeight = 340f;
private const float MinWidth = 480f;
private const float MinHeight = 260f;
private readonly Components.HonorificHeader _honorific;
private readonly Components.Sidebar _sidebar;
private readonly Components.MessageList _messages;
private readonly Components.InputBar _input;
private readonly Components.StatusBar _status;
private Tab? _activeTab;
public MainWindow(
Components.HonorificHeader honorific,
Components.Sidebar sidebar,
Components.MessageList messages,
Components.InputBar input,
Components.StatusBar status
)
: base($"{Plugin.PluginName}###hellion-main")
{
_honorific = honorific;
_sidebar = sidebar;
_messages = messages;
_input = input;
_status = status;
Size = new Vector2(DefaultWidth, DefaultHeight);
SizeCondition = ImGuiCond.FirstUseEver;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(MinWidth, MinHeight),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
};
IsOpen = Plugin.Config.MainWindowOpen;
RespectCloseHotkey = false;
}
public Tab? ActiveTab => _activeTab;
// new-shadow on Window.Toggle so the open path also writes Config —
// OnClose already covers the close path through the base behaviour.
public new void Toggle()
{
IsOpen = !IsOpen;
Plugin.Config.MainWindowOpen = IsOpen;
}
public override void OnClose()
{
Plugin.Config.MainWindowOpen = false;
}
public override void Draw()
{
// First-frame seed: the active tab defaults to the first persisted
// tab so the message list isn't empty on a clean session.
if (_activeTab is null && Plugin.Config.Tabs.Count > 0)
_activeTab = Plugin.Config.Tabs[0];
var statusHeight = Components.StatusBar.Height;
using (var body = ImRaii.Child("##hellion-body", new Vector2(-1f, -statusHeight)))
{
if (body.Success)
DrawBody();
}
_status.Draw(_activeTab);
}
private void DrawBody()
{
var bodyWidth = ImGui.GetContentRegionAvail().X;
_honorific.Draw(bodyWidth);
using (ImRaii.Group())
{
_sidebar.Draw(bodyWidth, Plugin.Config.Tabs, ref _activeTab);
}
ImGui.SameLine();
using (ImRaii.Group())
{
DrawMainArea();
}
}
private void DrawMainArea()
{
var inputHeight = Components.InputBar.Height;
using (var messages = ImRaii.Child("##hellion-main-area", new Vector2(-1f, -inputHeight)))
{
if (messages.Success)
_messages.Draw(_activeTab!);
}
_input.Draw(_activeTab);
}
}