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); /// Mode changes retain the unsaved editor model and the independent preview page. 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); } }