namespace XFESystem.Desktop;
public sealed partial class DesktopSession
{
private static DesktopBounds DocumentBody(DesktopWindow window) => new(window.Bounds.X + 2, window.Bounds.Y + 67,
Math.Max(1, window.Bounds.Width - 4), Math.Max(1, window.Bounds.Height - 96));
private static DesktopBounds EditorViewport(DesktopWindow window)
{
var body = DocumentBody(window);
return window.IsMarkdown && window.MarkdownView == MarkdownViewMode.Split
? body with { Width = Math.Max(1, (body.Width - 4) / 2) } : body;
}
private static DesktopBounds PreviewViewport(DesktopWindow window)
{
var body = DocumentBody(window);
if (window.MarkdownView != MarkdownViewMode.Split) return body;
int left = Math.Max(1, (body.Width - 4) / 2);
return new(body.X + left + 4, body.Y, Math.Max(1, body.Width - left - 4), body.Height);
}
private static bool ShowsSource(DesktopWindow window) => !window.IsMarkdown || window.MarkdownView != MarkdownViewMode.Preview;
private static bool ShowsPreview(DesktopWindow window) => window.IsMarkdown && window.MarkdownView != MarkdownViewMode.Edit;
private static IInteractiveBrowserSession? FocusedPage(DesktopWindow window) => window.IsMarkdown && window.PreviewFocused ? window.PreviewSession : window.BrowserSession;
private DesktopBounds MarkdownEditButton(DesktopWindow window) => new(window.Bounds.X + window.Bounds.Width - 98, window.Bounds.Y + 35, 92, 30);
private DesktopBounds MarkdownSplitButton(DesktopWindow window) => new(window.Bounds.X + window.Bounds.Width - 194, window.Bounds.Y + 35, 92, 30);
/// <summary>Mode changes retain the unsaved editor model and the independent preview page.</summary>
public void SetMarkdownView(int windowId, MarkdownViewMode mode)
{
var window = _windows.Find(candidate => candidate.Id == windowId && candidate.IsMarkdown);
if (window is null) return;
if (window.BrowserSession is IBackgroundEditorSession background && mode != MarkdownViewMode.Edit)
{
window.Status = "Preparing preview after queued editor input...";
if (!background.RequestEditorReadback((source, error) =>
{
if (!_windows.Contains(window)) return;
if (source is null || error is not null)
{ window.Status = "Preview failed: " + error; InvalidateScene(window.Bounds); return; }
if (window.Content != source) { window.Content = source; window.IsDirty = true; }
SetMarkdownViewCore(window, mode);
})) window.Status = "Preview could not be queued; retry when the editor worker is available.";
InvalidateScene(window.Bounds);
return;
}
if (window.BrowserSession?.ReadEditorText() is { } latest && latest != window.Content)
{ window.Content = latest; window.IsDirty = true; }
SetMarkdownViewCore(window, mode);
}
private void SetMarkdownViewCore(DesktopWindow window, MarkdownViewMode mode)
{
CancelPointerCapture();
(_webPointerCapturePreview ? _webPointerCapture?.PreviewSession : _webPointerCapture?.BrowserSession)?.Pointer(new("pointercancel", -1, -1));
_webPointerCapture = _webHoveredWindow = null;
window.MarkdownView = mode;
window.PreviewFocused = mode == MarkdownViewMode.Preview;
window.BrowserContentFocused = mode == MarkdownViewMode.Preview ? window.PreviewSession is not null : window.BrowserSession is not null;
if (mode != MarkdownViewMode.Preview && window.BrowserSession is null && window.EditorPreparation is null)
{
if (_editorPreparation is not null) BeginEditorPreparation(window);
// A missing worker still leaves a responsive basic source editor.
else window.Status = "Text editor | Ctrl+S saves | Split shows the live document";
}
if (ShowsPreview(window) && window.PreviewSource != window.Content) BeginMarkdownPreview(window);
ResizeDocumentPanes(window);
InvalidateScene(window.Bounds);
}
private void ResizeDocumentPanes(DesktopWindow window)
{
if (!window.IsMarkdown) return;
var source = EditorViewport(window); var preview = PreviewViewport(window);
if (ShowsSource(window)) window.BrowserSession?.ResizeViewport(source.Width, source.Height);
if (ShowsPreview(window)) window.PreviewSession?.ResizeViewport(preview.Width, preview.Height);
window.PreviewScroll = Math.Clamp(window.PreviewScroll, 0, Math.Max(0, (window.PreviewSession?.DocumentHeight ?? 0) - preview.Height));
}
private void BeginMarkdownPreview(DesktopWindow window)
{
if (window.PreviewPreparation is { } pending)
{ RetireEditorPreparation(pending); window.PreviewPreparation = null; }
var viewport = PreviewViewport(window);
window.PreviewObservedContent = window.Content;
window.PreviewError = null;
window.PreviewRefreshAt = 0;
window.Status = "Loading document preview in background...";
if (_editorPreparation is not null)
{
var request = new EditorPreparationRequest(window.Id, ++_nextEditorPreparationGeneration, window.Content, window.Location,
viewport.Width, viewport.Height, _monotonicMilliseconds?.Invoke() ?? _feedbackNow, EditorPreparationKind.Document, _theme.IsDark);
window.PreviewPreparationGeneration = request.Generation;
try { window.PreviewPreparation = _editorPreparation.Begin(request); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ window.PreviewSource = window.Content; window.PreviewError = error.Message; window.Status = "Document preview unavailable: " + error.Message; }
return;
}
// Explicit synchronous adapters are retained for host tooling. The
// native desktop always supplies the verified background service.
if (_browser is not IInteractiveBrowserFactory factory)
{ window.PreviewSource = window.Content; window.PreviewError = "Document preview requires the web renderer"; window.Status = window.PreviewError; return; }
IInteractiveBrowserSession? page = null;
try
{
page = factory.CreatePageSession(); page.SetCanvas(_canvas); page.ResizeViewport(viewport.Width, viewport.Height);
page.BeginDocumentPreview(window.Content, window.Location, _theme.IsDark);
RetireBrowserSession(window.PreviewSession); window.PreviewSession = page; window.PreviewSource = window.Content;
window.PreviewFocused = window.MarkdownView == MarkdownViewMode.Preview; window.BrowserContentFocused = window.PreviewFocused;
window.Status = "Document preview | Edit or Split to change the source";
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ RetireBrowserSession(page); window.PreviewSource = window.Content; window.PreviewError = error.Message; window.Status = "Document preview failed: " + error.Message; }
}
private void PollMarkdownPreviews(long now)
{
foreach (var window in _windows)
{
if (!window.IsMarkdown) continue;
if (window.PreviewObservedContent != window.Content)
{ window.PreviewObservedContent = window.Content; window.PreviewRefreshAt = now + 180; }
if (window.PreviewPreparation is { } operation && operation.TryTakeResult(out var result))
{
window.PreviewPreparation = null;
if (operation.Request.Generation != window.PreviewPreparationGeneration || operation.Request.Source != window.Content || operation.Request.Path != window.Location || operation.Request.Dark != _theme.IsDark)
{
RetireBrowserSession(result.Page); RetireEditorPreparation(operation);
window.PreviewRefreshAt = now + 180;
}
else if (result.Page is { } page)
{
try
{
bool equivalent = result.PreparedCanvas is IEditorPreparationCanvasHandoff handoff && handoff.TryCompleteEditorPreparationHandoff(_canvas);
page.CompleteEditorPreparationHandoff(_canvas, equivalent);
RetireBrowserSession(window.PreviewSession); window.PreviewSession = page;
window.PreviewSource = operation.Request.Source;
ResizeDocumentPanes(window);
window.Status = "Document preview | Edit or Split to change the source";
if (window.MarkdownView == MarkdownViewMode.Preview) { window.PreviewFocused = true; window.BrowserContentFocused = true; }
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ RetireBrowserSession(page); window.PreviewSource = operation.Request.Source; window.PreviewError = error.Message; window.Status = "Document preview failed: " + error.Message; }
}
else { window.PreviewSource = operation.Request.Source; window.PreviewError = result.Error; window.Status = "Document preview failed: " + result.Error; }
InvalidateScene(window.Bounds);
}
if (ShowsPreview(window) && window.PreviewSource != window.Content && window.PreviewPreparation is null)
{
if (window.PreviewRefreshAt == 0) window.PreviewRefreshAt = now + 180;
else if (now >= window.PreviewRefreshAt) BeginMarkdownPreview(window);
}
else if (window.PreviewSource == window.Content) window.PreviewRefreshAt = 0;
if (ShowsPreview(window) && window.PreviewSession is { } preview && (!window.IsMinimized || preview is IBackgroundEditorSession) && preview.AdvanceTime(now))
InvalidateScene(PreviewViewport(window));
}
}
private void DrawMarkdownToolbar(DesktopWindow window)
{
var b = window.Bounds;
Button(HitKind.EditorSave, window.Id, 0, new(b.X + 4, b.Y + 35, 84, 30), Panel);
DrawIcon(DesktopIcon.TextFile, b.X + 12, b.Y + 39, 18); _canvas.Text("Save", TextColor, b.X + 34, b.Y + 43);
var split = MarkdownSplitButton(window); var edit = MarkdownEditButton(window);
Button(HitKind.EditorSplit, window.Id, 0, split, window.MarkdownView == MarkdownViewMode.Split ? _theme.Selection : Panel);
Button(HitKind.EditorEdit, window.Id, 0, edit, Panel);
_canvas.Text("Split", TextColor, split.X + 25, split.Y + 8);
_canvas.Text(window.MarkdownView == MarkdownViewMode.Preview ? "Edit" : "Preview", TextColor, edit.X + 16, edit.Y + 8);
}
}
namespace XFESystem.Desktop;
public sealed partial class DesktopSession
{
private static DesktopBounds DocumentBody(DesktopWindow window) => new(window.Bounds.X + 2, window.Bounds.Y + 67,
Math.Max(1, window.Bounds.Width - 4), Math.Max(1, window.Bounds.Height - 96));
private static DesktopBounds EditorViewport(DesktopWindow window)
{
var body = DocumentBody(window);
return window.IsMarkdown && window.MarkdownView == MarkdownViewMode.Split
? body with { Width = Math.Max(1, (body.Width - 4) / 2) } : body;
}
private static DesktopBounds PreviewViewport(DesktopWindow window)
{
var body = DocumentBody(window);
if (window.MarkdownView != MarkdownViewMode.Split) return body;
int left = Math.Max(1, (body.Width - 4) / 2);
return new(body.X + left + 4, body.Y, Math.Max(1, body.Width - left - 4), body.Height);
}
private static bool ShowsSource(DesktopWindow window) => !window.IsMarkdown || window.MarkdownView != MarkdownViewMode.Preview;
private static bool ShowsPreview(DesktopWindow window) => window.IsMarkdown && window.MarkdownView != MarkdownViewMode.Edit;
private static IInteractiveBrowserSession? FocusedPage(DesktopWindow window) => window.IsMarkdown && window.PreviewFocused ? window.PreviewSession : window.BrowserSession;
private DesktopBounds MarkdownEditButton(DesktopWindow window) => new(window.Bounds.X + window.Bounds.Width - 98, window.Bounds.Y + 35, 92, 30);
private DesktopBounds MarkdownSplitButton(DesktopWindow window) => new(window.Bounds.X + window.Bounds.Width - 194, window.Bounds.Y + 35, 92, 30);
/// <summary>Mode changes retain the unsaved editor model and the independent preview page.</summary>
public void SetMarkdownView(int windowId, MarkdownViewMode mode)
{
var window = _windows.Find(candidate => candidate.Id == windowId && candidate.IsMarkdown);
if (window is null) return;
if (window.BrowserSession is IBackgroundEditorSession background && mode != MarkdownViewMode.Edit)
{
window.Status = "Preparing preview after queued editor input...";
if (!background.RequestEditorReadback((source, error) =>
{
if (!_windows.Contains(window)) return;
if (source is null || error is not null)
{ window.Status = "Preview failed: " + error; InvalidateScene(window.Bounds); return; }
if (window.Content != source) { window.Content = source; window.IsDirty = true; }
SetMarkdownViewCore(window, mode);
})) window.Status = "Preview could not be queued; retry when the editor worker is available.";
InvalidateScene(window.Bounds);
return;
}
if (window.BrowserSession?.ReadEditorText() is { } latest && latest != window.Content)
{ window.Content = latest; window.IsDirty = true; }
SetMarkdownViewCore(window, mode);
}
private void SetMarkdownViewCore(DesktopWindow window, MarkdownViewMode mode)
{
CancelPointerCapture();
(_webPointerCapturePreview ? _webPointerCapture?.PreviewSession : _webPointerCapture?.BrowserSession)?.Pointer(new("pointercancel", -1, -1));
_webPointerCapture = _webHoveredWindow = null;
window.MarkdownView = mode;
window.PreviewFocused = mode == MarkdownViewMode.Preview;
window.BrowserContentFocused = mode == MarkdownViewMode.Preview ? window.PreviewSession is not null : window.BrowserSession is not null;
if (mode != MarkdownViewMode.Preview && window.BrowserSession is null && window.EditorPreparation is null)
{
if (_editorPreparation is not null) BeginEditorPreparation(window);
// A missing worker still leaves a responsive basic source editor.
else window.Status = "Text editor | Ctrl+S saves | Split shows the live document";
}
if (ShowsPreview(window) && window.PreviewSource != window.Content) BeginMarkdownPreview(window);
ResizeDocumentPanes(window);
InvalidateScene(window.Bounds);
}
private void ResizeDocumentPanes(DesktopWindow window)
{
if (!window.IsMarkdown) return;
var source = EditorViewport(window); var preview = PreviewViewport(window);
if (ShowsSource(window)) window.BrowserSession?.ResizeViewport(source.Width, source.Height);
if (ShowsPreview(window)) window.PreviewSession?.ResizeViewport(preview.Width, preview.Height);
window.PreviewScroll = Math.Clamp(window.PreviewScroll, 0, Math.Max(0, (window.PreviewSession?.DocumentHeight ?? 0) - preview.Height));
}
private void BeginMarkdownPreview(DesktopWindow window)
{
if (window.PreviewPreparation is { } pending)
{ RetireEditorPreparation(pending); window.PreviewPreparation = null; }
var viewport = PreviewViewport(window);
window.PreviewObservedContent = window.Content;
window.PreviewError = null;
window.PreviewRefreshAt = 0;
window.Status = "Loading document preview in background...";
if (_editorPreparation is not null)
{
var request = new EditorPreparationRequest(window.Id, ++_nextEditorPreparationGeneration, window.Content, window.Location,
viewport.Width, viewport.Height, _monotonicMilliseconds?.Invoke() ?? _feedbackNow, EditorPreparationKind.Document, _theme.IsDark);
window.PreviewPreparationGeneration = request.Generation;
try { window.PreviewPreparation = _editorPreparation.Begin(request); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ window.PreviewSource = window.Content; window.PreviewError = error.Message; window.Status = "Document preview unavailable: " + error.Message; }
return;
}
// Explicit synchronous adapters are retained for host tooling. The
// native desktop always supplies the verified background service.
if (_browser is not IInteractiveBrowserFactory factory)
{ window.PreviewSource = window.Content; window.PreviewError = "Document preview requires the web renderer"; window.Status = window.PreviewError; return; }
IInteractiveBrowserSession? page = null;
try
{
page = factory.CreatePageSession(); page.SetCanvas(_canvas); page.ResizeViewport(viewport.Width, viewport.Height);
page.BeginDocumentPreview(window.Content, window.Location, _theme.IsDark);
RetireBrowserSession(window.PreviewSession); window.PreviewSession = page; window.PreviewSource = window.Content;
window.PreviewFocused = window.MarkdownView == MarkdownViewMode.Preview; window.BrowserContentFocused = window.PreviewFocused;
window.Status = "Document preview | Edit or Split to change the source";
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ RetireBrowserSession(page); window.PreviewSource = window.Content; window.PreviewError = error.Message; window.Status = "Document preview failed: " + error.Message; }
}
private void PollMarkdownPreviews(long now)
{
foreach (var window in _windows)
{
if (!window.IsMarkdown) continue;
if (window.PreviewObservedContent != window.Content)
{ window.PreviewObservedContent = window.Content; window.PreviewRefreshAt = now + 180; }
if (window.PreviewPreparation is { } operation && operation.TryTakeResult(out var result))
{
window.PreviewPreparation = null;
if (operation.Request.Generation != window.PreviewPreparationGeneration || operation.Request.Source != window.Content || operation.Request.Path != window.Location || operation.Request.Dark != _theme.IsDark)
{
RetireBrowserSession(result.Page); RetireEditorPreparation(operation);
window.PreviewRefreshAt = now + 180;
}
else if (result.Page is { } page)
{
try
{
bool equivalent = result.PreparedCanvas is IEditorPreparationCanvasHandoff handoff && handoff.TryCompleteEditorPreparationHandoff(_canvas);
page.CompleteEditorPreparationHandoff(_canvas, equivalent);
RetireBrowserSession(window.PreviewSession); window.PreviewSession = page;
window.PreviewSource = operation.Request.Source;
ResizeDocumentPanes(window);
window.Status = "Document preview | Edit or Split to change the source";
if (window.MarkdownView == MarkdownViewMode.Preview) { window.PreviewFocused = true; window.BrowserContentFocused = true; }
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ RetireBrowserSession(page); window.PreviewSource = operation.Request.Source; window.PreviewError = error.Message; window.Status = "Document preview failed: " + error.Message; }
}
else { window.PreviewSource = operation.Request.Source; window.PreviewError = result.Error; window.Status = "Document preview failed: " + result.Error; }
InvalidateScene(window.Bounds);
}
if (ShowsPreview(window) && window.PreviewSource != window.Content && window.PreviewPreparation is null)
{
if (window.PreviewRefreshAt == 0) window.PreviewRefreshAt = now + 180;
else if (now >= window.PreviewRefreshAt) BeginMarkdownPreview(window);
}
else if (window.PreviewSource == window.Content) window.PreviewRefreshAt = 0;
if (ShowsPreview(window) && window.PreviewSession is { } preview && (!window.IsMinimized || preview is IBackgroundEditorSession) && preview.AdvanceTime(now))
InvalidateScene(PreviewViewport(window));
}
}
private void DrawMarkdownToolbar(DesktopWindow window)
{
var b = window.Bounds;
Button(HitKind.EditorSave, window.Id, 0, new(b.X + 4, b.Y + 35, 84, 30), Panel);
DrawIcon(DesktopIcon.TextFile, b.X + 12, b.Y + 39, 18); _canvas.Text("Save", TextColor, b.X + 34, b.Y + 43);
var split = MarkdownSplitButton(window); var edit = MarkdownEditButton(window);
Button(HitKind.EditorSplit, window.Id, 0, split, window.MarkdownView == MarkdownViewMode.Split ? _theme.Selection : Panel);
Button(HitKind.EditorEdit, window.Id, 0, edit, Panel);
_canvas.Text("Split", TextColor, split.X + 25, split.Y + 8);
_canvas.Text(window.MarkdownView == MarkdownViewMode.Preview ? "Edit" : "Preview", TextColor, edit.X + 16, edit.Y + 8);
}
}