feat(layout): add top-tabs layout mode and shared channel resolver

This commit is contained in:
2026-05-29 14:06:41 +02:00
parent a9e70ce2af
commit ad892cbcb6
6 changed files with 98 additions and 25 deletions
+4
View File
@@ -201,9 +201,13 @@ internal static class PluginHostFactory
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<FontManager>()
));
services.AddSingleton(sp => new Ui.Components.TopTabBar(
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>()
));
services.AddSingleton(sp => new Ui.Windows.MainWindow(
sp.GetRequiredService<Ui.Components.HonorificHeader>(),
sp.GetRequiredService<Ui.Components.Sidebar>(),
sp.GetRequiredService<Ui.Components.TopTabBar>(),
sp.GetRequiredService<Ui.Components.MessageList>(),
sp.GetRequiredService<Ui.Components.InputBar>(),
sp.GetRequiredService<Ui.Components.StatusBar>(),
@@ -1,5 +1,4 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
namespace HellionChat.Ui.Components.Settings.Tabs;
@@ -16,11 +15,16 @@ internal sealed class WindowTab
{
if (ImGui.CollapsingHeader("Layout mode", ImGuiTreeNodeFlags.DefaultOpen))
{
// Sidebar is the v1.7.0 default; TopTabs is a v1.8.0 teaser.
ImGui.RadioButton("Sidebar", true);
using (ImRaii.Disabled(true))
var mode = Plugin.Config.MainWindowLayoutMode;
if (ImGui.RadioButton("Sidebar", mode == MainWindowLayoutMode.Sidebar))
{
ImGui.RadioButton("Top tabs (lands in v1.8.0)", false);
Plugin.Config.MainWindowLayoutMode = MainWindowLayoutMode.Sidebar;
_plugin.SaveConfig();
}
if (ImGui.RadioButton("Top tabs", mode == MainWindowLayoutMode.TopTabs))
{
Plugin.Config.MainWindowLayoutMode = MainWindowLayoutMode.TopTabs;
_plugin.SaveConfig();
}
}
+1 -20
View File
@@ -134,7 +134,7 @@ internal sealed class Sidebar
if (ImGui.IsItemClicked())
{
activeTab = tab;
EnsureCurrentChannel(tab);
TabLifecycleHelpers.EnsureCurrentChannel(tab);
}
dl.DrawHoverSheen(
@@ -252,23 +252,4 @@ internal sealed class Sidebar
ChatType.CustomEmote or ChatType.StandardEmote => FontAwesomeIcon.Comments,
_ => FontAwesomeIcon.Comment,
};
// Pick a sensible input channel for the tab if it has none yet —
// walking SelectedChannels for the first key with a ToInputChannel
// mapping lets the channel pill render the tab's actual channel
// instead of falling back to "—" on first activation.
private static void EnsureCurrentChannel(Tab tab)
{
if (tab.CurrentChannel.Channel != InputChannel.Invalid)
return;
foreach (var chatType in tab.SelectedChannels.Keys)
{
if (chatType.ToInputChannel() is { } input)
{
tab.CurrentChannel.SetChannel(input);
return;
}
}
}
}
+51
View File
@@ -0,0 +1,51 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using HellionChat.Util;
namespace HellionChat.Ui.Components;
// Horizontal tab strip — the alternative MainWindow layout to the Sidebar.
// Selection drives the same shared EnsureCurrentChannel path; pop-out is the
// same pool.TryOpen affordance as the sidebar (right-click context menu).
internal sealed class TopTabBar
{
private readonly Windows.ChannelPopoutPool _pool;
public TopTabBar(Windows.ChannelPopoutPool pool)
{
_pool = pool;
}
public void Draw(IList<Tab> tabs, ref Tab? activeTab)
{
for (var i = 0; i < tabs.Count; i++)
{
var tab = tabs[i];
if (i > 0)
ImGui.SameLine();
var selected = ReferenceEquals(tab, activeTab);
if (
ImGui.Selectable(
$"{tab.Name}###hellion_toptab_{i}",
selected,
ImGuiSelectableFlags.None,
new Vector2(0, 0)
)
)
{
activeTab = tab;
TabLifecycleHelpers.EnsureCurrentChannel(tab);
}
if (ImGui.BeginPopupContextItem($"toptab_ctx_{i}"))
{
if (ImGui.MenuItem("Pop Out"))
_pool.TryOpen(tab);
ImGui.EndPopup();
}
}
ImGui.Separator();
}
}
+14
View File
@@ -25,6 +25,7 @@ internal sealed class MainWindow : Window
private readonly Components.HonorificHeader _honorific;
private readonly Components.Sidebar _sidebar;
private readonly Components.TopTabBar _topTabs;
private readonly Components.MessageList _messages;
private readonly Components.InputBar _input;
private readonly Components.StatusBar _status;
@@ -39,6 +40,7 @@ internal sealed class MainWindow : Window
public MainWindow(
Components.HonorificHeader honorific,
Components.Sidebar sidebar,
Components.TopTabBar topTabs,
Components.MessageList messages,
Components.InputBar input,
Components.StatusBar status,
@@ -48,6 +50,7 @@ internal sealed class MainWindow : Window
{
_honorific = honorific;
_sidebar = sidebar;
_topTabs = topTabs;
_messages = messages;
_input = input;
_status = status;
@@ -121,6 +124,17 @@ internal sealed class MainWindow : Window
var bodyWidth = ImGui.GetContentRegionAvail().X;
_honorific.Draw(bodyWidth);
if (Plugin.Config.MainWindowLayoutMode == MainWindowLayoutMode.TopTabs)
{
_topTabs.Draw(Plugin.Config.Tabs, ref _activeTab);
using (ImRaii.Group())
{
DrawMainArea();
}
return;
}
// Sidebar layout (default).
using (ImRaii.Group())
{
_sidebar.Draw(bodyWidth, Plugin.Config.Tabs, ref _activeTab);
+19
View File
@@ -1,3 +1,5 @@
using HellionChat.Code;
namespace HellionChat.Util;
// Pure predicates for the TempTab pin lifecycle. Extracted from the strip
@@ -13,4 +15,21 @@ internal static class TabLifecycleHelpers
public static bool ShouldStripOnLoad(Tab t) => IsInUnpinnedPool(t);
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
// Shared by the click paths (Sidebar, TopTabBar) and the keybind tab-cycle
// path so every entry point resolves a tab's channel identically (no drift).
internal static void EnsureCurrentChannel(Tab tab)
{
if (tab.CurrentChannel.Channel != InputChannel.Invalid)
return;
foreach (var chatType in tab.SelectedChannels.Keys)
{
if (chatType.ToInputChannel() is { } input)
{
tab.CurrentChannel.SetChannel(input);
return;
}
}
}
}