using XFESystem.Core;
namespace XFESystem.Desktop;
/// <summary>Shared managed desktop. All filesystem and browser operations go through explicit guest adapters.</summary>
public sealed class DesktopSession
{
private DesktopTheme _theme;
private uint Background => _theme.Background;
private uint Panel => _theme.Panel;
private uint Border => _theme.Border;
private uint TextColor => _theme.AccentText;
private uint White => _theme.Text;
private uint Muted => _theme.Muted;
private uint Accent => _theme.AccentStroke;
private const uint TerminalBackground = 0x0C1321, TerminalForeground = 0x75C9F5;
private const int TaskbarHeight = 48, MinimumWidth = 320, MinimumHeight = 200;
private readonly IDesktopCanvas _canvas;
private readonly IWorkspaceStorage _storage;
private readonly IBrowserService? _browser;
private readonly Action? _consoleDemo;
private readonly Action<string>? _managedApplication;
private readonly Func<long>? _monotonicMilliseconds;
private readonly IEditorPreparationService? _editorPreparation;
private readonly Func<int>? _threadIdentity;
private readonly List<EditorSourceFrame> _editorSourceFrames = [];
private long _nextEditorPreparationGeneration;
public Action<string>? Trace { get; init; }
private readonly List<DesktopWindow> _windows = [];
private readonly List<int> _mru = [];
private readonly HashSet<int> _desktopHidden = [];
private List<int>? _switchOrder;
private int _switchIndex;
private int _startSelection;
private int _nextId = 1, _pointerX, _pointerY, _dragX, _dragY;
private int _resizeX, _resizeY, _lastTitleId, _lastTitleX, _lastTitleY;
private long _lastTitleTime = -1;
private bool _leftDown;
private bool _sceneDirty = true, _pointerDirty;
private DesktopBounds? _sceneDamage;
private DesktopBounds _renderedPointerBounds;
private DesktopWindow? _drag, _resize;
private DesktopWindow? _webPointerCapture, _webHoveredWindow;
private int _motionSceneOwner;
private DesktopBounds _motionSceneBounds;
private bool _motionSourceValid, _pendingMotion, _motionInvalidation, _motionIsResize;
private DesktopBounds _resizeBounds;
private HitTarget? _hovered, _pressed, _pendingWindowButton;
private long _feedbackNow, _feedbackExpires;
public DesktopTheme Theme => _theme;
public bool IsAppearancePersistent => _storage.IsPersistent;
public string AppearanceStatus { get; private set; } = "";
public IReadOnlyList<DesktopWindow> Windows => _windows;
public DesktopWindow? ActiveWindow => _windows.FindLast(w => !w.IsMinimized);
public DesktopBounds WorkArea => new(0, 0, _canvas.Width, Math.Max(1, _canvas.Height - TaskbarHeight));
public bool IsStartMenuOpen { get; private set; }
public bool IsWindowSwitching => _switchOrder is not null;
public bool HasBrowserContentFocus => !IsStartMenuOpen && ActiveWindow is { BrowserContentFocused: true, BrowserSession: not null };
public bool NeedsRender
{
get => _sceneDirty || _pointerDirty;
private set { if (value) { _sceneDirty = true; _sceneDamage = null; _motionSourceValid = false; } else { _sceneDirty = false; _pointerDirty = false; _sceneDamage = null; } }
}
public bool LastFrameCompleted { get; private set; }
public long RenderYieldCount { get; private set; }
public string Notification { get; private set; } = "RAM live - files stay in memory for this session";
public DesktopSession(IDesktopCanvas canvas, IWorkspaceStorage storage, IBrowserService? browser = null, Action? consoleDemo = null, Action<string>? managedApplication = null, Func<long>? monotonicMilliseconds = null, IEditorPreparationService? editorPreparation = null, Func<int>? threadIdentity = null)
{
_canvas = canvas;
_storage = storage;
_browser = browser;
_consoleDemo = consoleDemo;
_managedApplication = managedApplication;
_monotonicMilliseconds = monotonicMilliseconds;
_editorPreparation = editorPreparation;
_threadIdentity = threadIdentity;
_theme = DesktopTheme.Load(storage, out var appearanceError);
AppearanceStatus = appearanceError ?? (_storage.IsPersistent ? "Appearance settings are stored on disk" : "Appearance settings are stored in RAM for this session");
if (appearanceError is not null) Notification = appearanceError;
}
public DesktopWindow Open(DesktopApplication app, string? location = null) => OpenCore(app, location, true);
private DesktopWindow OpenCore(DesktopApplication app, string? location, bool navigateBrowser)
{
if (_windows.Count >= 12) throw new InvalidOperationException("Close a window before opening another.");
CancelPointerCapture();
var offset = (_nextId - 1) % 5 * 22;
int width = Math.Min(WorkArea.Width, Math.Max(MinimumWidth, Math.Min(800, _canvas.Width - 190)));
int height = Math.Min(WorkArea.Height, Math.Max(MinimumHeight, Math.Min(500, _canvas.Height - 120)));
var window = new DesktopWindow { Id = _nextId++, Application = app,
Bounds = Constrain(new(150 + offset, 66 + offset, width, height)), Title = app.ToString() };
window.RestoreBounds = window.Bounds;
_windows.Add(window);
_switchOrder = null;
RememberActivation(window);
IsStartMenuOpen = false;
if (app == DesktopApplication.Files) window.Location = location ?? SystemPaths.Documents;
else if (app == DesktopApplication.Editor)
{
window.Location = location ?? UniquePath(SystemPaths.Documents, "Document", ".txt");
window.Content = _storage.Exists(window.Location) ? _storage.ReadText(window.Location) : "";
window.Title = "Editor - " + SystemPaths.Name(window.Location);
if ((window.Location.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) || window.Location.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) && _browser is IInteractiveBrowserFactory factory)
{
if (_editorPreparation is not null) BeginEditorPreparation(window);
else
{
try
{
window.BrowserSession = factory.CreatePageSession();
window.BrowserSession.SetCanvas(_canvas);
window.BrowserSession.ResizeViewport(BrowserViewport(window).Width, BrowserViewport(window).Height);
window.BrowserSession.NavigateEditor(window.Content, window.Location);
window.BrowserContentFocused = true;
window.Status = "Monaco | Ctrl+S saves to " + window.Location;
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
window.BrowserSession = null;
window.Status = "Basic text editor | Monaco unavailable: " + error.Message;
}
}
}
}
else if (app == DesktopApplication.Browser)
{
window.Location = location ?? "about:Start";
window.AddressSelected = true;
if (navigateBrowser) Navigate(window);
else if (_browser is IInteractiveBrowserFactory factory)
{
window.BrowserSession = factory.CreatePageSession();
window.BrowserSession.SetCanvas(_canvas);
window.BrowserSession.ResizeViewport(BrowserViewport(window).Width, BrowserViewport(window).Height);
}
}
else if (app == DesktopApplication.Terminal) { window.Content = "XFESystem | XFEstudio\nC# NativeAOT development session\n\nClick Run Demo to execute the built-in C# console sample.\n"; }
else if (app == DesktopApplication.Settings)
{
window.Title = "Settings - Appearance"; window.AppearanceAccentText = _theme.AccentHex;
window.AppearanceAccentSelected = true; window.Status = AppearanceStatus;
}
NeedsRender = true;
return window;
}
private void BeginEditorPreparation(DesktopWindow window)
{
var viewport = BrowserViewport(window);
var request = new EditorPreparationRequest(window.Id, ++_nextEditorPreparationGeneration,
window.Content, window.Location, viewport.Width, viewport.Height, _monotonicMilliseconds?.Invoke() ?? _feedbackNow);
window.EditorPreparationGeneration = request.Generation;
window.EditorPreparationStarted = System.Diagnostics.Stopwatch.GetTimestamp();
window.BrowserSession = null; window.BrowserContentFocused = false;
window.Status = "Loading real Monaco in background | text editing remains available";
try { window.EditorPreparation = _editorPreparation!.Begin(request); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ window.EditorPreparation = null; window.Status = "Basic text editor | Background Monaco unavailable: " + error.Message; }
}
private void PollEditorPreparations()
{
foreach (var window in _windows)
{
if (window.EditorPreparation is not { } operation || !operation.TryTakeResult(out var result)) continue;
window.EditorPreparation = null;
if (window.Content != operation.Request.Source || window.Location != operation.Request.Path)
{
operation.Cancel();
TraceEditorPreparation("Superseded", operation.Request, result.WorkerThreadId);
BeginEditorPreparation(window);
InvalidateScene(window.Bounds);
continue;
}
if (result.Page is null)
{
window.Status = "Basic text editor | Background Monaco unavailable: " + result.Error;
TraceEditorPreparation("FailedUi", operation.Request, result.WorkerThreadId);
}
else
{
try
{
// The UI receives a proxy and independent delivery fonts.
// Its actual engine remains exclusively on the worker.
bool equivalentMetrics = result.PreparedCanvas is IEditorPreparationCanvasHandoff handoff && handoff.TryCompleteEditorPreparationHandoff(_canvas);
result.Page.CompleteEditorPreparationHandoff(_canvas, equivalentMetrics);
var viewport = BrowserViewport(window);
result.Page.ResizeViewport(viewport.Width, viewport.Height);
window.BrowserSession = result.Page;
window.BrowserContentFocused = true;
window.Status = "Monaco | Ctrl+S saves to " + window.Location;
TraceEditorPreparation("Ready", operation.Request, result.WorkerThreadId);
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
window.BrowserSession = null; window.BrowserContentFocused = false;
window.Status = "Basic text editor | Prepared Monaco unavailable: " + error.Message;
TraceEditorPreparation("FailedUi", operation.Request, result.WorkerThreadId);
}
}
InvalidateScene(window.Bounds);
}
}
private void TraceEditorPreparation(string state, EditorPreparationRequest request, int workerThread)
{
if (Trace is null) return;
var window = _windows.Find(candidate => candidate.Id == request.WindowId);
long elapsed = window is null ? 0 : System.Diagnostics.Stopwatch.GetTimestamp() - window.EditorPreparationStarted;
Trace("XFE_EDITOR_BACKGROUND|" + state + "|Window|" + request.WindowId + "|Generation|" + request.Generation +
"|UiThread|" + (_threadIdentity?.Invoke() ?? Environment.CurrentManagedThreadId) + "|Thread|" + workerThread +
"|SourceCharacters|" + request.Source.Length + "|ElapsedTicks|" + elapsed + "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");
}
private readonly record struct EditorSourceFrame(DesktopWindow Window, long Generation, string Source, int NonEmptyLines, DesktopBounds TextBounds);
private void ReportPresentedEditorSources(long presentedAt)
{
foreach (var frame in _editorSourceFrames)
{
var window = frame.Window;
if (window.EditorSourcePresentedGeneration == frame.Generation) continue;
window.EditorSourcePresentedGeneration = frame.Generation;
if (Trace is null) continue;
ulong hash = 14695981039346656037UL;
foreach (char character in frame.Source)
{
hash = unchecked((hash ^ (byte)character) * 1099511628211UL);
hash = unchecked((hash ^ (byte)(character >> 8)) * 1099511628211UL);
}
Trace("XFE_EDITOR_BACKGROUND|SourcePresented|Window|" + window.Id + "|Generation|" + frame.Generation +
"|UiThread|" + (_threadIdentity?.Invoke() ?? Environment.CurrentManagedThreadId) +
"|SourceCharacters|" + frame.Source.Length + "|FingerprintAlgorithm|UTF16LE-FNV1a64|SourceFingerprint|" + hash.ToString("x16") +
"|VisibleNonEmptyLines|" + frame.NonEmptyLines + "|TextX|" + frame.TextBounds.X + "|TextY|" + frame.TextBounds.Y +
"|TextWidth|" + frame.TextBounds.Width + "|TextHeight|" + frame.TextBounds.Height +
"|ElapsedTicks|" + (presentedAt - window.EditorPreparationStarted) + "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");
}
}
public void Close(int id)
{
var window = _windows.Find(w => w.Id == id);
if (window is null) return;
CancelPointerCapture();
if (window.IsDirty) { window.Status = "Unsaved changes - Ctrl+S to save, Escape to discard then close"; NeedsRender = true; return; }
if (window.EditorPreparation is { } preparation)
{
preparation.Cancel(); window.EditorPreparation = null;
TraceEditorPreparation("Cancel", preparation.Request, 0);
}
if (window.BrowserSession is IBackgroundEditorSession background) background.Cancel();
_windows.Remove(window);
if (ReferenceEquals(_webHoveredWindow, window)) _webHoveredWindow = null;
if (ReferenceEquals(_webPointerCapture, window)) _webPointerCapture = null;
window.BrowserSession = null;
_mru.Remove(window.Id);
_desktopHidden.Remove(window.Id);
if (_switchOrder is not null) { _switchOrder = null; if (ActiveWindow is { } next) RememberActivation(next); }
if (_drag == window) _drag = null;
if (_resize == window) _resize = null;
_lastTitleId = 0;
NeedsRender = true;
}
public void SetTerminalSnapshot(XFESystem.Console.TerminalSnapshot snapshot)
{
var window = _windows.FindLast(w => w.Application == DesktopApplication.Terminal) ?? Open(DesktopApplication.Terminal);
window.Terminal = snapshot;
Activate(window.Id);
NeedsRender = true;
}
public void Pointer(int x, int y, bool leftButton, int wheel = 0, long timestampMs = -1)
{
x = Math.Clamp(x, 0, _canvas.Width - 1);
y = Math.Clamp(y, 0, _canvas.Height - 1);
bool webInputChanged = x != _pointerX || y != _pointerY || leftButton != _leftDown || wheel != 0;
if (x != _pointerX || y != _pointerY) _pointerDirty = true;
_pointerX = x; _pointerY = y;
long now = timestampMs >= 0 ? timestampMs : _monotonicMilliseconds?.Invoke() ?? _feedbackNow;
now = Math.Max(now, _feedbackNow);
_feedbackNow = now;
UpdateHover(ResolveTarget(x, y));
// A button-break packet can also carry the last displacement. Apply
// that position to the existing capture before retiring it below.
bool capturedPosition = leftButton || _leftDown;
if (_drag is not null && capturedPosition)
{
var b = _drag.Bounds;
var previous = b;
if (_drag.IsMaximized || _drag.IsSnapped)
{
if (Math.Abs(x - _lastTitleX) <= 4 && Math.Abs(y - _lastTitleY) <= 4) goto DragFinished;
var restore = Constrain(_drag.RestoreBounds);
_dragX = Math.Clamp(_dragX * restore.Width / Math.Max(1, b.Width), 0, restore.Width - 1);
_dragY = Math.Min(_dragY, 24);
_drag.IsMaximized = false; _drag.IsSnapped = false;
b = restore;
}
_drag.Bounds = Constrain(b with { X = x - _dragX, Y = y - _dragY });
_drag.RestoreBounds = _drag.Bounds;
if (Math.Abs(x - _lastTitleX) > 4 || Math.Abs(y - _lastTitleY) > 4) _lastTitleId = 0;
if (previous != _drag.Bounds) InvalidateWindowMotion(_drag, previous);
}
DragFinished:
if (_resize is not null && capturedPosition)
{
var previous = _resize.Bounds;
_resize.Bounds = Constrain(_resizeBounds with
{
Width = Math.Clamp(_resizeBounds.Width + x - _resizeX, Math.Min(MinimumWidth, WorkArea.Width - _resizeBounds.X), WorkArea.Width - _resizeBounds.X),
Height = Math.Clamp(_resizeBounds.Height + y - _resizeY, Math.Min(MinimumHeight, WorkArea.Y + WorkArea.Height - _resizeBounds.Y), WorkArea.Y + WorkArea.Height - _resizeBounds.Y)
});
_resize.RestoreBounds = _resize.Bounds;
if (previous != _resize.Bounds) InvalidateWindowMotion(_resize, previous, resizing: true);
}
if (!leftButton) { _drag = null; _resize = null; }
if (wheel != 0 && ActiveWindow is { } active && active.Application != DesktopApplication.Settings && active.BrowserSession is null)
{
int count = active.Application == DesktopApplication.Files ? _storage.List(active.Location).Length : active.Content.Split('\n').Length;
int rows = active.Application == DesktopApplication.Files ? Math.Max(1, (active.Bounds.Height - 132) / 28) : ContentRows(active);
active.ScrollLine = Math.Clamp(active.ScrollLine - wheel * 3, 0, Math.Max(0, count - rows));
InvalidateScene(ScrollDamage(active));
}
if (leftButton && !_leftDown)
{
_pressed = _hovered; _feedbackExpires = long.MaxValue;
if (_pressed is { } pressed) InvalidateScene(pressed.Bounds);
try { Click(x, y, timestampMs >= 0 ? timestampMs : _monotonicMilliseconds?.Invoke() ?? -1); }
catch (IOException error) { if (ActiveWindow is { } errorWindow) errorWindow.Status = error.Message; }
catch (ArgumentException error) { if (ActiveWindow is { } errorWindow) errorWindow.Status = error.Message; }
catch (InvalidOperationException error) { Notification = error.Message; if (ActiveWindow is { } errorWindow) errorWindow.Status = error.Message; }
}
if (!leftButton && _leftDown)
{
if (_pendingWindowButton is { } pending && SameTarget(pending, ResolveTarget(x, y)))
{ if (pending.Kind == HitKind.WindowMinimize) Minimize(pending.WindowId); else Close(pending.WindowId); }
else if (_pendingWindowButton is not null) _pressed = null;
_pendingWindowButton = null;
_feedbackExpires = now + 120;
if (_pressed is { } pressed) InvalidateScene(pressed.Bounds);
UpdateHover(ResolveTarget(x, y));
}
if (webInputChanged) RouteBrowserPointer(x,y,leftButton,wheel);
_leftDown = leftButton;
// Captured motion can change the hit geometry before the next input
// packet. Retire the old hover rectangle in the same frame.
UpdateHover(ResolveTarget(x, y));
UpdateChromeTargets(now);
}
private void RouteBrowserPointer(int x,int y,bool leftButton,int wheel)
{
if (_webPointerCapture is not null && (!_windows.Contains(_webPointerCapture) || _webPointerCapture.IsMinimized)) _webPointerCapture = null;
var under = _windows.FindLast(w => !w.IsMinimized && w.Bounds.Contains(x,y));
DesktopWindow? target = _webPointerCapture ?? (_drag is null && _resize is null && under is { BrowserSession:not null } && BrowserViewport(under).Contains(x,y) ? under : null);
if (_webHoveredWindow is { BrowserSession:{ } oldPage } old && old != target)
{
long oldRevision = oldPage.Revision; oldPage.Pointer(new("mousemove",-1,-1));
if (oldPage.Revision != oldRevision) InvalidateScene(BrowserViewport(old));
}
_webHoveredWindow = target;
if (target?.BrowserSession is not { } page) return;
var area = BrowserViewport(target); long revision = page.Revision;
string type = leftButton && !_leftDown ? "mousedown" : !leftButton && _leftDown ? "mouseup" : "mousemove";
if (type == "mousedown") { target.BrowserContentFocused = true; _webPointerCapture = target; }
page.Pointer(new(type,x-area.X,y-area.Y,Buttons:leftButton?1:0));
if (type == "mouseup") _webPointerCapture = null;
if (wheel != 0)
{
bool prevented = page.Pointer(new("wheel",x-area.X,y-area.Y,DeltaY:-wheel*60));
if (!prevented) { target.BrowserScroll = Math.Clamp(target.BrowserScroll-wheel*60,0,Math.Max(0,page.DocumentHeight-area.Height)); InvalidateScene(area); }
}
if (page.Revision != revision) { UpdateBrowserPage(target); InvalidateScene(area); }
}
public void PointerReportsLost(int x,int y,bool leftButton,long timestampMs)
{
_webPointerCapture?.BrowserSession?.Pointer(new("pointercancel",-1,-1));
_webPointerCapture = _webHoveredWindow = null;
CancelPointerCapture(); _pressed = _hovered = null; _leftDown = leftButton;
_pointerX = Math.Clamp(x,0,_canvas.Width-1); _pointerY = Math.Clamp(y,0,_canvas.Height-1);
_feedbackNow = Math.Max(_feedbackNow,timestampMs); NeedsRender = true;
}
private void Click(int x, int y, long timestampMs)
{
NeedsRender = true;
if (y >= _canvas.Height - TaskbarHeight)
{
CancelPointerCapture();
if (x < 68) { IsStartMenuOpen = !IsStartMenuOpen; return; }
if (x >= _canvas.Width - 78) { OpenSettings(); return; }
IsStartMenuOpen = false;
var tasks = _windows.OrderBy(w => w.Id).ToArray();
int taskWidth = TaskWidth(tasks.Length), index = (x - 76) / taskWidth;
if (x >= 76 && index >= 0 && index < tasks.Length && x < 76 + tasks.Length * taskWidth)
{
var task = tasks[index];
if (task == ActiveWindow && !task.IsMinimized) Minimize(task.Id); else Activate(task.Id);
}
return;
}
if (IsStartMenuOpen)
{
CancelPointerCapture();
var menu = StartMenuBounds;
if (menu.Contains(x, y))
{
if (SettingsMenuButton.Contains(x, y)) { OpenSettings(); return; }
int item = (y - menu.Y - 54) / 48;
if (y >= menu.Y + 54 && item is >= 0 and < 4) Open((DesktopApplication)item);
return;
}
IsStartMenuOpen = false;
}
for (int i = _windows.Count - 1; i >= 0; i--)
{
var window = _windows[i]; var b = window.Bounds;
if (window.IsMinimized) continue;
if (!b.Contains(x, y)) continue;
Activate(window.Id, preserveTitleSequence: true);
int rx = x - b.X, ry = y - b.Y;
if (ry < 32)
{
if (rx >= b.Width - 64)
{ _pendingWindowButton = new(rx >= b.Width - 32 ? HitKind.WindowClose : HitKind.WindowMinimize, window.Id, 0, new(b.X + b.Width - (rx >= b.Width - 32 ? 32 : 64), b.Y, 32, 32)); return; }
if (_lastTitleId == window.Id && _lastTitleTime >= 0 && timestampMs >= 0 && timestampMs >= _lastTitleTime && timestampMs - _lastTitleTime <= 400
&& Math.Abs(x - _lastTitleX) <= 4 && Math.Abs(y - _lastTitleY) <= 4)
{ _lastTitleId = 0; ToggleMaximize(window.Id); return; }
_lastTitleId = window.Id; _lastTitleX = x; _lastTitleY = y; _lastTitleTime = timestampMs;
_drag = window; _dragX = rx; _dragY = ry; return;
}
_lastTitleId = 0;
if (!window.IsMaximized && !window.IsSnapped && rx >= b.Width - 18 && ry >= b.Height - 18)
{ _resize = window; _resizeBounds = b; _resizeX = x; _resizeY = y; return; }
if (window.Application == DesktopApplication.Files)
{
if (ry is >= 36 and < 64)
{
if (rx < 64) { window.Location = SystemPaths.Parent(window.Location); window.ScrollLine = 0; }
else if (rx < 168) _storage.CreateDirectory(UniquePath(window.Location, "Folder", ""));
else if (rx < 264) { var path = UniquePath(window.Location, "Document", ".txt"); _storage.WriteText(path, ""); Open(DesktopApplication.Editor, path); }
return;
}
int entryIndex = (ry - 96) / 28 + window.ScrollLine;
var entries = _storage.List(window.Location);
int visibleRows = Math.Max(0, (b.Height - 132) / 28);
if (ry >= 96 && ry < 96 + visibleRows * 28 && entryIndex >= 0 && entryIndex < entries.Length)
{
if (entries[entryIndex].IsDirectory) { window.Location = entries[entryIndex].Path; window.ScrollLine = 0; }
else OpenFile(entries[entryIndex].Path);
}
}
else if (window.Application == DesktopApplication.Editor)
{
if (ry < 64) { if (rx < 88) Save(window); else if (rx < 184) Preview(window); return; }
if (window.BrowserSession is not null) { window.BrowserContentFocused = true; return; }
int row = Math.Max(0, (ry - 76) / 20) + window.ScrollLine;
var lines = window.Content.Split('\n'); int at = 0;
for (int line = 0; line < Math.Min(row, lines.Length - 1); line++) at += lines[line].Length + 1;
var currentLine = lines[Math.Min(row, lines.Length - 1)];
int column = Math.Min(window.HorizontalScroll, currentLine.Length), pixels = Math.Max(0, rx - 20);
while (column < currentLine.Length)
{
int width = _canvas.TextWidth(currentLine[column].ToString());
if (pixels < (width + 1) / 2) break;
pixels -= width;
column++;
}
window.Caret = at + column;
}
else if (window.Application == DesktopApplication.Browser && ry < 64) { window.BrowserContentFocused = false; window.AddressSelected = true; window.Status = "Type an address, then press Enter"; }
else if (window.Application == DesktopApplication.Browser) window.BrowserContentFocused = true;
else if (window.Application == DesktopApplication.Terminal && ry < 64)
{
_consoleDemo?.Invoke();
window.Status = _consoleDemo is null ? "Console demo adapter is not installed" : "Console sample finished";
}
else if (window.Application == DesktopApplication.Settings) SettingsClick(window, rx, ry);
return;
}
CancelPointerCapture();
for (int i = 0; i < 5; i++) if (DesktopAppBounds(i).Contains(x, y))
{ if (i == 4) OpenSettings(); else Open((DesktopApplication)i); return; }
}
public void OpenFile(string path)
{
path = SystemPaths.Normalize(path);
if (path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
if (_managedApplication is null) { Notification = "Managed application loader is not connected"; NeedsRender = true; return; }
_managedApplication(path);
}
else Open(DesktopApplication.Editor, path);
}
private DesktopBounds Constrain(DesktopBounds bounds)
{
var area = WorkArea;
int width = Math.Clamp(bounds.Width, Math.Min(MinimumWidth, area.Width), area.Width);
int height = Math.Clamp(bounds.Height, Math.Min(MinimumHeight, area.Height), area.Height);
return new(Math.Clamp(bounds.X, area.X, area.X + area.Width - width),
Math.Clamp(bounds.Y, area.Y, area.Y + area.Height - height), width, height);
}
private void RememberActivation(DesktopWindow window)
{
_mru.Remove(window.Id); _mru.Insert(0, window.Id);
}
public void Activate(int id, bool preserveTitleSequence = false)
{
var window = _windows.Find(w => w.Id == id);
if (window is null) return;
CancelPointerCapture(preserveTitleSequence);
_switchOrder = null;
window.IsMinimized = false;
_desktopHidden.Remove(id);
_windows.Remove(window); _windows.Add(window);
RememberActivation(window);
NeedsRender = true;
}
public void Minimize(int id)
{
var window = _windows.Find(w => w.Id == id);
if (window is null) return;
CancelPointerCapture();
window.IsMinimized = true;
_switchOrder = null;
if (_drag == window) _drag = null;
if (_resize == window) _resize = null;
if (ActiveWindow is { } next) RememberActivation(next);
NeedsRender = true;
}
public void ToggleMaximize(int id)
{
var window = _windows.Find(w => w.Id == id);
if (window is null) return;
_drag = null; _resize = null;
if (window.IsMaximized) RestoreWindow(window);
else
{
if (!window.IsSnapped) window.RestoreBounds = window.Bounds;
window.Bounds = WorkArea; window.IsMaximized = true; window.IsSnapped = false;
}
Activate(id);
}
private void RestoreWindow(DesktopWindow window)
{
CancelPointerCapture();
if (window.IsMaximized || window.IsSnapped) window.Bounds = Constrain(window.RestoreBounds);
window.IsMaximized = false; window.IsSnapped = false;
NeedsRender = true;
}
private void Snap(DesktopWindow window, bool right)
{
CancelPointerCapture();
if (!window.IsMaximized && !window.IsSnapped) window.RestoreBounds = window.Bounds;
var area = WorkArea;
int leftWidth = area.Width / 2, width = right ? area.Width - leftWidth : leftWidth;
// On unusually small displays, a maximized window preserves the application's usable minimum.
if (width < MinimumWidth) { window.Bounds = area; window.IsMaximized = true; window.IsSnapped = false; }
else { window.Bounds = new(right ? area.X + leftWidth : area.X, area.Y, width, area.Height); window.IsMaximized = false; window.IsSnapped = true; }
NeedsRender = true;
}
public void ShowDesktop()
{
CancelPointerCapture();
_switchOrder = null; IsStartMenuOpen = false;
if (_desktopHidden.Count != 0)
{
foreach (var window in _windows) if (_desktopHidden.Contains(window.Id)) window.IsMinimized = false;
_desktopHidden.Clear();
if (ActiveWindow is { } active) RememberActivation(active);
}
else foreach (var window in _windows) if (!window.IsMinimized) { _desktopHidden.Add(window.Id); window.IsMinimized = true; }
NeedsRender = true;
}
/// <summary>Capture MRU once per held-Alt gesture; previews never reorder the cycle.</summary>
public void CycleWindow(bool reverse = false)
{
if (_windows.Count == 0) return;
CancelPointerCapture();
IsStartMenuOpen = false;
if (_switchOrder is null)
{
_switchOrder = _mru.Where(id => _windows.Exists(w => w.Id == id)).ToList();
_switchIndex = ActiveWindow is { } active ? _switchOrder.IndexOf(active.Id) : -1;
if (_switchIndex < 0 && reverse) _switchIndex = 0;
}
if (_switchOrder.Count == 0) return;
_switchIndex = (_switchIndex + (reverse ? -1 : 1) + _switchOrder.Count) % _switchOrder.Count;
var window = _windows.Find(w => w.Id == _switchOrder[_switchIndex])!;
window.IsMinimized = false; _desktopHidden.Remove(window.Id);
_windows.Remove(window); _windows.Add(window);
NeedsRender = true;
}
public void EndWindowSwitch()
{
if (_switchOrder is null) return;
_switchOrder = null;
if (ActiveWindow is { } active) RememberActivation(active);
NeedsRender = true;
}
private DesktopBounds StartMenuBounds => new(12, Math.Max(0, _canvas.Height - TaskbarHeight - 266), Math.Min(300, _canvas.Width - 24), 254);
private void CancelPointerCapture(bool preserveTitleSequence = false)
{
_drag = null; _resize = null;
_pendingWindowButton = null;
if (!preserveTitleSequence) { _lastTitleId = 0; _lastTitleTime = -1; }
}
private int TaskWidth(int count) => Math.Max(1, Math.Min(168, Math.Max(1, _canvas.Width - 176) / Math.Max(1, count)));
private DesktopBounds SettingsMenuButton => new(StartMenuBounds.X + StartMenuBounds.Width - 104, StartMenuBounds.Y + 12, 94, 30);
private DesktopBounds DesktopAppBounds(int index)
{
if (WorkArea.Height >= 554) return new(22, 84 + index * 94, 100, 74);
int height = Math.Clamp((WorkArea.Height - 80) / 5, 40, 74);
int gap = Math.Max(4, Math.Min(12, (WorkArea.Height - 32 - height * 5) / 4));
return new(22, 16 + index * (height + gap), 100, height);
}
public void OpenSettings()
{
IsStartMenuOpen = false;
var existing = _windows.Find(w => w.Application == DesktopApplication.Settings);
if (existing is not null) Activate(existing.Id); else TryOpen(DesktopApplication.Settings);
NeedsRender = true;
}
public void ApplyAppearance(DesktopThemeMode mode, uint accent)
{
_theme = new(mode, accent);
try
{
_theme.Save(_storage);
AppearanceStatus = _storage.IsPersistent ? "Appearance saved to disk" : "Appearance saved to RAM - lost after shutdown";
}
catch (Exception error) when (error is IOException or UnauthorizedAccessException or InvalidOperationException)
{ AppearanceStatus = "Applied for this session; appearance save failed: " + error.Message; }
foreach (var window in _windows.Where(w => w.Application == DesktopApplication.Settings))
{
window.AppearanceAccentText = _theme.AccentHex; window.AppearanceAccentSelected = true;
window.Status = AppearanceStatus;
}
Notification = AppearanceStatus;
NeedsRender = true;
}
private void SettingsClick(DesktopWindow window, int x, int y)
{
if (y is >= 88 and < 122)
{
window.AppearanceFocus = 0;
if (x is >= 24 and < 128) ApplyAppearance(DesktopThemeMode.Light, _theme.Accent);
else if (x is >= 140 and < 244) ApplyAppearance(DesktopThemeMode.Dark, _theme.Accent);
}
else if (y is >= 148 and < 182 && x >= 24)
{
int preset = (x - 24) / 48;
if (preset < DesktopTheme.PresetCount)
{ window.AppearanceFocus = 1; window.AppearancePreset = preset; ApplyAppearance(_theme.Mode, DesktopTheme.Preset(preset).Color); }
}
else if (y is >= 202 and < 236 && x is >= 24 and < 244)
{ window.AppearanceFocus = 2; window.AppearanceAccentSelected = true; window.Status = "Type #RRGGBB, then press Enter or Apply"; }
else if (y is >= 246 and < 280 && x is >= 24 and < 128)
{ window.AppearanceFocus = 3; ApplySettingsAccent(window); }
}
private void ApplySettingsAccent(DesktopWindow window)
{
if (!DesktopTheme.TryParseAccent(window.AppearanceAccentText, out var accent))
{ window.Status = "Invalid color - enter exactly #RRGGBB"; return; }
ApplyAppearance(_theme.Mode, accent);
}
private void SettingsKey(DesktopWindow window, DesktopKey key, char character)
{
if (key == DesktopKey.Tab) { window.AppearanceFocus = (window.AppearanceFocus + 1) % 4; window.AppearanceAccentSelected = window.AppearanceFocus == 2; return; }
if (key == DesktopKey.Save) { ApplySettingsAccent(window); return; }
if (key == DesktopKey.Enter)
{
if (window.AppearanceFocus == 0) ApplyAppearance(_theme.IsDark ? DesktopThemeMode.Light : DesktopThemeMode.Dark, _theme.Accent);
else if (window.AppearanceFocus == 1) ApplyAppearance(_theme.Mode, DesktopTheme.Preset(window.AppearancePreset).Color);
else ApplySettingsAccent(window);
return;
}
if (key is DesktopKey.Left or DesktopKey.Right)
{
if (window.AppearanceFocus == 0) ApplyAppearance(key == DesktopKey.Left ? DesktopThemeMode.Light : DesktopThemeMode.Dark, _theme.Accent);
else if (window.AppearanceFocus == 1)
{ window.AppearancePreset = (window.AppearancePreset + (key == DesktopKey.Left ? -1 : 1) + DesktopTheme.PresetCount) % DesktopTheme.PresetCount; ApplyAppearance(_theme.Mode, DesktopTheme.Preset(window.AppearancePreset).Color); }
return;
}
if (window.AppearanceFocus != 2) return;
if (key is DesktopKey.Backspace or DesktopKey.Delete)
{ window.AppearanceAccentText = window.AppearanceAccentSelected ? "" : window.AppearanceAccentText.Length > 0 ? window.AppearanceAccentText[..^1] : ""; window.AppearanceAccentSelected = false; }
else if (key == DesktopKey.Character && (character == '#' || Uri.IsHexDigit(character)))
{
if (window.AppearanceAccentSelected) window.AppearanceAccentText = "";
window.AppearanceAccentSelected = false;
if (window.AppearanceAccentText.Length < 7) window.AppearanceAccentText += char.ToUpperInvariant(character);
}
}
public void AdvanceFeedback(long monotonicMs)
{
PollEditorPreparations();
monotonicMs = Math.Max(monotonicMs, _feedbackNow);
_feedbackNow = monotonicMs;
if (!_leftDown && _pressed is { } pressed && monotonicMs >= _feedbackExpires)
{ _pressed = null; InvalidateScene(pressed.Bounds); }
UpdateChromeTargets(monotonicMs);
for (int index = 0; index < _windows.Count;)
{
var window = _windows[index];
if (window.BrowserSession is { } page && (!window.IsMinimized || page is IBackgroundEditorSession) && page.AdvanceTime(monotonicMs))
{ UpdateBrowserPage(window); InvalidateScene(BrowserViewport(window)); }
// Ordered readback callbacks (for example F5) can open a preview.
// They run on this UI thread and may change the window collection.
if (index < _windows.Count && ReferenceEquals(_windows[index], window)) index++;
}
}
public bool BrowserKey(BrowserKeyboardInput input)
{
if (!HasBrowserContentFocus || ActiveWindow is not { } window) return false;
bool accepted = window.BrowserSession!.Key(input);
if (accepted && window.Application == DesktopApplication.Editor && window.BrowserSession is IBackgroundEditorSession && input.Type == "keydown" &&
input.Key is not ("Shift" or "Control" or "Alt" or "Meta" or "CapsLock" or "NumLock" or "ScrollLock") &&
(input.Control || input.Alt || input.Text.Length != 0 || input.Key is not
("ArrowLeft" or "ArrowRight" or "ArrowUp" or "ArrowDown" or "Home" or "End" or "PageUp" or "PageDown" or
"Shift" or "Control" or "Alt" or "Meta" or "CapsLock" or "NumLock" or "ScrollLock" or "Escape")))
{ window.IsDirty = true; window.EditorInputVersion++; }
UpdateBrowserPage(window); InvalidateScene(BrowserViewport(window)); return accepted;
}
private static DesktopBounds BrowserViewport(DesktopWindow w) => new(w.Bounds.X + 2, w.Bounds.Y + 67, w.Bounds.Width - 4, Math.Max(1, w.Bounds.Height - 96));
private static void UpdateBrowserPage(DesktopWindow window)
{
if (window.Application == DesktopApplication.Editor)
{
if (window.BrowserSession is IBackgroundEditorSession { Diagnostics.Error: { } backgroundError })
window.Status = "Editor worker: " + backgroundError;
try
{
var text = window.BrowserSession!.ReadEditorText();
if (text is not null && text != window.Content) { window.Content = text; window.IsDirty = true; }
}
catch (IOException error) { window.Status = "Editor read failed: " + error.Message; window.IsDirty = true; }
return;
}
var page = window.BrowserSession!.CurrentPage;
window.Title = "Browser - " + page.Title; window.Content = page.Text; window.Location = page.Address;
}
private void UpdateChromeTargets(long monotonicMs)
{
foreach (var window in _windows)
{
bool visible = !window.IsMinimized;
bool hover = visible && _hovered is { } target && target.WindowId == window.Id;
bool pressed = visible && _pressed is { } button && button.WindowId == window.Id && (_leftDown || _feedbackNow < _feedbackExpires);
var changed = window.ChromeAnimation.SetTargets(monotonicMs,
visible && (hover && _hovered!.Value.Kind == HitKind.WindowMinimize || pressed && _pressed!.Value.Kind == HitKind.WindowMinimize),
visible && (hover && _hovered!.Value.Kind == HitKind.WindowClose || pressed && _pressed!.Value.Kind == HitKind.WindowClose),
visible && (hover && _hovered!.Value.Kind == HitKind.WindowTitle || _drag == window),
visible && !window.IsMaximized && !window.IsSnapped && (hover && _hovered!.Value.Kind == HitKind.WindowResize || _resize == window));
if (!visible) continue;
if ((changed & DesktopChromeAnimationChanges.Minimize) != 0) InvalidateScene(DesktopWindowChromeRenderer.CaptionDamage(window.Bounds, false));
if ((changed & DesktopChromeAnimationChanges.Close) != 0) InvalidateScene(DesktopWindowChromeRenderer.CaptionDamage(window.Bounds, true));
if ((changed & DesktopChromeAnimationChanges.TitleHint) != 0) InvalidateScene(DesktopWindowChromeRenderer.TitleDamage(window.Bounds));
if ((changed & DesktopChromeAnimationChanges.Resize) != 0) InvalidateScene(DesktopWindowChromeRenderer.ResizeDamage(window.Bounds));
}
}
private enum HitKind
{
DesktopApp, StartButton, TaskButton, TaskSettings, StartItem, MenuSettings,
WindowTitle, WindowMinimize, WindowClose, WindowResize, FilesUp, NewFolder, NewFile, FileEntry,
EditorSave, EditorPreview, TerminalDemo, BrowserAddress, SettingsLight, SettingsDark, SettingsPreset, SettingsAccent, SettingsApply
}
private readonly record struct HitTarget(HitKind Kind, int WindowId, int Index, DesktopBounds Bounds);
private static bool SameTarget(HitTarget? first, HitTarget? second) => first is { } a && second is { } b && a.Kind == b.Kind && a.WindowId == b.WindowId && a.Index == b.Index;
private void UpdateHover(HitTarget? target)
{
if (SameTarget(_hovered, target) && _hovered!.Value.Bounds == target!.Value.Bounds || _hovered is null && target is null) return;
if (_hovered is { } old) InvalidateScene(old.Bounds);
_hovered = target;
if (target is { } current) InvalidateScene(current.Bounds);
}
private HitTarget? ResolveTarget(int x, int y)
{
if (y >= _canvas.Height - TaskbarHeight)
{
if (x < 68) return new(HitKind.StartButton, 0, 0, new(10, _canvas.Height - 41, 52, 34));
if (x >= _canvas.Width - 78) return new(HitKind.TaskSettings, 0, 0, new(_canvas.Width - 78, _canvas.Height - 41, 70, 34));
var tasks = _windows.OrderBy(w => w.Id).ToArray(); int width = TaskWidth(tasks.Length), index = (x - 76) / width;
if (x >= 76 && index >= 0 && index < tasks.Length && x < 76 + tasks.Length * width) return new(HitKind.TaskButton, tasks[index].Id, 0, new(76 + index * width, _canvas.Height - 41, width, 34));
return null;
}
if (IsStartMenuOpen && StartMenuBounds.Contains(x, y))
{
if (SettingsMenuButton.Contains(x, y)) return new(HitKind.MenuSettings, 0, 0, SettingsMenuButton);
int item = (y - StartMenuBounds.Y - 54) / 48;
return y >= StartMenuBounds.Y + 54 && item is >= 0 and < 4 ? new(HitKind.StartItem, 0, item, new(StartMenuBounds.X + 10, StartMenuBounds.Y + 51 + item * 48, StartMenuBounds.Width - 20, 42)) : null;
}
var window = _windows.FindLast(w => !w.IsMinimized && w.Bounds.Contains(x, y));
if (window is null)
{
for (int item = 0; item < 5; item++) if (DesktopAppBounds(item).Contains(x, y)) return new(HitKind.DesktopApp, 0, item, DesktopAppBounds(item));
return null;
}
var b = window.Bounds; int rx = x - b.X, ry = y - b.Y;
if (ry < 32)
{
if (rx >= b.Width - 32) return new(HitKind.WindowClose, window.Id, 0, new(b.X + b.Width - 32, b.Y, 32, 32));
if (rx >= b.Width - 64) return new(HitKind.WindowMinimize, window.Id, 0, new(b.X + b.Width - 64, b.Y, 32, 32));
return new(HitKind.WindowTitle, window.Id, 0, new(b.X + 1, b.Y + 1, b.Width - 66, 31));
}
if (!window.IsMaximized && !window.IsSnapped && rx >= b.Width - 18 && ry >= b.Height - 18) return new(HitKind.WindowResize, window.Id, 0, new(b.X + b.Width - 18, b.Y + b.Height - 18, 18, 18));
if (window.Application == DesktopApplication.Files)
{
if (ry is >= 36 and < 64)
{
if (rx < 64) return new(HitKind.FilesUp, window.Id, 0, new(b.X + 4, b.Y + 35, 60, 30));
if (rx < 168) return new(HitKind.NewFolder, window.Id, 0, new(b.X + 64, b.Y + 35, 104, 30));
if (rx < 264) return new(HitKind.NewFile, window.Id, 0, new(b.X + 168, b.Y + 35, 96, 30));
}
int row = (ry - 96) / 28, visible = Math.Max(0, (b.Height - 132) / 28);
if (ry >= 96 && row >= 0 && row < visible && row + window.ScrollLine < _storage.List(window.Location).Length) return new(HitKind.FileEntry, window.Id, row + window.ScrollLine, new(b.X + 8, b.Y + 96 + row * 28, b.Width - 16, 28));
}
if (ry is >= 33 and < 64)
{
if (window.Application == DesktopApplication.Editor && rx < 184) return new(rx < 88 ? HitKind.EditorSave : HitKind.EditorPreview, window.Id, 0, new(b.X + (rx < 88 ? 4 : 88), b.Y + 35, rx < 88 ? 84 : 96, 30));
if (window.Application == DesktopApplication.Terminal) return new(HitKind.TerminalDemo, window.Id, 0, new(b.X + 4, b.Y + 35, b.Width - 8, 30));
if (window.Application == DesktopApplication.Browser) return new(HitKind.BrowserAddress, window.Id, 0, new(b.X + 4, b.Y + 35, b.Width - 8, 30));
}
if (window.Application == DesktopApplication.Settings)
{
if (ry is >= 88 and < 122 && rx is >= 24 and < 128) return new(HitKind.SettingsLight, window.Id, 0, new(b.X + 24, b.Y + 88, 104, 34));
if (ry is >= 88 and < 122 && rx is >= 140 and < 244) return new(HitKind.SettingsDark, window.Id, 0, new(b.X + 140, b.Y + 88, 104, 34));
if (ry is >= 148 and < 182 && rx >= 24 && rx < 24 + DesktopTheme.PresetCount * 48) { int preset = (rx - 24) / 48; return new(HitKind.SettingsPreset, window.Id, preset, new(b.X + 24 + preset * 48, b.Y + 148, 40, 34)); }
if (ry is >= 202 and < 236 && rx is >= 24 and < 244) return new(HitKind.SettingsAccent, window.Id, 0, new(b.X + 24, b.Y + 202, 220, 34));
if (ry is >= 246 and < 280 && rx is >= 24 and < 128) return new(HitKind.SettingsApply, window.Id, 0, new(b.X + 24, b.Y + 246, 104, 34));
}
return null;
}
private void Button(HitKind kind, int windowId, int index, DesktopBounds bounds, uint background, int radius = 8)
{
var target = new HitTarget(kind, windowId, index, bounds);
bool pressed = SameTarget(_pressed, target) && (_leftDown || _feedbackNow < _feedbackExpires), hovered = SameTarget(_hovered, target);
uint color = pressed ? DesktopTheme.Mix(_theme.Panel, _theme.Accent, _theme.IsDark ? 28 : 24) : hovered ? _theme.Selection : background;
RoundedFill(color, bounds.X, bounds.Y, bounds.Width, bounds.Height, radius);
if (pressed || hovered) _canvas.Line(Accent, bounds.X + radius, bounds.Y + bounds.Height - 2, bounds.X + bounds.Width - radius - 1, bounds.Y + bounds.Height - 2);
}
private bool IsPressed(HitKind kind, int windowId) => _pressed is { } pressed && pressed.Kind == kind && pressed.WindowId == windowId && (_leftDown || _feedbackNow < _feedbackExpires);
public void Key(DesktopKey key, char character = '\0')
{
InvalidateScene(ActiveWindow is { } active ? ShadowBounds(active.Bounds) : null);
switch (key)
{
case DesktopKey.StartMenu: CancelPointerCapture(); IsStartMenuOpen = !IsStartMenuOpen; _startSelection = 0; NeedsRender = true; return;
case DesktopKey.ShowDesktop: ShowDesktop(); return;
case DesktopKey.OpenFiles: TryOpen(DesktopApplication.Files); return;
case DesktopKey.SwitchWindow: CycleWindow(); return;
case DesktopKey.SwitchWindowBack: CycleWindow(true); return;
case DesktopKey.EndWindowSwitch: EndWindowSwitch(); return;
}
if (IsStartMenuOpen)
{
NeedsRender = true;
if (key == DesktopKey.Escape) IsStartMenuOpen = false;
else if (key == DesktopKey.Down || key == DesktopKey.Tab) _startSelection = (_startSelection + 1) % 5;
else if (key == DesktopKey.Up) _startSelection = (_startSelection + 4) % 5;
else if (key == DesktopKey.Enter) { if (_startSelection == 4) OpenSettings(); else TryOpen((DesktopApplication)_startSelection); }
return;
}
var window = ActiveWindow;
if (window is null) return;
switch (key)
{
case DesktopKey.CloseWindow: Close(window.Id); return;
case DesktopKey.Minimize: Minimize(window.Id); return;
case DesktopKey.Maximize: if (!window.IsMaximized) ToggleMaximize(window.Id); return;
case DesktopKey.Restore: if (window.IsMaximized || window.IsSnapped) RestoreWindow(window); else Minimize(window.Id); return;
case DesktopKey.SnapLeft: Snap(window, false); return;
case DesktopKey.SnapRight: Snap(window, true); return;
}
if (key == DesktopKey.Escape) { window.IsDirty = false; Close(window.Id); return; }
if (window.Application == DesktopApplication.Settings) { SettingsKey(window, key, character); return; }
if (key == DesktopKey.Run) { Preview(window); return; }
if (window.Application == DesktopApplication.Editor && key == DesktopKey.Save) { Save(window); return; }
if (window.Application == DesktopApplication.Editor && window.BrowserSession is not null)
{
string name = key switch { DesktopKey.Character => character.ToString(), DesktopKey.Enter => "Enter", DesktopKey.Backspace => "Backspace", DesktopKey.Delete => "Delete", DesktopKey.Left => "ArrowLeft", DesktopKey.Right => "ArrowRight", DesktopKey.Up => "ArrowUp", DesktopKey.Down => "ArrowDown", DesktopKey.Home => "Home", DesktopKey.End => "End", DesktopKey.Tab => "Tab", _ => "" };
if (name.Length != 0) { BrowserKey(new("keydown", name, Text: key == DesktopKey.Character ? character.ToString() : "")); BrowserKey(new("keyup", name)); }
return;
}
if (window.Application == DesktopApplication.Browser)
{
if (key == DesktopKey.FocusAddress) { window.BrowserContentFocused = false; window.AddressSelected = true; window.Status = "Address selected"; }
else if (window.BrowserContentFocused && window.BrowserSession is not null)
{
string name = key switch { DesktopKey.Character => character.ToString(), DesktopKey.Enter => "Enter", DesktopKey.Backspace => "Backspace", DesktopKey.Delete => "Delete", DesktopKey.Left => "ArrowLeft", DesktopKey.Right => "ArrowRight", DesktopKey.Up => "ArrowUp", DesktopKey.Down => "ArrowDown", DesktopKey.Home => "Home", DesktopKey.End => "End", DesktopKey.Tab => "Tab", _ => "" };
if (name.Length != 0) { BrowserKey(new("keydown", name, Text: key == DesktopKey.Character ? character.ToString() : "")); BrowserKey(new("keyup", name)); }
}
else if (key == DesktopKey.Enter) { Navigate(window); window.AddressSelected = false; }
else if (key == DesktopKey.Backspace && window.Location.Length > 0)
{ window.Location = window.AddressSelected ? "" : window.Location[..^1]; window.AddressSelected = false; }
else if (key == DesktopKey.Character && !char.IsControl(character) && window.Location.Length < 2048)
{ if (window.AddressSelected) window.Location = ""; window.AddressSelected = false; window.Location += character; }
return;
}
if (window.Application != DesktopApplication.Editor) return;
int at = window.Caret;
switch (key)
{
case DesktopKey.Save: Save(window); return;
case DesktopKey.Left: window.Caret = Math.Max(0, at - 1); break;
case DesktopKey.Right: window.Caret = Math.Min(window.Content.Length, at + 1); break;
case DesktopKey.Home: window.Caret = at == 0 ? 0 : window.Content.LastIndexOf('\n', at - 1) + 1; break;
case DesktopKey.End: int next = window.Content.IndexOf('\n', at); window.Caret = next < 0 ? window.Content.Length : next; break;
case DesktopKey.Up: VerticalCaret(window, -1); break;
case DesktopKey.Down: VerticalCaret(window, 1); break;
case DesktopKey.Backspace: if (at > 0) { window.Content = window.Content.Remove(at - 1, 1); window.Caret--; window.IsDirty = true; } break;
case DesktopKey.Delete: if (at < window.Content.Length) { window.Content = window.Content.Remove(at, 1); window.IsDirty = true; } break;
case DesktopKey.Enter: Insert(window, "\n"); break;
case DesktopKey.Tab: Insert(window, " "); break;
case DesktopKey.Character: if (!char.IsControl(character)) Insert(window, character.ToString()); break;
}
int row = 0;
for (int i = 0; i < window.Caret; i++) if (window.Content[i] == '\n') row++;
int rows = ContentRows(window);
if (row < window.ScrollLine) window.ScrollLine = row;
else if (row >= window.ScrollLine + rows) window.ScrollLine = row - rows + 1;
var lineStart = window.Caret == 0 ? 0 : window.Content.LastIndexOf('\n', window.Caret - 1) + 1;
int col = window.Caret - lineStart, availablePixels = Math.Max(8, window.Bounds.Width - 48);
if (col < window.HorizontalScroll) window.HorizontalScroll = col;
else
{
int width = _canvas.TextWidth(window.Content.Substring(lineStart + window.HorizontalScroll, col - window.HorizontalScroll));
while (width >= availablePixels && window.HorizontalScroll < col)
width -= _canvas.TextWidth(window.Content[lineStart + window.HorizontalScroll++].ToString());
}
}
private void TryOpen(DesktopApplication application)
{
try { Open(application); }
catch (InvalidOperationException error) { Notification = error.Message; }
}
private static void VerticalCaret(DesktopWindow window, int delta)
{
int start = window.Caret == 0 ? 0 : window.Content.LastIndexOf('\n', window.Caret - 1) + 1;
int column = window.Caret - start;
if (delta < 0 && start > 0) { int previous = start < 2 ? 0 : window.Content.LastIndexOf('\n', start - 2) + 1; window.Caret = Math.Min(previous + column, start - 1); }
if (delta > 0) { int end = window.Content.IndexOf('\n', window.Caret); if (end >= 0) { int after = window.Content.IndexOf('\n', end + 1); window.Caret = Math.Min(end + 1 + column, after < 0 ? window.Content.Length : after); } }
}
private static void Insert(DesktopWindow window, string value)
{
if (window.Content.Length + value.Length > 256 * 1024) { window.Status = "Editor size limit reached"; return; }
window.Content = window.Content.Insert(window.Caret, value); window.Caret += value.Length; window.IsDirty = true;
}
private void Save(DesktopWindow window)
{
if (window.BrowserSession is IBackgroundEditorSession background)
{
long version = window.EditorInputVersion;
window.Status = "Saving after queued editor input...";
if (!background.RequestEditorReadback((text, error) =>
{
if (!_windows.Contains(window)) return;
if (error is not null || text is null)
{ window.Status = "Save failed: " + (error ?? "Editor model is unavailable."); window.IsDirty = true; }
else
try
{
_storage.WriteText(window.Location, text);
window.Content = background.ReadEditorText() ?? text;
window.IsDirty = window.EditorInputVersion != version || window.Content != text;
window.Status = _storage.IsPersistent ? "Saved to disk" : "Saved to RAM live - lost after shutdown";
Trace?.Invoke("XFE_DESKTOP_SAVED|" + window.Location + "|" + text.Length + "\n");
}
catch (IOException exception) { window.Status = "Save failed: " + exception.Message; window.IsDirty = true; }
InvalidateScene(window.Bounds);
})) window.Status = "Save could not be queued; retry when the editor worker is available.";
return;
}
try
{
if (window.BrowserSession?.ReadEditorText() is { } text) window.Content = text;
_storage.WriteText(window.Location, window.Content);
window.IsDirty = false;
window.Status = _storage.IsPersistent ? "Saved to disk" : "Saved to RAM live - lost after shutdown";
}
catch (IOException error) { window.Status = "Save failed: " + error.Message; window.IsDirty = true; }
}
private void Preview(DesktopWindow window)
{
if (window.BrowserSession is IBackgroundEditorSession background)
{
window.Status = "Preparing preview after queued editor input...";
if (!background.RequestEditorReadback((source, error) =>
{
if (!_windows.Contains(window)) return;
if (error is not null || source is null) { window.Status = "Preview failed: " + error; InvalidateScene(window.Bounds); return; }
window.Content = source; PreviewCore(window, source);
})) window.Status = "Preview could not be queued; retry when the editor worker is available.";
return;
}
PreviewCore(window, null);
}
private void PreviewCore(DesktopWindow window, string? readback)
{
if (_browser is null) { window.Status = "Document/JavaScript engine is not connected to this kernel yet"; return; }
if (_windows.Count >= 12) { window.Status = "Close a window before opening a preview"; return; }
var preview = OpenCore(DesktopApplication.Browser, null, false);
try
{
if (readback is not null) window.Content = readback;
else if (window.BrowserSession?.ReadEditorText() is { } text) window.Content = text;
if (preview.BrowserSession is { } page)
{
page.BeginDocumentPreview(window.Content, window.Location, _theme.IsDark);
UpdateBrowserPage(preview); preview.BrowserScroll = 0;
preview.Status = "Website document styles and scripts | " + window.Location;
return;
}
preview.Location = "Preview: " + window.Location;
preview.Content = window.Location.EndsWith(".js", StringComparison.OrdinalIgnoreCase)
? _browser is IJavaScriptPreviewService isolated ? isolated.PreviewJavaScript(window.Content) : "An isolated JavaScript preview adapter is required"
: _browser.PreviewMarkdown(window.Content);
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
preview.BrowserSession = null; preview.BrowserContentFocused = false;
preview.Title = "Preview failed"; preview.Content = "Preview failed: " + error.Message;
preview.Status = "The source file was preserved";
}
}
private void Navigate(DesktopWindow window)
{
if (_browser is null)
{
window.Content = "XFESystem Browser\n\nThe native HTTP/TLS, HTML and JavaScript adapters are being ported.\n\nThis kernel does not claim a working internet browser yet.";
window.Status = "Browser backend unavailable"; return;
}
try
{
window.BrowserSession ??= (_browser as IInteractiveBrowserFactory)?.CreatePageSession();
window.BrowserSession?.SetCanvas(_canvas);
window.BrowserSession?.ResizeViewport(BrowserViewport(window).Width, BrowserViewport(window).Height);
var page = window.BrowserSession?.Navigate(window.Location) ?? _browser.Navigate(window.Location);
window.Title = "Browser - " + page.Title; window.Content = page.Text; window.Location = page.Address; window.BrowserScroll = 0;
}
catch (Exception error) { window.Content = "Navigation failed: " + error.Message; window.Status = "Page could not be loaded"; }
}
private string UniquePath(string directory, string stem, string extension)
{
for (int i = 1; i <= 1024; i++) { string path = SystemPaths.Combine(directory, stem + i + extension); if (!_storage.Exists(path)) return path; }
throw new IOException("No available name remains.");
}
private static int ContentRows(DesktopWindow w) => Math.Max(1, (w.Bounds.Height - 112) / 20);
private DesktopBounds ScrollDamage(DesktopWindow window)
{
if (window.Application is not (DesktopApplication.Editor or DesktopApplication.Browser)) return ShadowBounds(window.Bounds);
int viewportWidth = Math.Max(8, window.Bounds.Width - 48);
if (!ReferenceEquals(window.MeasuredContent, window.Content) || window.MeasuredHorizontalScroll != window.HorizontalScroll ||
window.MeasuredViewportWidth != viewportWidth)
{
int widest = 8;
foreach (string line in window.Content.Split('\n'))
{
int left = Math.Min(line.Length, window.HorizontalScroll);
widest = Math.Max(widest, _canvas.TextWidth(line[left..]));
if (widest >= viewportWidth) break;
}
window.MeasuredContent = window.Content;
window.MeasuredHorizontalScroll = window.HorizontalScroll;
window.MeasuredViewportWidth = viewportWidth;
window.MeasuredContentWidth = Math.Min(widest + 4, viewportWidth);
}
// Wheel scrolling changes glyphs and the caret, not the title, toolbar,
// status, or blank columns to the right of the longest document line.
return new(window.Bounds.X + 18, window.Bounds.Y + 74, window.MeasuredContentWidth + 6, ContentRows(window) * 20 + 4);
}
public void Render()
{
LastFrameCompleted = false;
_editorSourceFrames.Clear();
try
{
RenderCore();
if (LastFrameCompleted)
{
long presentedAt = System.Diagnostics.Stopwatch.GetTimestamp();
ReportPresentedEditorSources(presentedAt);
foreach (var window in _windows)
if (!window.IsMinimized) window.BrowserSession?.FramePresented();
}
}
catch (DesktopRenderYieldException)
{
RenderYieldCount++;
if (_canvas is IDesktopSceneMotionCanvas motionCanvas) motionCanvas.AbortScenePreservation();
_canvas.BeginScene(new(0, 0, _canvas.Width, _canvas.Height));
NeedsRender = true;
}
}
private void RenderCore()
{
if (!NeedsRender) return;
UpdateHover(ResolveTarget(_pointerX, _pointerY));
UpdateChromeTargets(_feedbackNow);
var pointerBounds = PointerBounds;
if (!_sceneDirty && _canvas.SupportsSceneCache)
{
_canvas.RestoreSceneRegion(_renderedPointerBounds);
_canvas.RestoreSceneRegion(pointerBounds);
DrawPointer();
_canvas.Present(Union(_renderedPointerBounds, pointerBounds));
LastFrameCompleted = true;
_renderedPointerBounds = pointerBounds;
NeedsRender = false;
return;
}
var fullScreen = new DesktopBounds(0, 0, _canvas.Width, _canvas.Height);
bool partial = _canvas.SupportsSceneCache && _sceneDamage is not null;
var damage = partial ? _sceneDamage!.Value : fullScreen;
_canvas.BeginScene(damage);
if (partial && _pendingMotion && _motionSourceValid && ActiveWindow is { } moving && moving.Id == _motionSceneOwner &&
_canvas is IDesktopSceneMotionCanvas motionCanvas)
{
var source = ReusableMotionBody(moving.Bounds); var target = MotionBody(moving.Bounds);
bool unchanged = _motionIsResize && !Intersects(source, _renderedPointerBounds) && !Intersects(source, pointerBounds);
motionCanvas.PreserveTranslatedSceneRegion(source, target.X, target.Y, unchanged);
}
DesktopWallpaper.Draw(_canvas, _theme, damage, WorkArea.Height);
for (int i = 0; i < 5; i++)
{
var icon = DesktopAppBounds(i);
RoundedFill(_theme.Shadow, icon.X, icon.Y + 2, icon.Width, icon.Height, 14);
Button(HitKind.DesktopApp, 0, i, icon, Panel, 14);
DrawIcon(ApplicationIcon((DesktopApplication)i), icon.X + 33, icon.Y + (icon.Height >= 66 ? 10 : 3), icon.Height >= 66 ? 32 : 24);
_canvas.Text(((DesktopApplication)i).ToString(), White, icon.X + 14, icon.Y + icon.Height - 22);
}
foreach (var window in _windows)
if (!window.IsMinimized && Intersects(ShadowBounds(window.Bounds), damage)) DrawWindow(window, damage);
DrawTaskbar();
if (IsStartMenuOpen) DrawStartMenu();
if (_switchOrder is not null) DrawWindowSwitcher();
if (_canvas is IDesktopSceneMotionCanvas composedMotion) composedMotion.EndScenePreservation();
if (_canvas.SupportsSceneCache) _canvas.SaveScene(damage);
CaptureMotionScene();
_canvas.BeginScene(fullScreen);
if (partial && !_pointerDirty && !Intersects(damage, _renderedPointerBounds) && !Intersects(damage, pointerBounds))
{
_canvas.Present(damage);
LastFrameCompleted = true;
_renderedPointerBounds = pointerBounds;
NeedsRender = false;
return;
}
if (_canvas.SupportsSceneCache)
{
_canvas.RestoreSceneRegion(_renderedPointerBounds);
_canvas.RestoreSceneRegion(pointerBounds);
}
DrawPointer();
if (partial) _canvas.Present(Union(damage, Union(_renderedPointerBounds, pointerBounds))); else _canvas.Present();
LastFrameCompleted = true;
_renderedPointerBounds = pointerBounds; NeedsRender = false;
}
private void DrawWindow(DesktopWindow window, DesktopBounds damage)
{
var b = window.Bounds;
RoundedFill(_theme.Shadow, b.X + 4, b.Y + 6, b.Width, b.Height, 18);
RoundedFill(window == ActiveWindow ? _theme.ActiveBorder : Border, b.X, b.Y, b.Width, b.Height, 18);
RoundedFill(Panel, b.X + 1, b.Y + 1, b.Width - 2, b.Height - 2, 17);
uint title = window == ActiveWindow ? _theme.ActiveTitle : Panel;
RoundedFill(title, b.X + 1, b.Y + 1, b.Width - 2, 48, 17);
_canvas.Fill(title, b.X + 1, b.Y + 18, b.Width - 2, 14);
_canvas.Fill(_theme.Surface, b.X + 1, b.Y + 32, b.Width - 2, b.Height - 61);
DrawIcon(ApplicationIcon(window.Application), b.X + 12, b.Y + 7, 18);
_canvas.Text(Clip(window.Title, Math.Max(0, (b.Width / 2 - 86) / 8)), White, b.X + 38, b.Y + 10);
_canvas.Fill(Panel, b.X + 1, b.Y + 33, b.Width - 2, 32);
if (window.Application == DesktopApplication.Files)
{
Button(HitKind.FilesUp, window.Id, 0, new(b.X + 4, b.Y + 35, 60, 30), Panel);
Button(HitKind.NewFolder, window.Id, 0, new(b.X + 64, b.Y + 35, 104, 30), Panel);
Button(HitKind.NewFile, window.Id, 0, new(b.X + 168, b.Y + 35, 96, 30), Panel);
DrawIcon(DesktopIcon.Folder, b.X + 12, b.Y + 39, 18);
_canvas.Text("Up", TextColor, b.X + 34, b.Y + 43);
DrawIcon(DesktopIcon.Folder, b.X + 68, b.Y + 39, 18);
_canvas.Text("New Folder", TextColor, b.X + 90, b.Y + 43);
DrawIcon(DesktopIcon.TextFile, b.X + 176, b.Y + 39, 18);
_canvas.Text("New File", TextColor, b.X + 198, b.Y + 43);
_canvas.Text(Clip(window.Location, (b.Width - 32) / 8), Muted, b.X + 14, b.Y + 76);
var entries = _storage.List(window.Location);
for (int i = window.ScrollLine; i < Math.Min(entries.Length, window.ScrollLine + (b.Height - 132) / 28); i++)
{
int y = b.Y + 100 + (i - window.ScrollLine) * 28;
Button(HitKind.FileEntry, window.Id, i, new(b.X + 8, y - 4, b.Width - 16, 28), _theme.Surface, 6);
DrawIcon(FileIcon(entries[i].Path, entries[i].IsDirectory), b.X + 18, y - 4, 22);
_canvas.Text(Clip(entries[i].Name, (b.Width - 76) / 8), White, b.X + 58, y);
}
}
else if (window.Application == DesktopApplication.Settings) DrawSettings(window);
else
{
if (window.Application == DesktopApplication.Editor)
{
Button(HitKind.EditorSave, window.Id, 0, new(b.X + 4, b.Y + 35, 84, 30), Panel);
Button(HitKind.EditorPreview, window.Id, 0, new(b.X + 88, b.Y + 35, 96, 30), Panel);
DrawIcon(DesktopIcon.TextFile, b.X + 12, b.Y + 39, 18); _canvas.Text("Save", TextColor, b.X + 34, b.Y + 43);
DrawIcon(DesktopIcon.Browser, b.X + 92, b.Y + 39, 18); _canvas.Text("Preview", TextColor, b.X + 114, b.Y + 43);
if (b.Width >= 400) _canvas.Text("Ctrl+S / F5", Muted, b.X + 208, b.Y + 43);
}
else
{
Button(window.Application == DesktopApplication.Terminal ? HitKind.TerminalDemo : HitKind.BrowserAddress, window.Id, 0, new(b.X + 4, b.Y + 35, b.Width - 8, 30), Panel);
DrawIcon(window.Application == DesktopApplication.Terminal ? DesktopIcon.ApplicationFile : DesktopIcon.Browser, b.X + 12, b.Y + 39, 18);
var toolbar = window.Application == DesktopApplication.Terminal ? "Run Demo" : Clip(window.Location, (b.Width - 58) / 8);
_canvas.Text(toolbar, TextColor, b.X + 38, b.Y + 43);
}
if (window.Application == DesktopApplication.Terminal) _canvas.Fill(TerminalBackground, b.X + 2, b.Y + 67, b.Width - 4, b.Height - 96);
if (window.BrowserSession is { } page)
if (Intersects(BrowserViewport(window), damage)) page.Paint(_canvas, BrowserViewport(window), window.BrowserScroll);
var lines = window.BrowserSession is null ? window.Content.Replace("\r", "").Split('\n') : [];
int sourceLinesDrawn = 0, sourceFirstY = 0, sourceLastY = 0, sourceWidth = 0;
for (int i = window.ScrollLine; i < Math.Min(lines.Length, window.ScrollLine + ContentRows(window)); i++)
{
string line = lines[i];
int left = Math.Min(line.Length, window.HorizontalScroll);
var visible = Clip(line[left..], (b.Width - 48) / 8);
int y = b.Y + 76 + (i - window.ScrollLine) * 20;
if (window.Application == DesktopApplication.Editor && IsCode(window.Location))
foreach (var span in SyntaxHighlighter.Tokenize(visible)) _canvas.Text(span.Text, LightSyntaxColor(span.Color), b.X + 20 + _canvas.TextWidth(visible[..span.Start]), y);
else _canvas.Text(visible, window.Application == DesktopApplication.Terminal ? TerminalForeground : White, b.X + 20, y);
if (window == ActiveWindow && window.EditorPreparation is not null && window.BrowserSession is null && !string.IsNullOrWhiteSpace(visible) &&
Intersects(new(b.X + 20, y, Math.Max(1, _canvas.TextWidth(visible)), 20), damage))
{
if (sourceLinesDrawn++ == 0) sourceFirstY = y;
sourceLastY = y; sourceWidth = Math.Max(sourceWidth, _canvas.TextWidth(visible));
}
}
if (sourceLinesDrawn != 0)
_editorSourceFrames.Add(new(window, window.EditorPreparationGeneration, window.Content, sourceLinesDrawn,
new(b.X + 20, sourceFirstY, sourceWidth, sourceLastY - sourceFirstY + 20)));
if (window.Application == DesktopApplication.Terminal && window.Terminal is { } terminal)
{
int rows = Math.Min(terminal.Rows, ContentRows(window)), columns = Math.Min(terminal.Columns, (b.Width - 40) / 8);
_canvas.Fill(TerminalBackground, b.X + 2, b.Y + 67, b.Width - 4, b.Height - 96);
for (int row = 0; row < rows; row++)
for (int col = 0; col < columns; col++)
{
var cell = terminal.GetCell(col, row);
var fg = cell.Style.DisplayForeground; var bg = cell.Style.DisplayBackground;
uint foreground = (uint)(fg.Red << 16 | fg.Green << 8 | fg.Blue), background = (uint)(bg.Red << 16 | bg.Green << 8 | bg.Blue);
int x = b.X + 20 + col * 8, y = b.Y + 76 + row * 20;
_canvas.Fill(background, x, y, 8 * Math.Max(1, cell.DisplayWidth), 20);
}
for (int row = 0; row < rows; row++)
for (int col = 0; col < columns; col++)
{
var cell = terminal.GetCell(col, row); var fg = cell.Style.DisplayForeground;
uint foreground = (uint)(fg.Red << 16 | fg.Green << 8 | fg.Blue);
int x = b.X + 20 + col * 8, y = b.Y + 76 + row * 20;
if (!cell.IsContinuation) _canvas.Text(cell.Text, foreground, x, y);
if (cell.Style.Underline) _canvas.Line(foreground, x, y + 17, x + 8 * Math.Max(1, cell.DisplayWidth) - 1, y + 17);
}
}
if (window == ActiveWindow && window.Application == DesktopApplication.Editor && window.BrowserSession is null)
{
int row = 0, col = 0;
for (int i = 0; i < window.Caret; i++) if (window.Content[i] == '\n') { row++; col = 0; } else col++;
if (row >= window.ScrollLine && row < window.ScrollLine + ContentRows(window))
{
int start = window.Caret - col + Math.Min(col, window.HorizontalScroll);
int caretX = b.X + 20 + _canvas.TextWidth(window.Content.Substring(start, window.Caret - start));
if (caretX >= b.X + 20 && caretX < b.X + b.Width - 28)
_canvas.Line(Accent, caretX, b.Y + 76 + (row - window.ScrollLine) * 20,
caretX, b.Y + 92 + (row - window.ScrollLine) * 20);
}
}
}
_canvas.Fill(Panel, b.X + 1, b.Y + b.Height - 28, b.Width - 2, 10);
_canvas.Text(Clip((window.IsDirty ? "Modified | " : "") + window.Status, (b.Width - 48) / 8), Muted, b.X + 14, b.Y + b.Height - 20);
var chrome = window.ChromeAnimation;
DesktopWindowChromeRenderer.Draw(_canvas, b, _theme, window == ActiveWindow, chrome.MinimizeProgress,
chrome.CloseProgress, chrome.TitleHintProgress, chrome.ResizeProgress, window.IsMaximized || window.IsSnapped,
IsPressed(HitKind.WindowMinimize, window.Id), IsPressed(HitKind.WindowClose, window.Id));
}
private void DrawTaskbar()
{
int y = _canvas.Height - TaskbarHeight;
_canvas.Fill(Panel, 0, y, _canvas.Width, TaskbarHeight);
_canvas.Line(Border, 0, y, _canvas.Width - 1, y);
Button(HitKind.StartButton, 0, 0, new(10, y + 7, 52, 34), IsStartMenuOpen ? _theme.Selection : _theme.SoftAccent, 10);
if (_canvas is IBrandedDesktopCanvas branded) branded.BrandIcon(25, y + 12, 24);
else _canvas.Text("XFE", TextColor, 23, y + 18);
var tasks = _windows.OrderBy(w => w.Id).ToArray();
int width = TaskWidth(tasks.Length);
for (int i = 0; i < tasks.Length; i++)
{
var window = tasks[i]; int x = 76 + i * width;
bool active = window == ActiveWindow;
Button(HitKind.TaskButton, window.Id, 0, new(x, y + 7, Math.Max(1, width - 6), 34), active ? _theme.Selection : _theme.SoftAccent, 9);
if (width >= 30) DrawIcon(ApplicationIcon(window.Application), x + 8, y + 13, 22);
if (width >= 80) _canvas.Text(Clip(window.Title, (width - 46) / 8), window.IsMinimized ? Muted : White, x + 36, y + 18);
_canvas.Line(active ? Accent : Muted, x + 8, y + 40, x + Math.Max(8, width - 14), y + 40);
}
if (tasks.Length == 0) _canvas.Text(Clip(Notification, (_canvas.Width - 184) / 8), Muted, 84, y + 18);
Button(HitKind.TaskSettings, 0, 0, new(_canvas.Width - 78, y + 7, 70, 34), _theme.SoftAccent, 9);
DrawIcon(ApplicationIcon(DesktopApplication.Settings), _canvas.Width - 54, y + 13, 22);
}
private void DrawStartMenu()
{
var b = StartMenuBounds;
RoundedFill(_theme.Shadow, b.X + 4, b.Y + 5, b.Width, b.Height, 18);
RoundedFill(Panel, b.X, b.Y, b.Width, b.Height, 18);
_canvas.Text("XFESystem", TextColor, b.X + 18, b.Y + 20);
Button(HitKind.MenuSettings, 0, 0, SettingsMenuButton, _startSelection == 4 ? _theme.Selection : _theme.SoftAccent);
_canvas.Text("Settings", White, SettingsMenuButton.X + 12, SettingsMenuButton.Y + 9);
for (int i = 0; i < 4; i++)
{
int y = b.Y + 54 + i * 48;
Button(HitKind.StartItem, 0, i, new(b.X + 10, y - 3, b.Width - 20, 42), i == _startSelection ? _theme.Selection : Panel, 10);
DrawIcon((DesktopIcon)i, b.X + 20, y + 2, 28);
_canvas.Text(((DesktopApplication)i).ToString(), White, b.X + 64, y + 10);
}
}
private void DrawWindowSwitcher()
{
var order = _switchOrder!;
int width = Math.Min(_canvas.Width - 32, Math.Max(240, order.Count * 126 + 24));
int x = (_canvas.Width - width) / 2, y = _canvas.Height / 2 - 54;
RoundedFill(_theme.Shadow, x + 4, y + 6, width, 108, 18);
RoundedFill(Panel, x, y, width, 108, 18);
int cell = (width - 24) / order.Count;
for (int i = 0; i < order.Count; i++)
{
var window = _windows.Find(w => w.Id == order[i]);
if (window is null) continue;
int itemX = x + 12 + cell * i;
if (i == _switchIndex) RoundedFill(_theme.Selection, itemX, y + 10, cell - 4, 88, 12);
DrawIcon(ApplicationIcon(window.Application), itemX + Math.Max(4, (cell - 36) / 2), y + 20, Math.Min(36, cell - 8));
_canvas.Text(Clip(window.Title, Math.Max(1, (cell - 12) / 8)), White, itemX + 6, y + 72);
}
}
private void DrawIcon(DesktopIcon icon, int x, int y, int size)
{
if (_canvas is IDesktopIconCanvas icons) icons.DrawIcon(icon, x, y, size);
}
private static DesktopIcon ApplicationIcon(DesktopApplication app) => app == DesktopApplication.Settings ? DesktopIcon.Settings : (DesktopIcon)app;
private void DrawSettings(DesktopWindow window)
{
var b = window.Bounds;
void Label(string text, uint color, int x, int y)
{ if (y + 20 <= b.Y + b.Height - 28) _canvas.Text(Clip(text, (b.Width - x + b.X - 24) / 8), color, x, y); }
void Option(HitKind kind, int index, int x, int y, int width, bool selected, string? text = null)
{
if (y + 34 > b.Height - 28) return;
var bounds = new DesktopBounds(b.X + x, b.Y + y, width, 34);
Button(kind, window.Id, index, bounds, selected ? _theme.Selection : Panel);
if (selected) _canvas.Line(Accent, bounds.X + 7, bounds.Y + 32, bounds.X + bounds.Width - 8, bounds.Y + 32);
if (text is not null) _canvas.Text(text, White, bounds.X + 16, bounds.Y + 11);
}
Label("Appearance", TextColor, b.X + 24, b.Y + 45);
Label("Theme", Muted, b.X + 24, b.Y + 67);
Option(HitKind.SettingsLight, 0, 24, 88, 104, !_theme.IsDark, "Light");
Option(HitKind.SettingsDark, 0, 140, 88, 104, _theme.IsDark, "Dark");
Label("Accent color", Muted, b.X + 24, b.Y + 127);
for (int i = 0; i < DesktopTheme.PresetCount; i++)
{
uint color = DesktopTheme.Preset(i).Color;
Option(HitKind.SettingsPreset, i, 24 + i * 48, 148, 40, _theme.Accent == color);
if (182 <= b.Height - 28) RoundedFill(color, b.X + 34 + i * 48, b.Y + 156, 20, 18, 5);
}
Label("Custom #RRGGBB", Muted, b.X + 24, b.Y + 183);
Option(HitKind.SettingsAccent, 0, 24, 202, 220, window.AppearanceFocus == 2, window.AppearanceAccentText + (window.AppearanceFocus == 2 ? " |" : ""));
Option(HitKind.SettingsApply, 0, 24, 246, 104, window.AppearanceFocus == 3, "Apply");
Label("Tab: focus Enter: apply", Muted, b.X + 24, b.Y + 298);
Label(_storage.IsPersistent ? "Settings are stored on disk" : "RAM live: settings reset after shutdown", Muted, b.X + 24, b.Y + 324);
Label("Current accent: " + _theme.AccentHex, TextColor, b.X + 24, b.Y + 350);
}
private static DesktopIcon FileIcon(string path, bool directory)
{
if (directory) return DesktopIcon.Folder;
if (path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) return DesktopIcon.ApplicationFile;
if (path.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) return DesktopIcon.MarkdownFile;
return IsCode(path) ? DesktopIcon.CodeFile : DesktopIcon.TextFile;
}
private void RoundedFill(uint color, int x, int y, int width, int height, int radius)
=> DesktopChromeRenderer.RoundedFill(_canvas, color, x, y, width, height, radius);
private DesktopPointerShape PointerShape
{
get
{
if (_resize is not null) return DesktopPointerShape.Resize;
if (_pointerY >= _canvas.Height - TaskbarHeight ||
IsStartMenuOpen && StartMenuBounds.Contains(_pointerX, _pointerY)) return DesktopPointerShape.Arrow;
var hit = _windows.FindLast(w => !w.IsMinimized && w.Bounds.Contains(_pointerX, _pointerY));
if (hit is null) return DesktopPointerShape.Arrow;
if (hit.BrowserSession?.TextCursor == true && BrowserViewport(hit).Contains(_pointerX,_pointerY)) return DesktopPointerShape.Text;
if (!hit.IsMaximized && !hit.IsSnapped && _pointerX >= hit.Bounds.X + hit.Bounds.Width - 18 &&
_pointerY >= hit.Bounds.Y + hit.Bounds.Height - 18) return DesktopPointerShape.Resize;
int x = _pointerX - hit.Bounds.X, y = _pointerY - hit.Bounds.Y;
bool text = hit.Application == DesktopApplication.Editor && y >= 70 && y < hit.Bounds.Height - 28 ||
hit.Application == DesktopApplication.Browser && y >= 33 && y < 65 ||
hit.Application == DesktopApplication.Settings && x >= 24 && x < 244 && y >= 202 && y < 236;
return text ? DesktopPointerShape.Text : DesktopPointerShape.Arrow;
}
}
private void DrawPointer() => DesktopPointerRenderer.Draw(_canvas, _pointerX, _pointerY, PointerShape, _theme.AccentStroke);
private DesktopBounds PointerBounds
{
get
{
var bounds = DesktopPointerRenderer.GetBounds(_pointerX, _pointerY, PointerShape);
int left = Math.Max(0, bounds.X), top = Math.Max(0, bounds.Y);
int right = Math.Min(_canvas.Width, bounds.X + bounds.Width), bottom = Math.Min(_canvas.Height, bounds.Y + bounds.Height);
return new(left, top, Math.Max(0, right - left), Math.Max(0, bottom - top));
}
}
private static DesktopBounds Union(DesktopBounds first, DesktopBounds second)
{
int x = Math.Min(first.X, second.X), y = Math.Min(first.Y, second.Y);
return new(x, y, Math.Max(first.X + first.Width, second.X + second.Width) - x, Math.Max(first.Y + first.Height, second.Y + second.Height) - y);
}
private DesktopBounds ShadowBounds(DesktopBounds bounds)
{
int left = Math.Max(0, bounds.X - 1), top = Math.Max(0, bounds.Y - 1);
return new(left, top, Math.Min(_canvas.Width, bounds.X + bounds.Width + 8) - left, Math.Min(_canvas.Height, bounds.Y + bounds.Height + 9) - top);
}
private void InvalidateScene(DesktopBounds? damage)
{
if (!_motionInvalidation && (damage is null || _motionSourceValid && ActiveWindow is { } active &&
(Intersects(damage.Value, ReusableMotionBody(active.Bounds)) ||
Intersects(damage.Value, ReusableMotionBody(active.Bounds) with { X = active.Bounds.X + 2, Y = active.Bounds.Y + 65 }))))
_motionSourceValid = false;
if (damage is null) { NeedsRender = true; return; }
if (!_sceneDirty) _sceneDamage = damage;
else if (_sceneDamage is { } previous) _sceneDamage = Union(previous, damage.Value);
_sceneDirty = true;
}
private static DesktopBounds MotionBody(DesktopBounds bounds) => new(bounds.X + 2, bounds.Y + 65, Math.Max(0, bounds.Width - 4), Math.Max(0, bounds.Height - 109));
private DesktopBounds ReusableMotionBody(DesktopBounds target) => _motionIsResize ?
new(_motionSceneBounds.X + 2, _motionSceneBounds.Y + 65,
Math.Max(0, Math.Min(_motionSceneBounds.Width, target.Width) -
(ActiveWindow is { Application: DesktopApplication.Editor } editor && IsCode(editor.Location) ? 160 : 96)),
// A complete bottom 64px guard also covers both layouts' partial
// last row at every 20/28px visible-row threshold, not only chrome.
Math.Max(0, Math.Min(_motionSceneBounds.Height, target.Height) - 129)) : MotionBody(_motionSceneBounds);
private void InvalidateWindowMotion(DesktopWindow window, DesktopBounds previous, bool resizing = false)
{
_pendingMotion = _motionSourceValid && resizing == _motionIsResize && window.Id == _motionSceneOwner &&
(resizing ? window.Application != DesktopApplication.Settings && window.BrowserSession is null && window.Bounds.X == _motionSceneBounds.X && window.Bounds.Y == _motionSceneBounds.Y :
window.Bounds.Width == _motionSceneBounds.Width && window.Bounds.Height == _motionSceneBounds.Height);
_motionInvalidation = true;
try { InvalidateScene(Union(ShadowBounds(previous), ShadowBounds(window.Bounds))); }
finally { _motionInvalidation = false; }
}
private void CaptureMotionScene()
{
_pendingMotion = false;
var moving = _drag ?? _resize;
_motionSourceValid = _canvas.SupportsSceneCache && _canvas is IDesktopSceneMotionCanvas && _leftDown &&
moving is not null && moving == ActiveWindow && !IsStartMenuOpen && _switchOrder is null &&
_hovered is { } target && target.WindowId == moving.Id &&
(target.Kind == HitKind.WindowTitle && _drag == moving ||
target.Kind == HitKind.WindowResize && _resize == moving && moving.Application != DesktopApplication.Settings && moving.BrowserSession is null);
if (_motionSourceValid) { _motionSceneOwner = moving!.Id; _motionSceneBounds = moving.Bounds; _motionIsResize = _resize == moving; }
}
private uint LightSyntaxColor(uint color) => _theme.SyntaxColor(color);
private static bool IsCode(string path) => path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".js", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".json", StringComparison.OrdinalIgnoreCase);
private string Clip(string text, int max)
{
int available = Math.Max(0, max) * 8;
if (_canvas.TextWidth(text) <= available) return text;
int suffixWidth = _canvas.TextWidth("...");
if (suffixWidth > available) return "";
int length = 0, width = suffixWidth;
while (length < text.Length)
{
int next = _canvas.TextWidth(text[length].ToString());
if (width + next > available) break;
width += next;
length++;
}
return text[..length] + "...";
}
private static bool Intersects(DesktopBounds first, DesktopBounds second) => first.X < second.X + second.Width &&
first.X + first.Width > second.X && first.Y < second.Y + second.Height && first.Y + first.Height > second.Y;
}
using XFESystem.Core;
namespace XFESystem.Desktop;
/// <summary>Shared managed desktop. All filesystem and browser operations go through explicit guest adapters.</summary>
public sealed class DesktopSession
{
private DesktopTheme _theme;
private uint Background => _theme.Background;
private uint Panel => _theme.Panel;
private uint Border => _theme.Border;
private uint TextColor => _theme.AccentText;
private uint White => _theme.Text;
private uint Muted => _theme.Muted;
private uint Accent => _theme.AccentStroke;
private const uint TerminalBackground = 0x0C1321, TerminalForeground = 0x75C9F5;
private const int TaskbarHeight = 48, MinimumWidth = 320, MinimumHeight = 200;
private readonly IDesktopCanvas _canvas;
private readonly IWorkspaceStorage _storage;
private readonly IBrowserService? _browser;
private readonly Action? _consoleDemo;
private readonly Action<string>? _managedApplication;
private readonly Func<long>? _monotonicMilliseconds;
private readonly IEditorPreparationService? _editorPreparation;
private readonly Func<int>? _threadIdentity;
private readonly List<EditorSourceFrame> _editorSourceFrames = [];
private long _nextEditorPreparationGeneration;
public Action<string>? Trace { get; init; }
private readonly List<DesktopWindow> _windows = [];
private readonly List<int> _mru = [];
private readonly HashSet<int> _desktopHidden = [];
private List<int>? _switchOrder;
private int _switchIndex;
private int _startSelection;
private int _nextId = 1, _pointerX, _pointerY, _dragX, _dragY;
private int _resizeX, _resizeY, _lastTitleId, _lastTitleX, _lastTitleY;
private long _lastTitleTime = -1;
private bool _leftDown;
private bool _sceneDirty = true, _pointerDirty;
private DesktopBounds? _sceneDamage;
private DesktopBounds _renderedPointerBounds;
private DesktopWindow? _drag, _resize;
private DesktopWindow? _webPointerCapture, _webHoveredWindow;
private int _motionSceneOwner;
private DesktopBounds _motionSceneBounds;
private bool _motionSourceValid, _pendingMotion, _motionInvalidation, _motionIsResize;
private DesktopBounds _resizeBounds;
private HitTarget? _hovered, _pressed, _pendingWindowButton;
private long _feedbackNow, _feedbackExpires;
public DesktopTheme Theme => _theme;
public bool IsAppearancePersistent => _storage.IsPersistent;
public string AppearanceStatus { get; private set; } = "";
public IReadOnlyList<DesktopWindow> Windows => _windows;
public DesktopWindow? ActiveWindow => _windows.FindLast(w => !w.IsMinimized);
public DesktopBounds WorkArea => new(0, 0, _canvas.Width, Math.Max(1, _canvas.Height - TaskbarHeight));
public bool IsStartMenuOpen { get; private set; }
public bool IsWindowSwitching => _switchOrder is not null;
public bool HasBrowserContentFocus => !IsStartMenuOpen && ActiveWindow is { BrowserContentFocused: true, BrowserSession: not null };
public bool NeedsRender
{
get => _sceneDirty || _pointerDirty;
private set { if (value) { _sceneDirty = true; _sceneDamage = null; _motionSourceValid = false; } else { _sceneDirty = false; _pointerDirty = false; _sceneDamage = null; } }
}
public bool LastFrameCompleted { get; private set; }
public long RenderYieldCount { get; private set; }
public string Notification { get; private set; } = "RAM live - files stay in memory for this session";
public DesktopSession(IDesktopCanvas canvas, IWorkspaceStorage storage, IBrowserService? browser = null, Action? consoleDemo = null, Action<string>? managedApplication = null, Func<long>? monotonicMilliseconds = null, IEditorPreparationService? editorPreparation = null, Func<int>? threadIdentity = null)
{
_canvas = canvas;
_storage = storage;
_browser = browser;
_consoleDemo = consoleDemo;
_managedApplication = managedApplication;
_monotonicMilliseconds = monotonicMilliseconds;
_editorPreparation = editorPreparation;
_threadIdentity = threadIdentity;
_theme = DesktopTheme.Load(storage, out var appearanceError);
AppearanceStatus = appearanceError ?? (_storage.IsPersistent ? "Appearance settings are stored on disk" : "Appearance settings are stored in RAM for this session");
if (appearanceError is not null) Notification = appearanceError;
}
public DesktopWindow Open(DesktopApplication app, string? location = null) => OpenCore(app, location, true);
private DesktopWindow OpenCore(DesktopApplication app, string? location, bool navigateBrowser)
{
if (_windows.Count >= 12) throw new InvalidOperationException("Close a window before opening another.");
CancelPointerCapture();
var offset = (_nextId - 1) % 5 * 22;
int width = Math.Min(WorkArea.Width, Math.Max(MinimumWidth, Math.Min(800, _canvas.Width - 190)));
int height = Math.Min(WorkArea.Height, Math.Max(MinimumHeight, Math.Min(500, _canvas.Height - 120)));
var window = new DesktopWindow { Id = _nextId++, Application = app,
Bounds = Constrain(new(150 + offset, 66 + offset, width, height)), Title = app.ToString() };
window.RestoreBounds = window.Bounds;
_windows.Add(window);
_switchOrder = null;
RememberActivation(window);
IsStartMenuOpen = false;
if (app == DesktopApplication.Files) window.Location = location ?? SystemPaths.Documents;
else if (app == DesktopApplication.Editor)
{
window.Location = location ?? UniquePath(SystemPaths.Documents, "Document", ".txt");
window.Content = _storage.Exists(window.Location) ? _storage.ReadText(window.Location) : "";
window.Title = "Editor - " + SystemPaths.Name(window.Location);
if ((window.Location.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) || window.Location.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) && _browser is IInteractiveBrowserFactory factory)
{
if (_editorPreparation is not null) BeginEditorPreparation(window);
else
{
try
{
window.BrowserSession = factory.CreatePageSession();
window.BrowserSession.SetCanvas(_canvas);
window.BrowserSession.ResizeViewport(BrowserViewport(window).Width, BrowserViewport(window).Height);
window.BrowserSession.NavigateEditor(window.Content, window.Location);
window.BrowserContentFocused = true;
window.Status = "Monaco | Ctrl+S saves to " + window.Location;
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
window.BrowserSession = null;
window.Status = "Basic text editor | Monaco unavailable: " + error.Message;
}
}
}
}
else if (app == DesktopApplication.Browser)
{
window.Location = location ?? "about:Start";
window.AddressSelected = true;
if (navigateBrowser) Navigate(window);
else if (_browser is IInteractiveBrowserFactory factory)
{
window.BrowserSession = factory.CreatePageSession();
window.BrowserSession.SetCanvas(_canvas);
window.BrowserSession.ResizeViewport(BrowserViewport(window).Width, BrowserViewport(window).Height);
}
}
else if (app == DesktopApplication.Terminal) { window.Content = "XFESystem | XFEstudio\nC# NativeAOT development session\n\nClick Run Demo to execute the built-in C# console sample.\n"; }
else if (app == DesktopApplication.Settings)
{
window.Title = "Settings - Appearance"; window.AppearanceAccentText = _theme.AccentHex;
window.AppearanceAccentSelected = true; window.Status = AppearanceStatus;
}
NeedsRender = true;
return window;
}
private void BeginEditorPreparation(DesktopWindow window)
{
var viewport = BrowserViewport(window);
var request = new EditorPreparationRequest(window.Id, ++_nextEditorPreparationGeneration,
window.Content, window.Location, viewport.Width, viewport.Height, _monotonicMilliseconds?.Invoke() ?? _feedbackNow);
window.EditorPreparationGeneration = request.Generation;
window.EditorPreparationStarted = System.Diagnostics.Stopwatch.GetTimestamp();
window.BrowserSession = null; window.BrowserContentFocused = false;
window.Status = "Loading real Monaco in background | text editing remains available";
try { window.EditorPreparation = _editorPreparation!.Begin(request); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ window.EditorPreparation = null; window.Status = "Basic text editor | Background Monaco unavailable: " + error.Message; }
}
private void PollEditorPreparations()
{
foreach (var window in _windows)
{
if (window.EditorPreparation is not { } operation || !operation.TryTakeResult(out var result)) continue;
window.EditorPreparation = null;
if (window.Content != operation.Request.Source || window.Location != operation.Request.Path)
{
operation.Cancel();
TraceEditorPreparation("Superseded", operation.Request, result.WorkerThreadId);
BeginEditorPreparation(window);
InvalidateScene(window.Bounds);
continue;
}
if (result.Page is null)
{
window.Status = "Basic text editor | Background Monaco unavailable: " + result.Error;
TraceEditorPreparation("FailedUi", operation.Request, result.WorkerThreadId);
}
else
{
try
{
// The UI receives a proxy and independent delivery fonts.
// Its actual engine remains exclusively on the worker.
bool equivalentMetrics = result.PreparedCanvas is IEditorPreparationCanvasHandoff handoff && handoff.TryCompleteEditorPreparationHandoff(_canvas);
result.Page.CompleteEditorPreparationHandoff(_canvas, equivalentMetrics);
var viewport = BrowserViewport(window);
result.Page.ResizeViewport(viewport.Width, viewport.Height);
window.BrowserSession = result.Page;
window.BrowserContentFocused = true;
window.Status = "Monaco | Ctrl+S saves to " + window.Location;
TraceEditorPreparation("Ready", operation.Request, result.WorkerThreadId);
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
window.BrowserSession = null; window.BrowserContentFocused = false;
window.Status = "Basic text editor | Prepared Monaco unavailable: " + error.Message;
TraceEditorPreparation("FailedUi", operation.Request, result.WorkerThreadId);
}
}
InvalidateScene(window.Bounds);
}
}
private void TraceEditorPreparation(string state, EditorPreparationRequest request, int workerThread)
{
if (Trace is null) return;
var window = _windows.Find(candidate => candidate.Id == request.WindowId);
long elapsed = window is null ? 0 : System.Diagnostics.Stopwatch.GetTimestamp() - window.EditorPreparationStarted;
Trace("XFE_EDITOR_BACKGROUND|" + state + "|Window|" + request.WindowId + "|Generation|" + request.Generation +
"|UiThread|" + (_threadIdentity?.Invoke() ?? Environment.CurrentManagedThreadId) + "|Thread|" + workerThread +
"|SourceCharacters|" + request.Source.Length + "|ElapsedTicks|" + elapsed + "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");
}
private readonly record struct EditorSourceFrame(DesktopWindow Window, long Generation, string Source, int NonEmptyLines, DesktopBounds TextBounds);
private void ReportPresentedEditorSources(long presentedAt)
{
foreach (var frame in _editorSourceFrames)
{
var window = frame.Window;
if (window.EditorSourcePresentedGeneration == frame.Generation) continue;
window.EditorSourcePresentedGeneration = frame.Generation;
if (Trace is null) continue;
ulong hash = 14695981039346656037UL;
foreach (char character in frame.Source)
{
hash = unchecked((hash ^ (byte)character) * 1099511628211UL);
hash = unchecked((hash ^ (byte)(character >> 8)) * 1099511628211UL);
}
Trace("XFE_EDITOR_BACKGROUND|SourcePresented|Window|" + window.Id + "|Generation|" + frame.Generation +
"|UiThread|" + (_threadIdentity?.Invoke() ?? Environment.CurrentManagedThreadId) +
"|SourceCharacters|" + frame.Source.Length + "|FingerprintAlgorithm|UTF16LE-FNV1a64|SourceFingerprint|" + hash.ToString("x16") +
"|VisibleNonEmptyLines|" + frame.NonEmptyLines + "|TextX|" + frame.TextBounds.X + "|TextY|" + frame.TextBounds.Y +
"|TextWidth|" + frame.TextBounds.Width + "|TextHeight|" + frame.TextBounds.Height +
"|ElapsedTicks|" + (presentedAt - window.EditorPreparationStarted) + "|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");
}
}
public void Close(int id)
{
var window = _windows.Find(w => w.Id == id);
if (window is null) return;
CancelPointerCapture();
if (window.IsDirty) { window.Status = "Unsaved changes - Ctrl+S to save, Escape to discard then close"; NeedsRender = true; return; }
if (window.EditorPreparation is { } preparation)
{
preparation.Cancel(); window.EditorPreparation = null;
TraceEditorPreparation("Cancel", preparation.Request, 0);
}
if (window.BrowserSession is IBackgroundEditorSession background) background.Cancel();
_windows.Remove(window);
if (ReferenceEquals(_webHoveredWindow, window)) _webHoveredWindow = null;
if (ReferenceEquals(_webPointerCapture, window)) _webPointerCapture = null;
window.BrowserSession = null;
_mru.Remove(window.Id);
_desktopHidden.Remove(window.Id);
if (_switchOrder is not null) { _switchOrder = null; if (ActiveWindow is { } next) RememberActivation(next); }
if (_drag == window) _drag = null;
if (_resize == window) _resize = null;
_lastTitleId = 0;
NeedsRender = true;
}
public void SetTerminalSnapshot(XFESystem.Console.TerminalSnapshot snapshot)
{
var window = _windows.FindLast(w => w.Application == DesktopApplication.Terminal) ?? Open(DesktopApplication.Terminal);
window.Terminal = snapshot;
Activate(window.Id);
NeedsRender = true;
}
public void Pointer(int x, int y, bool leftButton, int wheel = 0, long timestampMs = -1)
{
x = Math.Clamp(x, 0, _canvas.Width - 1);
y = Math.Clamp(y, 0, _canvas.Height - 1);
bool webInputChanged = x != _pointerX || y != _pointerY || leftButton != _leftDown || wheel != 0;
if (x != _pointerX || y != _pointerY) _pointerDirty = true;
_pointerX = x; _pointerY = y;
long now = timestampMs >= 0 ? timestampMs : _monotonicMilliseconds?.Invoke() ?? _feedbackNow;
now = Math.Max(now, _feedbackNow);
_feedbackNow = now;
UpdateHover(ResolveTarget(x, y));
// A button-break packet can also carry the last displacement. Apply
// that position to the existing capture before retiring it below.
bool capturedPosition = leftButton || _leftDown;
if (_drag is not null && capturedPosition)
{
var b = _drag.Bounds;
var previous = b;
if (_drag.IsMaximized || _drag.IsSnapped)
{
if (Math.Abs(x - _lastTitleX) <= 4 && Math.Abs(y - _lastTitleY) <= 4) goto DragFinished;
var restore = Constrain(_drag.RestoreBounds);
_dragX = Math.Clamp(_dragX * restore.Width / Math.Max(1, b.Width), 0, restore.Width - 1);
_dragY = Math.Min(_dragY, 24);
_drag.IsMaximized = false; _drag.IsSnapped = false;
b = restore;
}
_drag.Bounds = Constrain(b with { X = x - _dragX, Y = y - _dragY });
_drag.RestoreBounds = _drag.Bounds;
if (Math.Abs(x - _lastTitleX) > 4 || Math.Abs(y - _lastTitleY) > 4) _lastTitleId = 0;
if (previous != _drag.Bounds) InvalidateWindowMotion(_drag, previous);
}
DragFinished:
if (_resize is not null && capturedPosition)
{
var previous = _resize.Bounds;
_resize.Bounds = Constrain(_resizeBounds with
{
Width = Math.Clamp(_resizeBounds.Width + x - _resizeX, Math.Min(MinimumWidth, WorkArea.Width - _resizeBounds.X), WorkArea.Width - _resizeBounds.X),
Height = Math.Clamp(_resizeBounds.Height + y - _resizeY, Math.Min(MinimumHeight, WorkArea.Y + WorkArea.Height - _resizeBounds.Y), WorkArea.Y + WorkArea.Height - _resizeBounds.Y)
});
_resize.RestoreBounds = _resize.Bounds;
if (previous != _resize.Bounds) InvalidateWindowMotion(_resize, previous, resizing: true);
}
if (!leftButton) { _drag = null; _resize = null; }
if (wheel != 0 && ActiveWindow is { } active && active.Application != DesktopApplication.Settings && active.BrowserSession is null)
{
int count = active.Application == DesktopApplication.Files ? _storage.List(active.Location).Length : active.Content.Split('\n').Length;
int rows = active.Application == DesktopApplication.Files ? Math.Max(1, (active.Bounds.Height - 132) / 28) : ContentRows(active);
active.ScrollLine = Math.Clamp(active.ScrollLine - wheel * 3, 0, Math.Max(0, count - rows));
InvalidateScene(ScrollDamage(active));
}
if (leftButton && !_leftDown)
{
_pressed = _hovered; _feedbackExpires = long.MaxValue;
if (_pressed is { } pressed) InvalidateScene(pressed.Bounds);
try { Click(x, y, timestampMs >= 0 ? timestampMs : _monotonicMilliseconds?.Invoke() ?? -1); }
catch (IOException error) { if (ActiveWindow is { } errorWindow) errorWindow.Status = error.Message; }
catch (ArgumentException error) { if (ActiveWindow is { } errorWindow) errorWindow.Status = error.Message; }
catch (InvalidOperationException error) { Notification = error.Message; if (ActiveWindow is { } errorWindow) errorWindow.Status = error.Message; }
}
if (!leftButton && _leftDown)
{
if (_pendingWindowButton is { } pending && SameTarget(pending, ResolveTarget(x, y)))
{ if (pending.Kind == HitKind.WindowMinimize) Minimize(pending.WindowId); else Close(pending.WindowId); }
else if (_pendingWindowButton is not null) _pressed = null;
_pendingWindowButton = null;
_feedbackExpires = now + 120;
if (_pressed is { } pressed) InvalidateScene(pressed.Bounds);
UpdateHover(ResolveTarget(x, y));
}
if (webInputChanged) RouteBrowserPointer(x,y,leftButton,wheel);
_leftDown = leftButton;
// Captured motion can change the hit geometry before the next input
// packet. Retire the old hover rectangle in the same frame.
UpdateHover(ResolveTarget(x, y));
UpdateChromeTargets(now);
}
private void RouteBrowserPointer(int x,int y,bool leftButton,int wheel)
{
if (_webPointerCapture is not null && (!_windows.Contains(_webPointerCapture) || _webPointerCapture.IsMinimized)) _webPointerCapture = null;
var under = _windows.FindLast(w => !w.IsMinimized && w.Bounds.Contains(x,y));
DesktopWindow? target = _webPointerCapture ?? (_drag is null && _resize is null && under is { BrowserSession:not null } && BrowserViewport(under).Contains(x,y) ? under : null);
if (_webHoveredWindow is { BrowserSession:{ } oldPage } old && old != target)
{
long oldRevision = oldPage.Revision; oldPage.Pointer(new("mousemove",-1,-1));
if (oldPage.Revision != oldRevision) InvalidateScene(BrowserViewport(old));
}
_webHoveredWindow = target;
if (target?.BrowserSession is not { } page) return;
var area = BrowserViewport(target); long revision = page.Revision;
string type = leftButton && !_leftDown ? "mousedown" : !leftButton && _leftDown ? "mouseup" : "mousemove";
if (type == "mousedown") { target.BrowserContentFocused = true; _webPointerCapture = target; }
page.Pointer(new(type,x-area.X,y-area.Y,Buttons:leftButton?1:0));
if (type == "mouseup") _webPointerCapture = null;
if (wheel != 0)
{
bool prevented = page.Pointer(new("wheel",x-area.X,y-area.Y,DeltaY:-wheel*60));
if (!prevented) { target.BrowserScroll = Math.Clamp(target.BrowserScroll-wheel*60,0,Math.Max(0,page.DocumentHeight-area.Height)); InvalidateScene(area); }
}
if (page.Revision != revision) { UpdateBrowserPage(target); InvalidateScene(area); }
}
public void PointerReportsLost(int x,int y,bool leftButton,long timestampMs)
{
_webPointerCapture?.BrowserSession?.Pointer(new("pointercancel",-1,-1));
_webPointerCapture = _webHoveredWindow = null;
CancelPointerCapture(); _pressed = _hovered = null; _leftDown = leftButton;
_pointerX = Math.Clamp(x,0,_canvas.Width-1); _pointerY = Math.Clamp(y,0,_canvas.Height-1);
_feedbackNow = Math.Max(_feedbackNow,timestampMs); NeedsRender = true;
}
private void Click(int x, int y, long timestampMs)
{
NeedsRender = true;
if (y >= _canvas.Height - TaskbarHeight)
{
CancelPointerCapture();
if (x < 68) { IsStartMenuOpen = !IsStartMenuOpen; return; }
if (x >= _canvas.Width - 78) { OpenSettings(); return; }
IsStartMenuOpen = false;
var tasks = _windows.OrderBy(w => w.Id).ToArray();
int taskWidth = TaskWidth(tasks.Length), index = (x - 76) / taskWidth;
if (x >= 76 && index >= 0 && index < tasks.Length && x < 76 + tasks.Length * taskWidth)
{
var task = tasks[index];
if (task == ActiveWindow && !task.IsMinimized) Minimize(task.Id); else Activate(task.Id);
}
return;
}
if (IsStartMenuOpen)
{
CancelPointerCapture();
var menu = StartMenuBounds;
if (menu.Contains(x, y))
{
if (SettingsMenuButton.Contains(x, y)) { OpenSettings(); return; }
int item = (y - menu.Y - 54) / 48;
if (y >= menu.Y + 54 && item is >= 0 and < 4) Open((DesktopApplication)item);
return;
}
IsStartMenuOpen = false;
}
for (int i = _windows.Count - 1; i >= 0; i--)
{
var window = _windows[i]; var b = window.Bounds;
if (window.IsMinimized) continue;
if (!b.Contains(x, y)) continue;
Activate(window.Id, preserveTitleSequence: true);
int rx = x - b.X, ry = y - b.Y;
if (ry < 32)
{
if (rx >= b.Width - 64)
{ _pendingWindowButton = new(rx >= b.Width - 32 ? HitKind.WindowClose : HitKind.WindowMinimize, window.Id, 0, new(b.X + b.Width - (rx >= b.Width - 32 ? 32 : 64), b.Y, 32, 32)); return; }
if (_lastTitleId == window.Id && _lastTitleTime >= 0 && timestampMs >= 0 && timestampMs >= _lastTitleTime && timestampMs - _lastTitleTime <= 400
&& Math.Abs(x - _lastTitleX) <= 4 && Math.Abs(y - _lastTitleY) <= 4)
{ _lastTitleId = 0; ToggleMaximize(window.Id); return; }
_lastTitleId = window.Id; _lastTitleX = x; _lastTitleY = y; _lastTitleTime = timestampMs;
_drag = window; _dragX = rx; _dragY = ry; return;
}
_lastTitleId = 0;
if (!window.IsMaximized && !window.IsSnapped && rx >= b.Width - 18 && ry >= b.Height - 18)
{ _resize = window; _resizeBounds = b; _resizeX = x; _resizeY = y; return; }
if (window.Application == DesktopApplication.Files)
{
if (ry is >= 36 and < 64)
{
if (rx < 64) { window.Location = SystemPaths.Parent(window.Location); window.ScrollLine = 0; }
else if (rx < 168) _storage.CreateDirectory(UniquePath(window.Location, "Folder", ""));
else if (rx < 264) { var path = UniquePath(window.Location, "Document", ".txt"); _storage.WriteText(path, ""); Open(DesktopApplication.Editor, path); }
return;
}
int entryIndex = (ry - 96) / 28 + window.ScrollLine;
var entries = _storage.List(window.Location);
int visibleRows = Math.Max(0, (b.Height - 132) / 28);
if (ry >= 96 && ry < 96 + visibleRows * 28 && entryIndex >= 0 && entryIndex < entries.Length)
{
if (entries[entryIndex].IsDirectory) { window.Location = entries[entryIndex].Path; window.ScrollLine = 0; }
else OpenFile(entries[entryIndex].Path);
}
}
else if (window.Application == DesktopApplication.Editor)
{
if (ry < 64) { if (rx < 88) Save(window); else if (rx < 184) Preview(window); return; }
if (window.BrowserSession is not null) { window.BrowserContentFocused = true; return; }
int row = Math.Max(0, (ry - 76) / 20) + window.ScrollLine;
var lines = window.Content.Split('\n'); int at = 0;
for (int line = 0; line < Math.Min(row, lines.Length - 1); line++) at += lines[line].Length + 1;
var currentLine = lines[Math.Min(row, lines.Length - 1)];
int column = Math.Min(window.HorizontalScroll, currentLine.Length), pixels = Math.Max(0, rx - 20);
while (column < currentLine.Length)
{
int width = _canvas.TextWidth(currentLine[column].ToString());
if (pixels < (width + 1) / 2) break;
pixels -= width;
column++;
}
window.Caret = at + column;
}
else if (window.Application == DesktopApplication.Browser && ry < 64) { window.BrowserContentFocused = false; window.AddressSelected = true; window.Status = "Type an address, then press Enter"; }
else if (window.Application == DesktopApplication.Browser) window.BrowserContentFocused = true;
else if (window.Application == DesktopApplication.Terminal && ry < 64)
{
_consoleDemo?.Invoke();
window.Status = _consoleDemo is null ? "Console demo adapter is not installed" : "Console sample finished";
}
else if (window.Application == DesktopApplication.Settings) SettingsClick(window, rx, ry);
return;
}
CancelPointerCapture();
for (int i = 0; i < 5; i++) if (DesktopAppBounds(i).Contains(x, y))
{ if (i == 4) OpenSettings(); else Open((DesktopApplication)i); return; }
}
public void OpenFile(string path)
{
path = SystemPaths.Normalize(path);
if (path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
if (_managedApplication is null) { Notification = "Managed application loader is not connected"; NeedsRender = true; return; }
_managedApplication(path);
}
else Open(DesktopApplication.Editor, path);
}
private DesktopBounds Constrain(DesktopBounds bounds)
{
var area = WorkArea;
int width = Math.Clamp(bounds.Width, Math.Min(MinimumWidth, area.Width), area.Width);
int height = Math.Clamp(bounds.Height, Math.Min(MinimumHeight, area.Height), area.Height);
return new(Math.Clamp(bounds.X, area.X, area.X + area.Width - width),
Math.Clamp(bounds.Y, area.Y, area.Y + area.Height - height), width, height);
}
private void RememberActivation(DesktopWindow window)
{
_mru.Remove(window.Id); _mru.Insert(0, window.Id);
}
public void Activate(int id, bool preserveTitleSequence = false)
{
var window = _windows.Find(w => w.Id == id);
if (window is null) return;
CancelPointerCapture(preserveTitleSequence);
_switchOrder = null;
window.IsMinimized = false;
_desktopHidden.Remove(id);
_windows.Remove(window); _windows.Add(window);
RememberActivation(window);
NeedsRender = true;
}
public void Minimize(int id)
{
var window = _windows.Find(w => w.Id == id);
if (window is null) return;
CancelPointerCapture();
window.IsMinimized = true;
_switchOrder = null;
if (_drag == window) _drag = null;
if (_resize == window) _resize = null;
if (ActiveWindow is { } next) RememberActivation(next);
NeedsRender = true;
}
public void ToggleMaximize(int id)
{
var window = _windows.Find(w => w.Id == id);
if (window is null) return;
_drag = null; _resize = null;
if (window.IsMaximized) RestoreWindow(window);
else
{
if (!window.IsSnapped) window.RestoreBounds = window.Bounds;
window.Bounds = WorkArea; window.IsMaximized = true; window.IsSnapped = false;
}
Activate(id);
}
private void RestoreWindow(DesktopWindow window)
{
CancelPointerCapture();
if (window.IsMaximized || window.IsSnapped) window.Bounds = Constrain(window.RestoreBounds);
window.IsMaximized = false; window.IsSnapped = false;
NeedsRender = true;
}
private void Snap(DesktopWindow window, bool right)
{
CancelPointerCapture();
if (!window.IsMaximized && !window.IsSnapped) window.RestoreBounds = window.Bounds;
var area = WorkArea;
int leftWidth = area.Width / 2, width = right ? area.Width - leftWidth : leftWidth;
// On unusually small displays, a maximized window preserves the application's usable minimum.
if (width < MinimumWidth) { window.Bounds = area; window.IsMaximized = true; window.IsSnapped = false; }
else { window.Bounds = new(right ? area.X + leftWidth : area.X, area.Y, width, area.Height); window.IsMaximized = false; window.IsSnapped = true; }
NeedsRender = true;
}
public void ShowDesktop()
{
CancelPointerCapture();
_switchOrder = null; IsStartMenuOpen = false;
if (_desktopHidden.Count != 0)
{
foreach (var window in _windows) if (_desktopHidden.Contains(window.Id)) window.IsMinimized = false;
_desktopHidden.Clear();
if (ActiveWindow is { } active) RememberActivation(active);
}
else foreach (var window in _windows) if (!window.IsMinimized) { _desktopHidden.Add(window.Id); window.IsMinimized = true; }
NeedsRender = true;
}
/// <summary>Capture MRU once per held-Alt gesture; previews never reorder the cycle.</summary>
public void CycleWindow(bool reverse = false)
{
if (_windows.Count == 0) return;
CancelPointerCapture();
IsStartMenuOpen = false;
if (_switchOrder is null)
{
_switchOrder = _mru.Where(id => _windows.Exists(w => w.Id == id)).ToList();
_switchIndex = ActiveWindow is { } active ? _switchOrder.IndexOf(active.Id) : -1;
if (_switchIndex < 0 && reverse) _switchIndex = 0;
}
if (_switchOrder.Count == 0) return;
_switchIndex = (_switchIndex + (reverse ? -1 : 1) + _switchOrder.Count) % _switchOrder.Count;
var window = _windows.Find(w => w.Id == _switchOrder[_switchIndex])!;
window.IsMinimized = false; _desktopHidden.Remove(window.Id);
_windows.Remove(window); _windows.Add(window);
NeedsRender = true;
}
public void EndWindowSwitch()
{
if (_switchOrder is null) return;
_switchOrder = null;
if (ActiveWindow is { } active) RememberActivation(active);
NeedsRender = true;
}
private DesktopBounds StartMenuBounds => new(12, Math.Max(0, _canvas.Height - TaskbarHeight - 266), Math.Min(300, _canvas.Width - 24), 254);
private void CancelPointerCapture(bool preserveTitleSequence = false)
{
_drag = null; _resize = null;
_pendingWindowButton = null;
if (!preserveTitleSequence) { _lastTitleId = 0; _lastTitleTime = -1; }
}
private int TaskWidth(int count) => Math.Max(1, Math.Min(168, Math.Max(1, _canvas.Width - 176) / Math.Max(1, count)));
private DesktopBounds SettingsMenuButton => new(StartMenuBounds.X + StartMenuBounds.Width - 104, StartMenuBounds.Y + 12, 94, 30);
private DesktopBounds DesktopAppBounds(int index)
{
if (WorkArea.Height >= 554) return new(22, 84 + index * 94, 100, 74);
int height = Math.Clamp((WorkArea.Height - 80) / 5, 40, 74);
int gap = Math.Max(4, Math.Min(12, (WorkArea.Height - 32 - height * 5) / 4));
return new(22, 16 + index * (height + gap), 100, height);
}
public void OpenSettings()
{
IsStartMenuOpen = false;
var existing = _windows.Find(w => w.Application == DesktopApplication.Settings);
if (existing is not null) Activate(existing.Id); else TryOpen(DesktopApplication.Settings);
NeedsRender = true;
}
public void ApplyAppearance(DesktopThemeMode mode, uint accent)
{
_theme = new(mode, accent);
try
{
_theme.Save(_storage);
AppearanceStatus = _storage.IsPersistent ? "Appearance saved to disk" : "Appearance saved to RAM - lost after shutdown";
}
catch (Exception error) when (error is IOException or UnauthorizedAccessException or InvalidOperationException)
{ AppearanceStatus = "Applied for this session; appearance save failed: " + error.Message; }
foreach (var window in _windows.Where(w => w.Application == DesktopApplication.Settings))
{
window.AppearanceAccentText = _theme.AccentHex; window.AppearanceAccentSelected = true;
window.Status = AppearanceStatus;
}
Notification = AppearanceStatus;
NeedsRender = true;
}
private void SettingsClick(DesktopWindow window, int x, int y)
{
if (y is >= 88 and < 122)
{
window.AppearanceFocus = 0;
if (x is >= 24 and < 128) ApplyAppearance(DesktopThemeMode.Light, _theme.Accent);
else if (x is >= 140 and < 244) ApplyAppearance(DesktopThemeMode.Dark, _theme.Accent);
}
else if (y is >= 148 and < 182 && x >= 24)
{
int preset = (x - 24) / 48;
if (preset < DesktopTheme.PresetCount)
{ window.AppearanceFocus = 1; window.AppearancePreset = preset; ApplyAppearance(_theme.Mode, DesktopTheme.Preset(preset).Color); }
}
else if (y is >= 202 and < 236 && x is >= 24 and < 244)
{ window.AppearanceFocus = 2; window.AppearanceAccentSelected = true; window.Status = "Type #RRGGBB, then press Enter or Apply"; }
else if (y is >= 246 and < 280 && x is >= 24 and < 128)
{ window.AppearanceFocus = 3; ApplySettingsAccent(window); }
}
private void ApplySettingsAccent(DesktopWindow window)
{
if (!DesktopTheme.TryParseAccent(window.AppearanceAccentText, out var accent))
{ window.Status = "Invalid color - enter exactly #RRGGBB"; return; }
ApplyAppearance(_theme.Mode, accent);
}
private void SettingsKey(DesktopWindow window, DesktopKey key, char character)
{
if (key == DesktopKey.Tab) { window.AppearanceFocus = (window.AppearanceFocus + 1) % 4; window.AppearanceAccentSelected = window.AppearanceFocus == 2; return; }
if (key == DesktopKey.Save) { ApplySettingsAccent(window); return; }
if (key == DesktopKey.Enter)
{
if (window.AppearanceFocus == 0) ApplyAppearance(_theme.IsDark ? DesktopThemeMode.Light : DesktopThemeMode.Dark, _theme.Accent);
else if (window.AppearanceFocus == 1) ApplyAppearance(_theme.Mode, DesktopTheme.Preset(window.AppearancePreset).Color);
else ApplySettingsAccent(window);
return;
}
if (key is DesktopKey.Left or DesktopKey.Right)
{
if (window.AppearanceFocus == 0) ApplyAppearance(key == DesktopKey.Left ? DesktopThemeMode.Light : DesktopThemeMode.Dark, _theme.Accent);
else if (window.AppearanceFocus == 1)
{ window.AppearancePreset = (window.AppearancePreset + (key == DesktopKey.Left ? -1 : 1) + DesktopTheme.PresetCount) % DesktopTheme.PresetCount; ApplyAppearance(_theme.Mode, DesktopTheme.Preset(window.AppearancePreset).Color); }
return;
}
if (window.AppearanceFocus != 2) return;
if (key is DesktopKey.Backspace or DesktopKey.Delete)
{ window.AppearanceAccentText = window.AppearanceAccentSelected ? "" : window.AppearanceAccentText.Length > 0 ? window.AppearanceAccentText[..^1] : ""; window.AppearanceAccentSelected = false; }
else if (key == DesktopKey.Character && (character == '#' || Uri.IsHexDigit(character)))
{
if (window.AppearanceAccentSelected) window.AppearanceAccentText = "";
window.AppearanceAccentSelected = false;
if (window.AppearanceAccentText.Length < 7) window.AppearanceAccentText += char.ToUpperInvariant(character);
}
}
public void AdvanceFeedback(long monotonicMs)
{
PollEditorPreparations();
monotonicMs = Math.Max(monotonicMs, _feedbackNow);
_feedbackNow = monotonicMs;
if (!_leftDown && _pressed is { } pressed && monotonicMs >= _feedbackExpires)
{ _pressed = null; InvalidateScene(pressed.Bounds); }
UpdateChromeTargets(monotonicMs);
for (int index = 0; index < _windows.Count;)
{
var window = _windows[index];
if (window.BrowserSession is { } page && (!window.IsMinimized || page is IBackgroundEditorSession) && page.AdvanceTime(monotonicMs))
{ UpdateBrowserPage(window); InvalidateScene(BrowserViewport(window)); }
// Ordered readback callbacks (for example F5) can open a preview.
// They run on this UI thread and may change the window collection.
if (index < _windows.Count && ReferenceEquals(_windows[index], window)) index++;
}
}
public bool BrowserKey(BrowserKeyboardInput input)
{
if (!HasBrowserContentFocus || ActiveWindow is not { } window) return false;
bool accepted = window.BrowserSession!.Key(input);
if (accepted && window.Application == DesktopApplication.Editor && window.BrowserSession is IBackgroundEditorSession && input.Type == "keydown" &&
input.Key is not ("Shift" or "Control" or "Alt" or "Meta" or "CapsLock" or "NumLock" or "ScrollLock") &&
(input.Control || input.Alt || input.Text.Length != 0 || input.Key is not
("ArrowLeft" or "ArrowRight" or "ArrowUp" or "ArrowDown" or "Home" or "End" or "PageUp" or "PageDown" or
"Shift" or "Control" or "Alt" or "Meta" or "CapsLock" or "NumLock" or "ScrollLock" or "Escape")))
{ window.IsDirty = true; window.EditorInputVersion++; }
UpdateBrowserPage(window); InvalidateScene(BrowserViewport(window)); return accepted;
}
private static DesktopBounds BrowserViewport(DesktopWindow w) => new(w.Bounds.X + 2, w.Bounds.Y + 67, w.Bounds.Width - 4, Math.Max(1, w.Bounds.Height - 96));
private static void UpdateBrowserPage(DesktopWindow window)
{
if (window.Application == DesktopApplication.Editor)
{
if (window.BrowserSession is IBackgroundEditorSession { Diagnostics.Error: { } backgroundError })
window.Status = "Editor worker: " + backgroundError;
try
{
var text = window.BrowserSession!.ReadEditorText();
if (text is not null && text != window.Content) { window.Content = text; window.IsDirty = true; }
}
catch (IOException error) { window.Status = "Editor read failed: " + error.Message; window.IsDirty = true; }
return;
}
var page = window.BrowserSession!.CurrentPage;
window.Title = "Browser - " + page.Title; window.Content = page.Text; window.Location = page.Address;
}
private void UpdateChromeTargets(long monotonicMs)
{
foreach (var window in _windows)
{
bool visible = !window.IsMinimized;
bool hover = visible && _hovered is { } target && target.WindowId == window.Id;
bool pressed = visible && _pressed is { } button && button.WindowId == window.Id && (_leftDown || _feedbackNow < _feedbackExpires);
var changed = window.ChromeAnimation.SetTargets(monotonicMs,
visible && (hover && _hovered!.Value.Kind == HitKind.WindowMinimize || pressed && _pressed!.Value.Kind == HitKind.WindowMinimize),
visible && (hover && _hovered!.Value.Kind == HitKind.WindowClose || pressed && _pressed!.Value.Kind == HitKind.WindowClose),
visible && (hover && _hovered!.Value.Kind == HitKind.WindowTitle || _drag == window),
visible && !window.IsMaximized && !window.IsSnapped && (hover && _hovered!.Value.Kind == HitKind.WindowResize || _resize == window));
if (!visible) continue;
if ((changed & DesktopChromeAnimationChanges.Minimize) != 0) InvalidateScene(DesktopWindowChromeRenderer.CaptionDamage(window.Bounds, false));
if ((changed & DesktopChromeAnimationChanges.Close) != 0) InvalidateScene(DesktopWindowChromeRenderer.CaptionDamage(window.Bounds, true));
if ((changed & DesktopChromeAnimationChanges.TitleHint) != 0) InvalidateScene(DesktopWindowChromeRenderer.TitleDamage(window.Bounds));
if ((changed & DesktopChromeAnimationChanges.Resize) != 0) InvalidateScene(DesktopWindowChromeRenderer.ResizeDamage(window.Bounds));
}
}
private enum HitKind
{
DesktopApp, StartButton, TaskButton, TaskSettings, StartItem, MenuSettings,
WindowTitle, WindowMinimize, WindowClose, WindowResize, FilesUp, NewFolder, NewFile, FileEntry,
EditorSave, EditorPreview, TerminalDemo, BrowserAddress, SettingsLight, SettingsDark, SettingsPreset, SettingsAccent, SettingsApply
}
private readonly record struct HitTarget(HitKind Kind, int WindowId, int Index, DesktopBounds Bounds);
private static bool SameTarget(HitTarget? first, HitTarget? second) => first is { } a && second is { } b && a.Kind == b.Kind && a.WindowId == b.WindowId && a.Index == b.Index;
private void UpdateHover(HitTarget? target)
{
if (SameTarget(_hovered, target) && _hovered!.Value.Bounds == target!.Value.Bounds || _hovered is null && target is null) return;
if (_hovered is { } old) InvalidateScene(old.Bounds);
_hovered = target;
if (target is { } current) InvalidateScene(current.Bounds);
}
private HitTarget? ResolveTarget(int x, int y)
{
if (y >= _canvas.Height - TaskbarHeight)
{
if (x < 68) return new(HitKind.StartButton, 0, 0, new(10, _canvas.Height - 41, 52, 34));
if (x >= _canvas.Width - 78) return new(HitKind.TaskSettings, 0, 0, new(_canvas.Width - 78, _canvas.Height - 41, 70, 34));
var tasks = _windows.OrderBy(w => w.Id).ToArray(); int width = TaskWidth(tasks.Length), index = (x - 76) / width;
if (x >= 76 && index >= 0 && index < tasks.Length && x < 76 + tasks.Length * width) return new(HitKind.TaskButton, tasks[index].Id, 0, new(76 + index * width, _canvas.Height - 41, width, 34));
return null;
}
if (IsStartMenuOpen && StartMenuBounds.Contains(x, y))
{
if (SettingsMenuButton.Contains(x, y)) return new(HitKind.MenuSettings, 0, 0, SettingsMenuButton);
int item = (y - StartMenuBounds.Y - 54) / 48;
return y >= StartMenuBounds.Y + 54 && item is >= 0 and < 4 ? new(HitKind.StartItem, 0, item, new(StartMenuBounds.X + 10, StartMenuBounds.Y + 51 + item * 48, StartMenuBounds.Width - 20, 42)) : null;
}
var window = _windows.FindLast(w => !w.IsMinimized && w.Bounds.Contains(x, y));
if (window is null)
{
for (int item = 0; item < 5; item++) if (DesktopAppBounds(item).Contains(x, y)) return new(HitKind.DesktopApp, 0, item, DesktopAppBounds(item));
return null;
}
var b = window.Bounds; int rx = x - b.X, ry = y - b.Y;
if (ry < 32)
{
if (rx >= b.Width - 32) return new(HitKind.WindowClose, window.Id, 0, new(b.X + b.Width - 32, b.Y, 32, 32));
if (rx >= b.Width - 64) return new(HitKind.WindowMinimize, window.Id, 0, new(b.X + b.Width - 64, b.Y, 32, 32));
return new(HitKind.WindowTitle, window.Id, 0, new(b.X + 1, b.Y + 1, b.Width - 66, 31));
}
if (!window.IsMaximized && !window.IsSnapped && rx >= b.Width - 18 && ry >= b.Height - 18) return new(HitKind.WindowResize, window.Id, 0, new(b.X + b.Width - 18, b.Y + b.Height - 18, 18, 18));
if (window.Application == DesktopApplication.Files)
{
if (ry is >= 36 and < 64)
{
if (rx < 64) return new(HitKind.FilesUp, window.Id, 0, new(b.X + 4, b.Y + 35, 60, 30));
if (rx < 168) return new(HitKind.NewFolder, window.Id, 0, new(b.X + 64, b.Y + 35, 104, 30));
if (rx < 264) return new(HitKind.NewFile, window.Id, 0, new(b.X + 168, b.Y + 35, 96, 30));
}
int row = (ry - 96) / 28, visible = Math.Max(0, (b.Height - 132) / 28);
if (ry >= 96 && row >= 0 && row < visible && row + window.ScrollLine < _storage.List(window.Location).Length) return new(HitKind.FileEntry, window.Id, row + window.ScrollLine, new(b.X + 8, b.Y + 96 + row * 28, b.Width - 16, 28));
}
if (ry is >= 33 and < 64)
{
if (window.Application == DesktopApplication.Editor && rx < 184) return new(rx < 88 ? HitKind.EditorSave : HitKind.EditorPreview, window.Id, 0, new(b.X + (rx < 88 ? 4 : 88), b.Y + 35, rx < 88 ? 84 : 96, 30));
if (window.Application == DesktopApplication.Terminal) return new(HitKind.TerminalDemo, window.Id, 0, new(b.X + 4, b.Y + 35, b.Width - 8, 30));
if (window.Application == DesktopApplication.Browser) return new(HitKind.BrowserAddress, window.Id, 0, new(b.X + 4, b.Y + 35, b.Width - 8, 30));
}
if (window.Application == DesktopApplication.Settings)
{
if (ry is >= 88 and < 122 && rx is >= 24 and < 128) return new(HitKind.SettingsLight, window.Id, 0, new(b.X + 24, b.Y + 88, 104, 34));
if (ry is >= 88 and < 122 && rx is >= 140 and < 244) return new(HitKind.SettingsDark, window.Id, 0, new(b.X + 140, b.Y + 88, 104, 34));
if (ry is >= 148 and < 182 && rx >= 24 && rx < 24 + DesktopTheme.PresetCount * 48) { int preset = (rx - 24) / 48; return new(HitKind.SettingsPreset, window.Id, preset, new(b.X + 24 + preset * 48, b.Y + 148, 40, 34)); }
if (ry is >= 202 and < 236 && rx is >= 24 and < 244) return new(HitKind.SettingsAccent, window.Id, 0, new(b.X + 24, b.Y + 202, 220, 34));
if (ry is >= 246 and < 280 && rx is >= 24 and < 128) return new(HitKind.SettingsApply, window.Id, 0, new(b.X + 24, b.Y + 246, 104, 34));
}
return null;
}
private void Button(HitKind kind, int windowId, int index, DesktopBounds bounds, uint background, int radius = 8)
{
var target = new HitTarget(kind, windowId, index, bounds);
bool pressed = SameTarget(_pressed, target) && (_leftDown || _feedbackNow < _feedbackExpires), hovered = SameTarget(_hovered, target);
uint color = pressed ? DesktopTheme.Mix(_theme.Panel, _theme.Accent, _theme.IsDark ? 28 : 24) : hovered ? _theme.Selection : background;
RoundedFill(color, bounds.X, bounds.Y, bounds.Width, bounds.Height, radius);
if (pressed || hovered) _canvas.Line(Accent, bounds.X + radius, bounds.Y + bounds.Height - 2, bounds.X + bounds.Width - radius - 1, bounds.Y + bounds.Height - 2);
}
private bool IsPressed(HitKind kind, int windowId) => _pressed is { } pressed && pressed.Kind == kind && pressed.WindowId == windowId && (_leftDown || _feedbackNow < _feedbackExpires);
public void Key(DesktopKey key, char character = '\0')
{
InvalidateScene(ActiveWindow is { } active ? ShadowBounds(active.Bounds) : null);
switch (key)
{
case DesktopKey.StartMenu: CancelPointerCapture(); IsStartMenuOpen = !IsStartMenuOpen; _startSelection = 0; NeedsRender = true; return;
case DesktopKey.ShowDesktop: ShowDesktop(); return;
case DesktopKey.OpenFiles: TryOpen(DesktopApplication.Files); return;
case DesktopKey.SwitchWindow: CycleWindow(); return;
case DesktopKey.SwitchWindowBack: CycleWindow(true); return;
case DesktopKey.EndWindowSwitch: EndWindowSwitch(); return;
}
if (IsStartMenuOpen)
{
NeedsRender = true;
if (key == DesktopKey.Escape) IsStartMenuOpen = false;
else if (key == DesktopKey.Down || key == DesktopKey.Tab) _startSelection = (_startSelection + 1) % 5;
else if (key == DesktopKey.Up) _startSelection = (_startSelection + 4) % 5;
else if (key == DesktopKey.Enter) { if (_startSelection == 4) OpenSettings(); else TryOpen((DesktopApplication)_startSelection); }
return;
}
var window = ActiveWindow;
if (window is null) return;
switch (key)
{
case DesktopKey.CloseWindow: Close(window.Id); return;
case DesktopKey.Minimize: Minimize(window.Id); return;
case DesktopKey.Maximize: if (!window.IsMaximized) ToggleMaximize(window.Id); return;
case DesktopKey.Restore: if (window.IsMaximized || window.IsSnapped) RestoreWindow(window); else Minimize(window.Id); return;
case DesktopKey.SnapLeft: Snap(window, false); return;
case DesktopKey.SnapRight: Snap(window, true); return;
}
if (key == DesktopKey.Escape) { window.IsDirty = false; Close(window.Id); return; }
if (window.Application == DesktopApplication.Settings) { SettingsKey(window, key, character); return; }
if (key == DesktopKey.Run) { Preview(window); return; }
if (window.Application == DesktopApplication.Editor && key == DesktopKey.Save) { Save(window); return; }
if (window.Application == DesktopApplication.Editor && window.BrowserSession is not null)
{
string name = key switch { DesktopKey.Character => character.ToString(), DesktopKey.Enter => "Enter", DesktopKey.Backspace => "Backspace", DesktopKey.Delete => "Delete", DesktopKey.Left => "ArrowLeft", DesktopKey.Right => "ArrowRight", DesktopKey.Up => "ArrowUp", DesktopKey.Down => "ArrowDown", DesktopKey.Home => "Home", DesktopKey.End => "End", DesktopKey.Tab => "Tab", _ => "" };
if (name.Length != 0) { BrowserKey(new("keydown", name, Text: key == DesktopKey.Character ? character.ToString() : "")); BrowserKey(new("keyup", name)); }
return;
}
if (window.Application == DesktopApplication.Browser)
{
if (key == DesktopKey.FocusAddress) { window.BrowserContentFocused = false; window.AddressSelected = true; window.Status = "Address selected"; }
else if (window.BrowserContentFocused && window.BrowserSession is not null)
{
string name = key switch { DesktopKey.Character => character.ToString(), DesktopKey.Enter => "Enter", DesktopKey.Backspace => "Backspace", DesktopKey.Delete => "Delete", DesktopKey.Left => "ArrowLeft", DesktopKey.Right => "ArrowRight", DesktopKey.Up => "ArrowUp", DesktopKey.Down => "ArrowDown", DesktopKey.Home => "Home", DesktopKey.End => "End", DesktopKey.Tab => "Tab", _ => "" };
if (name.Length != 0) { BrowserKey(new("keydown", name, Text: key == DesktopKey.Character ? character.ToString() : "")); BrowserKey(new("keyup", name)); }
}
else if (key == DesktopKey.Enter) { Navigate(window); window.AddressSelected = false; }
else if (key == DesktopKey.Backspace && window.Location.Length > 0)
{ window.Location = window.AddressSelected ? "" : window.Location[..^1]; window.AddressSelected = false; }
else if (key == DesktopKey.Character && !char.IsControl(character) && window.Location.Length < 2048)
{ if (window.AddressSelected) window.Location = ""; window.AddressSelected = false; window.Location += character; }
return;
}
if (window.Application != DesktopApplication.Editor) return;
int at = window.Caret;
switch (key)
{
case DesktopKey.Save: Save(window); return;
case DesktopKey.Left: window.Caret = Math.Max(0, at - 1); break;
case DesktopKey.Right: window.Caret = Math.Min(window.Content.Length, at + 1); break;
case DesktopKey.Home: window.Caret = at == 0 ? 0 : window.Content.LastIndexOf('\n', at - 1) + 1; break;
case DesktopKey.End: int next = window.Content.IndexOf('\n', at); window.Caret = next < 0 ? window.Content.Length : next; break;
case DesktopKey.Up: VerticalCaret(window, -1); break;
case DesktopKey.Down: VerticalCaret(window, 1); break;
case DesktopKey.Backspace: if (at > 0) { window.Content = window.Content.Remove(at - 1, 1); window.Caret--; window.IsDirty = true; } break;
case DesktopKey.Delete: if (at < window.Content.Length) { window.Content = window.Content.Remove(at, 1); window.IsDirty = true; } break;
case DesktopKey.Enter: Insert(window, "\n"); break;
case DesktopKey.Tab: Insert(window, " "); break;
case DesktopKey.Character: if (!char.IsControl(character)) Insert(window, character.ToString()); break;
}
int row = 0;
for (int i = 0; i < window.Caret; i++) if (window.Content[i] == '\n') row++;
int rows = ContentRows(window);
if (row < window.ScrollLine) window.ScrollLine = row;
else if (row >= window.ScrollLine + rows) window.ScrollLine = row - rows + 1;
var lineStart = window.Caret == 0 ? 0 : window.Content.LastIndexOf('\n', window.Caret - 1) + 1;
int col = window.Caret - lineStart, availablePixels = Math.Max(8, window.Bounds.Width - 48);
if (col < window.HorizontalScroll) window.HorizontalScroll = col;
else
{
int width = _canvas.TextWidth(window.Content.Substring(lineStart + window.HorizontalScroll, col - window.HorizontalScroll));
while (width >= availablePixels && window.HorizontalScroll < col)
width -= _canvas.TextWidth(window.Content[lineStart + window.HorizontalScroll++].ToString());
}
}
private void TryOpen(DesktopApplication application)
{
try { Open(application); }
catch (InvalidOperationException error) { Notification = error.Message; }
}
private static void VerticalCaret(DesktopWindow window, int delta)
{
int start = window.Caret == 0 ? 0 : window.Content.LastIndexOf('\n', window.Caret - 1) + 1;
int column = window.Caret - start;
if (delta < 0 && start > 0) { int previous = start < 2 ? 0 : window.Content.LastIndexOf('\n', start - 2) + 1; window.Caret = Math.Min(previous + column, start - 1); }
if (delta > 0) { int end = window.Content.IndexOf('\n', window.Caret); if (end >= 0) { int after = window.Content.IndexOf('\n', end + 1); window.Caret = Math.Min(end + 1 + column, after < 0 ? window.Content.Length : after); } }
}
private static void Insert(DesktopWindow window, string value)
{
if (window.Content.Length + value.Length > 256 * 1024) { window.Status = "Editor size limit reached"; return; }
window.Content = window.Content.Insert(window.Caret, value); window.Caret += value.Length; window.IsDirty = true;
}
private void Save(DesktopWindow window)
{
if (window.BrowserSession is IBackgroundEditorSession background)
{
long version = window.EditorInputVersion;
window.Status = "Saving after queued editor input...";
if (!background.RequestEditorReadback((text, error) =>
{
if (!_windows.Contains(window)) return;
if (error is not null || text is null)
{ window.Status = "Save failed: " + (error ?? "Editor model is unavailable."); window.IsDirty = true; }
else
try
{
_storage.WriteText(window.Location, text);
window.Content = background.ReadEditorText() ?? text;
window.IsDirty = window.EditorInputVersion != version || window.Content != text;
window.Status = _storage.IsPersistent ? "Saved to disk" : "Saved to RAM live - lost after shutdown";
Trace?.Invoke("XFE_DESKTOP_SAVED|" + window.Location + "|" + text.Length + "\n");
}
catch (IOException exception) { window.Status = "Save failed: " + exception.Message; window.IsDirty = true; }
InvalidateScene(window.Bounds);
})) window.Status = "Save could not be queued; retry when the editor worker is available.";
return;
}
try
{
if (window.BrowserSession?.ReadEditorText() is { } text) window.Content = text;
_storage.WriteText(window.Location, window.Content);
window.IsDirty = false;
window.Status = _storage.IsPersistent ? "Saved to disk" : "Saved to RAM live - lost after shutdown";
}
catch (IOException error) { window.Status = "Save failed: " + error.Message; window.IsDirty = true; }
}
private void Preview(DesktopWindow window)
{
if (window.BrowserSession is IBackgroundEditorSession background)
{
window.Status = "Preparing preview after queued editor input...";
if (!background.RequestEditorReadback((source, error) =>
{
if (!_windows.Contains(window)) return;
if (error is not null || source is null) { window.Status = "Preview failed: " + error; InvalidateScene(window.Bounds); return; }
window.Content = source; PreviewCore(window, source);
})) window.Status = "Preview could not be queued; retry when the editor worker is available.";
return;
}
PreviewCore(window, null);
}
private void PreviewCore(DesktopWindow window, string? readback)
{
if (_browser is null) { window.Status = "Document/JavaScript engine is not connected to this kernel yet"; return; }
if (_windows.Count >= 12) { window.Status = "Close a window before opening a preview"; return; }
var preview = OpenCore(DesktopApplication.Browser, null, false);
try
{
if (readback is not null) window.Content = readback;
else if (window.BrowserSession?.ReadEditorText() is { } text) window.Content = text;
if (preview.BrowserSession is { } page)
{
page.BeginDocumentPreview(window.Content, window.Location, _theme.IsDark);
UpdateBrowserPage(preview); preview.BrowserScroll = 0;
preview.Status = "Website document styles and scripts | " + window.Location;
return;
}
preview.Location = "Preview: " + window.Location;
preview.Content = window.Location.EndsWith(".js", StringComparison.OrdinalIgnoreCase)
? _browser is IJavaScriptPreviewService isolated ? isolated.PreviewJavaScript(window.Content) : "An isolated JavaScript preview adapter is required"
: _browser.PreviewMarkdown(window.Content);
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
preview.BrowserSession = null; preview.BrowserContentFocused = false;
preview.Title = "Preview failed"; preview.Content = "Preview failed: " + error.Message;
preview.Status = "The source file was preserved";
}
}
private void Navigate(DesktopWindow window)
{
if (_browser is null)
{
window.Content = "XFESystem Browser\n\nThe native HTTP/TLS, HTML and JavaScript adapters are being ported.\n\nThis kernel does not claim a working internet browser yet.";
window.Status = "Browser backend unavailable"; return;
}
try
{
window.BrowserSession ??= (_browser as IInteractiveBrowserFactory)?.CreatePageSession();
window.BrowserSession?.SetCanvas(_canvas);
window.BrowserSession?.ResizeViewport(BrowserViewport(window).Width, BrowserViewport(window).Height);
var page = window.BrowserSession?.Navigate(window.Location) ?? _browser.Navigate(window.Location);
window.Title = "Browser - " + page.Title; window.Content = page.Text; window.Location = page.Address; window.BrowserScroll = 0;
}
catch (Exception error) { window.Content = "Navigation failed: " + error.Message; window.Status = "Page could not be loaded"; }
}
private string UniquePath(string directory, string stem, string extension)
{
for (int i = 1; i <= 1024; i++) { string path = SystemPaths.Combine(directory, stem + i + extension); if (!_storage.Exists(path)) return path; }
throw new IOException("No available name remains.");
}
private static int ContentRows(DesktopWindow w) => Math.Max(1, (w.Bounds.Height - 112) / 20);
private DesktopBounds ScrollDamage(DesktopWindow window)
{
if (window.Application is not (DesktopApplication.Editor or DesktopApplication.Browser)) return ShadowBounds(window.Bounds);
int viewportWidth = Math.Max(8, window.Bounds.Width - 48);
if (!ReferenceEquals(window.MeasuredContent, window.Content) || window.MeasuredHorizontalScroll != window.HorizontalScroll ||
window.MeasuredViewportWidth != viewportWidth)
{
int widest = 8;
foreach (string line in window.Content.Split('\n'))
{
int left = Math.Min(line.Length, window.HorizontalScroll);
widest = Math.Max(widest, _canvas.TextWidth(line[left..]));
if (widest >= viewportWidth) break;
}
window.MeasuredContent = window.Content;
window.MeasuredHorizontalScroll = window.HorizontalScroll;
window.MeasuredViewportWidth = viewportWidth;
window.MeasuredContentWidth = Math.Min(widest + 4, viewportWidth);
}
// Wheel scrolling changes glyphs and the caret, not the title, toolbar,
// status, or blank columns to the right of the longest document line.
return new(window.Bounds.X + 18, window.Bounds.Y + 74, window.MeasuredContentWidth + 6, ContentRows(window) * 20 + 4);
}
public void Render()
{
LastFrameCompleted = false;
_editorSourceFrames.Clear();
try
{
RenderCore();
if (LastFrameCompleted)
{
long presentedAt = System.Diagnostics.Stopwatch.GetTimestamp();
ReportPresentedEditorSources(presentedAt);
foreach (var window in _windows)
if (!window.IsMinimized) window.BrowserSession?.FramePresented();
}
}
catch (DesktopRenderYieldException)
{
RenderYieldCount++;
if (_canvas is IDesktopSceneMotionCanvas motionCanvas) motionCanvas.AbortScenePreservation();
_canvas.BeginScene(new(0, 0, _canvas.Width, _canvas.Height));
NeedsRender = true;
}
}
private void RenderCore()
{
if (!NeedsRender) return;
UpdateHover(ResolveTarget(_pointerX, _pointerY));
UpdateChromeTargets(_feedbackNow);
var pointerBounds = PointerBounds;
if (!_sceneDirty && _canvas.SupportsSceneCache)
{
_canvas.RestoreSceneRegion(_renderedPointerBounds);
_canvas.RestoreSceneRegion(pointerBounds);
DrawPointer();
_canvas.Present(Union(_renderedPointerBounds, pointerBounds));
LastFrameCompleted = true;
_renderedPointerBounds = pointerBounds;
NeedsRender = false;
return;
}
var fullScreen = new DesktopBounds(0, 0, _canvas.Width, _canvas.Height);
bool partial = _canvas.SupportsSceneCache && _sceneDamage is not null;
var damage = partial ? _sceneDamage!.Value : fullScreen;
_canvas.BeginScene(damage);
if (partial && _pendingMotion && _motionSourceValid && ActiveWindow is { } moving && moving.Id == _motionSceneOwner &&
_canvas is IDesktopSceneMotionCanvas motionCanvas)
{
var source = ReusableMotionBody(moving.Bounds); var target = MotionBody(moving.Bounds);
bool unchanged = _motionIsResize && !Intersects(source, _renderedPointerBounds) && !Intersects(source, pointerBounds);
motionCanvas.PreserveTranslatedSceneRegion(source, target.X, target.Y, unchanged);
}
DesktopWallpaper.Draw(_canvas, _theme, damage, WorkArea.Height);
for (int i = 0; i < 5; i++)
{
var icon = DesktopAppBounds(i);
RoundedFill(_theme.Shadow, icon.X, icon.Y + 2, icon.Width, icon.Height, 14);
Button(HitKind.DesktopApp, 0, i, icon, Panel, 14);
DrawIcon(ApplicationIcon((DesktopApplication)i), icon.X + 33, icon.Y + (icon.Height >= 66 ? 10 : 3), icon.Height >= 66 ? 32 : 24);
_canvas.Text(((DesktopApplication)i).ToString(), White, icon.X + 14, icon.Y + icon.Height - 22);
}
foreach (var window in _windows)
if (!window.IsMinimized && Intersects(ShadowBounds(window.Bounds), damage)) DrawWindow(window, damage);
DrawTaskbar();
if (IsStartMenuOpen) DrawStartMenu();
if (_switchOrder is not null) DrawWindowSwitcher();
if (_canvas is IDesktopSceneMotionCanvas composedMotion) composedMotion.EndScenePreservation();
if (_canvas.SupportsSceneCache) _canvas.SaveScene(damage);
CaptureMotionScene();
_canvas.BeginScene(fullScreen);
if (partial && !_pointerDirty && !Intersects(damage, _renderedPointerBounds) && !Intersects(damage, pointerBounds))
{
_canvas.Present(damage);
LastFrameCompleted = true;
_renderedPointerBounds = pointerBounds;
NeedsRender = false;
return;
}
if (_canvas.SupportsSceneCache)
{
_canvas.RestoreSceneRegion(_renderedPointerBounds);
_canvas.RestoreSceneRegion(pointerBounds);
}
DrawPointer();
if (partial) _canvas.Present(Union(damage, Union(_renderedPointerBounds, pointerBounds))); else _canvas.Present();
LastFrameCompleted = true;
_renderedPointerBounds = pointerBounds; NeedsRender = false;
}
private void DrawWindow(DesktopWindow window, DesktopBounds damage)
{
var b = window.Bounds;
RoundedFill(_theme.Shadow, b.X + 4, b.Y + 6, b.Width, b.Height, 18);
RoundedFill(window == ActiveWindow ? _theme.ActiveBorder : Border, b.X, b.Y, b.Width, b.Height, 18);
RoundedFill(Panel, b.X + 1, b.Y + 1, b.Width - 2, b.Height - 2, 17);
uint title = window == ActiveWindow ? _theme.ActiveTitle : Panel;
RoundedFill(title, b.X + 1, b.Y + 1, b.Width - 2, 48, 17);
_canvas.Fill(title, b.X + 1, b.Y + 18, b.Width - 2, 14);
_canvas.Fill(_theme.Surface, b.X + 1, b.Y + 32, b.Width - 2, b.Height - 61);
DrawIcon(ApplicationIcon(window.Application), b.X + 12, b.Y + 7, 18);
_canvas.Text(Clip(window.Title, Math.Max(0, (b.Width / 2 - 86) / 8)), White, b.X + 38, b.Y + 10);
_canvas.Fill(Panel, b.X + 1, b.Y + 33, b.Width - 2, 32);
if (window.Application == DesktopApplication.Files)
{
Button(HitKind.FilesUp, window.Id, 0, new(b.X + 4, b.Y + 35, 60, 30), Panel);
Button(HitKind.NewFolder, window.Id, 0, new(b.X + 64, b.Y + 35, 104, 30), Panel);
Button(HitKind.NewFile, window.Id, 0, new(b.X + 168, b.Y + 35, 96, 30), Panel);
DrawIcon(DesktopIcon.Folder, b.X + 12, b.Y + 39, 18);
_canvas.Text("Up", TextColor, b.X + 34, b.Y + 43);
DrawIcon(DesktopIcon.Folder, b.X + 68, b.Y + 39, 18);
_canvas.Text("New Folder", TextColor, b.X + 90, b.Y + 43);
DrawIcon(DesktopIcon.TextFile, b.X + 176, b.Y + 39, 18);
_canvas.Text("New File", TextColor, b.X + 198, b.Y + 43);
_canvas.Text(Clip(window.Location, (b.Width - 32) / 8), Muted, b.X + 14, b.Y + 76);
var entries = _storage.List(window.Location);
for (int i = window.ScrollLine; i < Math.Min(entries.Length, window.ScrollLine + (b.Height - 132) / 28); i++)
{
int y = b.Y + 100 + (i - window.ScrollLine) * 28;
Button(HitKind.FileEntry, window.Id, i, new(b.X + 8, y - 4, b.Width - 16, 28), _theme.Surface, 6);
DrawIcon(FileIcon(entries[i].Path, entries[i].IsDirectory), b.X + 18, y - 4, 22);
_canvas.Text(Clip(entries[i].Name, (b.Width - 76) / 8), White, b.X + 58, y);
}
}
else if (window.Application == DesktopApplication.Settings) DrawSettings(window);
else
{
if (window.Application == DesktopApplication.Editor)
{
Button(HitKind.EditorSave, window.Id, 0, new(b.X + 4, b.Y + 35, 84, 30), Panel);
Button(HitKind.EditorPreview, window.Id, 0, new(b.X + 88, b.Y + 35, 96, 30), Panel);
DrawIcon(DesktopIcon.TextFile, b.X + 12, b.Y + 39, 18); _canvas.Text("Save", TextColor, b.X + 34, b.Y + 43);
DrawIcon(DesktopIcon.Browser, b.X + 92, b.Y + 39, 18); _canvas.Text("Preview", TextColor, b.X + 114, b.Y + 43);
if (b.Width >= 400) _canvas.Text("Ctrl+S / F5", Muted, b.X + 208, b.Y + 43);
}
else
{
Button(window.Application == DesktopApplication.Terminal ? HitKind.TerminalDemo : HitKind.BrowserAddress, window.Id, 0, new(b.X + 4, b.Y + 35, b.Width - 8, 30), Panel);
DrawIcon(window.Application == DesktopApplication.Terminal ? DesktopIcon.ApplicationFile : DesktopIcon.Browser, b.X + 12, b.Y + 39, 18);
var toolbar = window.Application == DesktopApplication.Terminal ? "Run Demo" : Clip(window.Location, (b.Width - 58) / 8);
_canvas.Text(toolbar, TextColor, b.X + 38, b.Y + 43);
}
if (window.Application == DesktopApplication.Terminal) _canvas.Fill(TerminalBackground, b.X + 2, b.Y + 67, b.Width - 4, b.Height - 96);
if (window.BrowserSession is { } page)
if (Intersects(BrowserViewport(window), damage)) page.Paint(_canvas, BrowserViewport(window), window.BrowserScroll);
var lines = window.BrowserSession is null ? window.Content.Replace("\r", "").Split('\n') : [];
int sourceLinesDrawn = 0, sourceFirstY = 0, sourceLastY = 0, sourceWidth = 0;
for (int i = window.ScrollLine; i < Math.Min(lines.Length, window.ScrollLine + ContentRows(window)); i++)
{
string line = lines[i];
int left = Math.Min(line.Length, window.HorizontalScroll);
var visible = Clip(line[left..], (b.Width - 48) / 8);
int y = b.Y + 76 + (i - window.ScrollLine) * 20;
if (window.Application == DesktopApplication.Editor && IsCode(window.Location))
foreach (var span in SyntaxHighlighter.Tokenize(visible)) _canvas.Text(span.Text, LightSyntaxColor(span.Color), b.X + 20 + _canvas.TextWidth(visible[..span.Start]), y);
else _canvas.Text(visible, window.Application == DesktopApplication.Terminal ? TerminalForeground : White, b.X + 20, y);
if (window == ActiveWindow && window.EditorPreparation is not null && window.BrowserSession is null && !string.IsNullOrWhiteSpace(visible) &&
Intersects(new(b.X + 20, y, Math.Max(1, _canvas.TextWidth(visible)), 20), damage))
{
if (sourceLinesDrawn++ == 0) sourceFirstY = y;
sourceLastY = y; sourceWidth = Math.Max(sourceWidth, _canvas.TextWidth(visible));
}
}
if (sourceLinesDrawn != 0)
_editorSourceFrames.Add(new(window, window.EditorPreparationGeneration, window.Content, sourceLinesDrawn,
new(b.X + 20, sourceFirstY, sourceWidth, sourceLastY - sourceFirstY + 20)));
if (window.Application == DesktopApplication.Terminal && window.Terminal is { } terminal)
{
int rows = Math.Min(terminal.Rows, ContentRows(window)), columns = Math.Min(terminal.Columns, (b.Width - 40) / 8);
_canvas.Fill(TerminalBackground, b.X + 2, b.Y + 67, b.Width - 4, b.Height - 96);
for (int row = 0; row < rows; row++)
for (int col = 0; col < columns; col++)
{
var cell = terminal.GetCell(col, row);
var fg = cell.Style.DisplayForeground; var bg = cell.Style.DisplayBackground;
uint foreground = (uint)(fg.Red << 16 | fg.Green << 8 | fg.Blue), background = (uint)(bg.Red << 16 | bg.Green << 8 | bg.Blue);
int x = b.X + 20 + col * 8, y = b.Y + 76 + row * 20;
_canvas.Fill(background, x, y, 8 * Math.Max(1, cell.DisplayWidth), 20);
}
for (int row = 0; row < rows; row++)
for (int col = 0; col < columns; col++)
{
var cell = terminal.GetCell(col, row); var fg = cell.Style.DisplayForeground;
uint foreground = (uint)(fg.Red << 16 | fg.Green << 8 | fg.Blue);
int x = b.X + 20 + col * 8, y = b.Y + 76 + row * 20;
if (!cell.IsContinuation) _canvas.Text(cell.Text, foreground, x, y);
if (cell.Style.Underline) _canvas.Line(foreground, x, y + 17, x + 8 * Math.Max(1, cell.DisplayWidth) - 1, y + 17);
}
}
if (window == ActiveWindow && window.Application == DesktopApplication.Editor && window.BrowserSession is null)
{
int row = 0, col = 0;
for (int i = 0; i < window.Caret; i++) if (window.Content[i] == '\n') { row++; col = 0; } else col++;
if (row >= window.ScrollLine && row < window.ScrollLine + ContentRows(window))
{
int start = window.Caret - col + Math.Min(col, window.HorizontalScroll);
int caretX = b.X + 20 + _canvas.TextWidth(window.Content.Substring(start, window.Caret - start));
if (caretX >= b.X + 20 && caretX < b.X + b.Width - 28)
_canvas.Line(Accent, caretX, b.Y + 76 + (row - window.ScrollLine) * 20,
caretX, b.Y + 92 + (row - window.ScrollLine) * 20);
}
}
}
_canvas.Fill(Panel, b.X + 1, b.Y + b.Height - 28, b.Width - 2, 10);
_canvas.Text(Clip((window.IsDirty ? "Modified | " : "") + window.Status, (b.Width - 48) / 8), Muted, b.X + 14, b.Y + b.Height - 20);
var chrome = window.ChromeAnimation;
DesktopWindowChromeRenderer.Draw(_canvas, b, _theme, window == ActiveWindow, chrome.MinimizeProgress,
chrome.CloseProgress, chrome.TitleHintProgress, chrome.ResizeProgress, window.IsMaximized || window.IsSnapped,
IsPressed(HitKind.WindowMinimize, window.Id), IsPressed(HitKind.WindowClose, window.Id));
}
private void DrawTaskbar()
{
int y = _canvas.Height - TaskbarHeight;
_canvas.Fill(Panel, 0, y, _canvas.Width, TaskbarHeight);
_canvas.Line(Border, 0, y, _canvas.Width - 1, y);
Button(HitKind.StartButton, 0, 0, new(10, y + 7, 52, 34), IsStartMenuOpen ? _theme.Selection : _theme.SoftAccent, 10);
if (_canvas is IBrandedDesktopCanvas branded) branded.BrandIcon(25, y + 12, 24);
else _canvas.Text("XFE", TextColor, 23, y + 18);
var tasks = _windows.OrderBy(w => w.Id).ToArray();
int width = TaskWidth(tasks.Length);
for (int i = 0; i < tasks.Length; i++)
{
var window = tasks[i]; int x = 76 + i * width;
bool active = window == ActiveWindow;
Button(HitKind.TaskButton, window.Id, 0, new(x, y + 7, Math.Max(1, width - 6), 34), active ? _theme.Selection : _theme.SoftAccent, 9);
if (width >= 30) DrawIcon(ApplicationIcon(window.Application), x + 8, y + 13, 22);
if (width >= 80) _canvas.Text(Clip(window.Title, (width - 46) / 8), window.IsMinimized ? Muted : White, x + 36, y + 18);
_canvas.Line(active ? Accent : Muted, x + 8, y + 40, x + Math.Max(8, width - 14), y + 40);
}
if (tasks.Length == 0) _canvas.Text(Clip(Notification, (_canvas.Width - 184) / 8), Muted, 84, y + 18);
Button(HitKind.TaskSettings, 0, 0, new(_canvas.Width - 78, y + 7, 70, 34), _theme.SoftAccent, 9);
DrawIcon(ApplicationIcon(DesktopApplication.Settings), _canvas.Width - 54, y + 13, 22);
}
private void DrawStartMenu()
{
var b = StartMenuBounds;
RoundedFill(_theme.Shadow, b.X + 4, b.Y + 5, b.Width, b.Height, 18);
RoundedFill(Panel, b.X, b.Y, b.Width, b.Height, 18);
_canvas.Text("XFESystem", TextColor, b.X + 18, b.Y + 20);
Button(HitKind.MenuSettings, 0, 0, SettingsMenuButton, _startSelection == 4 ? _theme.Selection : _theme.SoftAccent);
_canvas.Text("Settings", White, SettingsMenuButton.X + 12, SettingsMenuButton.Y + 9);
for (int i = 0; i < 4; i++)
{
int y = b.Y + 54 + i * 48;
Button(HitKind.StartItem, 0, i, new(b.X + 10, y - 3, b.Width - 20, 42), i == _startSelection ? _theme.Selection : Panel, 10);
DrawIcon((DesktopIcon)i, b.X + 20, y + 2, 28);
_canvas.Text(((DesktopApplication)i).ToString(), White, b.X + 64, y + 10);
}
}
private void DrawWindowSwitcher()
{
var order = _switchOrder!;
int width = Math.Min(_canvas.Width - 32, Math.Max(240, order.Count * 126 + 24));
int x = (_canvas.Width - width) / 2, y = _canvas.Height / 2 - 54;
RoundedFill(_theme.Shadow, x + 4, y + 6, width, 108, 18);
RoundedFill(Panel, x, y, width, 108, 18);
int cell = (width - 24) / order.Count;
for (int i = 0; i < order.Count; i++)
{
var window = _windows.Find(w => w.Id == order[i]);
if (window is null) continue;
int itemX = x + 12 + cell * i;
if (i == _switchIndex) RoundedFill(_theme.Selection, itemX, y + 10, cell - 4, 88, 12);
DrawIcon(ApplicationIcon(window.Application), itemX + Math.Max(4, (cell - 36) / 2), y + 20, Math.Min(36, cell - 8));
_canvas.Text(Clip(window.Title, Math.Max(1, (cell - 12) / 8)), White, itemX + 6, y + 72);
}
}
private void DrawIcon(DesktopIcon icon, int x, int y, int size)
{
if (_canvas is IDesktopIconCanvas icons) icons.DrawIcon(icon, x, y, size);
}
private static DesktopIcon ApplicationIcon(DesktopApplication app) => app == DesktopApplication.Settings ? DesktopIcon.Settings : (DesktopIcon)app;
private void DrawSettings(DesktopWindow window)
{
var b = window.Bounds;
void Label(string text, uint color, int x, int y)
{ if (y + 20 <= b.Y + b.Height - 28) _canvas.Text(Clip(text, (b.Width - x + b.X - 24) / 8), color, x, y); }
void Option(HitKind kind, int index, int x, int y, int width, bool selected, string? text = null)
{
if (y + 34 > b.Height - 28) return;
var bounds = new DesktopBounds(b.X + x, b.Y + y, width, 34);
Button(kind, window.Id, index, bounds, selected ? _theme.Selection : Panel);
if (selected) _canvas.Line(Accent, bounds.X + 7, bounds.Y + 32, bounds.X + bounds.Width - 8, bounds.Y + 32);
if (text is not null) _canvas.Text(text, White, bounds.X + 16, bounds.Y + 11);
}
Label("Appearance", TextColor, b.X + 24, b.Y + 45);
Label("Theme", Muted, b.X + 24, b.Y + 67);
Option(HitKind.SettingsLight, 0, 24, 88, 104, !_theme.IsDark, "Light");
Option(HitKind.SettingsDark, 0, 140, 88, 104, _theme.IsDark, "Dark");
Label("Accent color", Muted, b.X + 24, b.Y + 127);
for (int i = 0; i < DesktopTheme.PresetCount; i++)
{
uint color = DesktopTheme.Preset(i).Color;
Option(HitKind.SettingsPreset, i, 24 + i * 48, 148, 40, _theme.Accent == color);
if (182 <= b.Height - 28) RoundedFill(color, b.X + 34 + i * 48, b.Y + 156, 20, 18, 5);
}
Label("Custom #RRGGBB", Muted, b.X + 24, b.Y + 183);
Option(HitKind.SettingsAccent, 0, 24, 202, 220, window.AppearanceFocus == 2, window.AppearanceAccentText + (window.AppearanceFocus == 2 ? " |" : ""));
Option(HitKind.SettingsApply, 0, 24, 246, 104, window.AppearanceFocus == 3, "Apply");
Label("Tab: focus Enter: apply", Muted, b.X + 24, b.Y + 298);
Label(_storage.IsPersistent ? "Settings are stored on disk" : "RAM live: settings reset after shutdown", Muted, b.X + 24, b.Y + 324);
Label("Current accent: " + _theme.AccentHex, TextColor, b.X + 24, b.Y + 350);
}
private static DesktopIcon FileIcon(string path, bool directory)
{
if (directory) return DesktopIcon.Folder;
if (path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) return DesktopIcon.ApplicationFile;
if (path.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) return DesktopIcon.MarkdownFile;
return IsCode(path) ? DesktopIcon.CodeFile : DesktopIcon.TextFile;
}
private void RoundedFill(uint color, int x, int y, int width, int height, int radius)
=> DesktopChromeRenderer.RoundedFill(_canvas, color, x, y, width, height, radius);
private DesktopPointerShape PointerShape
{
get
{
if (_resize is not null) return DesktopPointerShape.Resize;
if (_pointerY >= _canvas.Height - TaskbarHeight ||
IsStartMenuOpen && StartMenuBounds.Contains(_pointerX, _pointerY)) return DesktopPointerShape.Arrow;
var hit = _windows.FindLast(w => !w.IsMinimized && w.Bounds.Contains(_pointerX, _pointerY));
if (hit is null) return DesktopPointerShape.Arrow;
if (hit.BrowserSession?.TextCursor == true && BrowserViewport(hit).Contains(_pointerX,_pointerY)) return DesktopPointerShape.Text;
if (!hit.IsMaximized && !hit.IsSnapped && _pointerX >= hit.Bounds.X + hit.Bounds.Width - 18 &&
_pointerY >= hit.Bounds.Y + hit.Bounds.Height - 18) return DesktopPointerShape.Resize;
int x = _pointerX - hit.Bounds.X, y = _pointerY - hit.Bounds.Y;
bool text = hit.Application == DesktopApplication.Editor && y >= 70 && y < hit.Bounds.Height - 28 ||
hit.Application == DesktopApplication.Browser && y >= 33 && y < 65 ||
hit.Application == DesktopApplication.Settings && x >= 24 && x < 244 && y >= 202 && y < 236;
return text ? DesktopPointerShape.Text : DesktopPointerShape.Arrow;
}
}
private void DrawPointer() => DesktopPointerRenderer.Draw(_canvas, _pointerX, _pointerY, PointerShape, _theme.AccentStroke);
private DesktopBounds PointerBounds
{
get
{
var bounds = DesktopPointerRenderer.GetBounds(_pointerX, _pointerY, PointerShape);
int left = Math.Max(0, bounds.X), top = Math.Max(0, bounds.Y);
int right = Math.Min(_canvas.Width, bounds.X + bounds.Width), bottom = Math.Min(_canvas.Height, bounds.Y + bounds.Height);
return new(left, top, Math.Max(0, right - left), Math.Max(0, bottom - top));
}
}
private static DesktopBounds Union(DesktopBounds first, DesktopBounds second)
{
int x = Math.Min(first.X, second.X), y = Math.Min(first.Y, second.Y);
return new(x, y, Math.Max(first.X + first.Width, second.X + second.Width) - x, Math.Max(first.Y + first.Height, second.Y + second.Height) - y);
}
private DesktopBounds ShadowBounds(DesktopBounds bounds)
{
int left = Math.Max(0, bounds.X - 1), top = Math.Max(0, bounds.Y - 1);
return new(left, top, Math.Min(_canvas.Width, bounds.X + bounds.Width + 8) - left, Math.Min(_canvas.Height, bounds.Y + bounds.Height + 9) - top);
}
private void InvalidateScene(DesktopBounds? damage)
{
if (!_motionInvalidation && (damage is null || _motionSourceValid && ActiveWindow is { } active &&
(Intersects(damage.Value, ReusableMotionBody(active.Bounds)) ||
Intersects(damage.Value, ReusableMotionBody(active.Bounds) with { X = active.Bounds.X + 2, Y = active.Bounds.Y + 65 }))))
_motionSourceValid = false;
if (damage is null) { NeedsRender = true; return; }
if (!_sceneDirty) _sceneDamage = damage;
else if (_sceneDamage is { } previous) _sceneDamage = Union(previous, damage.Value);
_sceneDirty = true;
}
private static DesktopBounds MotionBody(DesktopBounds bounds) => new(bounds.X + 2, bounds.Y + 65, Math.Max(0, bounds.Width - 4), Math.Max(0, bounds.Height - 109));
private DesktopBounds ReusableMotionBody(DesktopBounds target) => _motionIsResize ?
new(_motionSceneBounds.X + 2, _motionSceneBounds.Y + 65,
Math.Max(0, Math.Min(_motionSceneBounds.Width, target.Width) -
(ActiveWindow is { Application: DesktopApplication.Editor } editor && IsCode(editor.Location) ? 160 : 96)),
// A complete bottom 64px guard also covers both layouts' partial
// last row at every 20/28px visible-row threshold, not only chrome.
Math.Max(0, Math.Min(_motionSceneBounds.Height, target.Height) - 129)) : MotionBody(_motionSceneBounds);
private void InvalidateWindowMotion(DesktopWindow window, DesktopBounds previous, bool resizing = false)
{
_pendingMotion = _motionSourceValid && resizing == _motionIsResize && window.Id == _motionSceneOwner &&
(resizing ? window.Application != DesktopApplication.Settings && window.BrowserSession is null && window.Bounds.X == _motionSceneBounds.X && window.Bounds.Y == _motionSceneBounds.Y :
window.Bounds.Width == _motionSceneBounds.Width && window.Bounds.Height == _motionSceneBounds.Height);
_motionInvalidation = true;
try { InvalidateScene(Union(ShadowBounds(previous), ShadowBounds(window.Bounds))); }
finally { _motionInvalidation = false; }
}
private void CaptureMotionScene()
{
_pendingMotion = false;
var moving = _drag ?? _resize;
_motionSourceValid = _canvas.SupportsSceneCache && _canvas is IDesktopSceneMotionCanvas && _leftDown &&
moving is not null && moving == ActiveWindow && !IsStartMenuOpen && _switchOrder is null &&
_hovered is { } target && target.WindowId == moving.Id &&
(target.Kind == HitKind.WindowTitle && _drag == moving ||
target.Kind == HitKind.WindowResize && _resize == moving && moving.Application != DesktopApplication.Settings && moving.BrowserSession is null);
if (_motionSourceValid) { _motionSceneOwner = moving!.Id; _motionSceneBounds = moving.Bounds; _motionIsResize = _resize == moving; }
}
private uint LightSyntaxColor(uint color) => _theme.SyntaxColor(color);
private static bool IsCode(string path) => path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".js", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".json", StringComparison.OrdinalIgnoreCase);
private string Clip(string text, int max)
{
int available = Math.Max(0, max) * 8;
if (_canvas.TextWidth(text) <= available) return text;
int suffixWidth = _canvas.TextWidth("...");
if (suffixWidth > available) return "";
int length = 0, width = suffixWidth;
while (length < text.Length)
{
int next = _canvas.TextWidth(text[length].ToString());
if (width + next > available) break;
width += next;
length++;
}
return text[..length] + "...";
}
private static bool Intersects(DesktopBounds first, DesktopBounds second) => first.X < second.X + second.Width &&
first.X + first.Width > second.X && first.Y < second.Y + second.Height && first.Y + first.Height > second.Y;
}