using System.Globalization;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using ClaudeDo.Ui.Converters;
namespace ClaudeDo.Ui.Views.Controls;
/// Reusable modal chrome: titlebar (drag + close) wrapping a body and optional footer.
public class ModalShell : ContentControl
{
public static readonly StyledProperty TitleProperty =
AvaloniaProperty.Register(nameof(Title));
public static readonly StyledProperty FooterProperty =
AvaloniaProperty.Register(nameof(Footer));
// Window-level actions rendered in the titlebar, left of maximise/close. Keeps a single
// always-visible action (e.g. the usage monitor's refresh) out of the body's vertical flow.
public static readonly StyledProperty HeaderActionsProperty =
AvaloniaProperty.Register(nameof(HeaderActions));
public static readonly StyledProperty CloseCommandProperty =
AvaloniaProperty.Register(nameof(CloseCommand));
// Opt-in: most modals are fixed-purpose dialogs that don't need maximise. Set true only
// on windows (e.g. the diff viewer) that benefit from a maximised full-screen layout.
public static readonly StyledProperty ShowMaximizeButtonProperty =
AvaloniaProperty.Register(nameof(ShowMaximizeButton));
public string? Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
public object? Footer { get => GetValue(FooterProperty); set => SetValue(FooterProperty, value); }
public object? HeaderActions { get => GetValue(HeaderActionsProperty); set => SetValue(HeaderActionsProperty, value); }
public ICommand? CloseCommand { get => GetValue(CloseCommandProperty); set => SetValue(CloseCommandProperty, value); }
public bool ShowMaximizeButton { get => GetValue(ShowMaximizeButtonProperty); set => SetValue(ShowMaximizeButtonProperty, value); }
private Window? _window;
private PathIcon? _maxIcon;
private Button? _maxButton;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (e.NameScope.Find("PART_TitleBar") is { } bar)
bar.PointerPressed += OnTitleBarPressed;
_maxButton = e.NameScope.Find("PART_MaxButton");
if (_maxButton is not null) _maxButton.Click += OnMaxButtonClick;
_maxIcon = e.NameScope.Find("PART_MaxIcon");
UpdateMaxIcon();
}
/// Mirrors what MainWindow does for itself: with an extended client area a maximised window
/// overhangs the screen by the invisible resize border, so the chrome has to be inset by
/// OffScreenMargin or its edges (and the close button) get clipped. Doing it here covers
/// every modal at once instead of per code-behind.
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_window = TopLevel.GetTopLevel(this) as Window;
if (_window is null) return;
Margin = _window.OffScreenMargin;
_window.PropertyChanged += OnWindowPropertyChanged;
UpdateMaxIcon();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (_window is not null) _window.PropertyChanged -= OnWindowPropertyChanged;
_window = null;
}
private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == Window.OffScreenMarginProperty && _window is not null)
Margin = _window.OffScreenMargin;
if (e.Property == Window.WindowStateProperty)
UpdateMaxIcon();
}
private void UpdateMaxIcon()
{
if (_maxIcon is null || _window is null) return;
var maximized = _window.WindowState == WindowState.Maximized;
_maxIcon.Data = (Geometry?)IconKeyConverter.Instance.Convert(
maximized ? "mdi:WindowRestore" : "mdi:WindowMaximize", typeof(Geometry), null, CultureInfo.InvariantCulture);
if (_maxButton is not null)
{
var tipKey = maximized ? "modals.diff.restore" : "modals.diff.maximize";
ToolTip.SetTip(_maxButton, ClaudeDo.Ui.Localization.Loc.T(tipKey));
}
}
private void OnMaxButtonClick(object? sender, RoutedEventArgs e) => ToggleMaximize();
private void ToggleMaximize()
{
if (_window is null) return;
_window.WindowState = _window.WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
}
///
/// Hands the drag to the window manager instead of moving the window by hand.
///
///
/// This is what makes Windows snap work — drag to the top edge to maximise, to a side for
/// half the screen, with the usual preview overlay. Assigning Window.Position per
/// pointer-move (what this used to do) bypasses the OS move loop entirely, so no snap
/// gesture is ever recognised. The OS only snaps resizable windows, so the non-resizable
/// modals opt out on their own without a flag here.
///
/// Note: VisualRoot is a TopLevelHost rather than the Window in Avalonia 12, so the
/// window has to come from . Do not capture the pointer —
/// runs its own modal loop and needs the input.
///
private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
if (ShowMaximizeButton && e.ClickCount == 2)
{
ToggleMaximize();
return;
}
(TopLevel.GetTopLevel(this) as Window)?.BeginMoveDrag(e);
}
}