diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c944075a..e1a3be3a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,18 @@ ### Changed +* [PostIt] La liste des billets et l'éditeur Markdown se partagent + l'espace disponible et défilent séparément ; un séparateur permet + de redimensionner les deux zones. + ### Fixed +* [PostIt] Défilement des articles longs en édition et en aperçu : + suppression de la limite de hauteur qui masquait le contenu Markdown + et du défilement imbriqué autour d'AvaloniaEdit. Six tests headless + couvrent les barres automatiques, le redimensionnement et le + défilement indépendant de la liste. + ## [1.0.8-rc23] - unstable ### Added diff --git a/doc/architecture/postit.md b/doc/architecture/postit.md index 70f3f2fdc..b1bf83cf7 100644 --- a/doc/architecture/postit.md +++ b/doc/architecture/postit.md @@ -126,6 +126,20 @@ le DI est construit. Ordre, dans cet ordre : > à cette table, *ne pas* ajouter de registration pour > `Settings` ailleurs que la ligne `AddSingleton(settings)`. +## Défilement de la page des billets + +La page `BlogsPage` utilise une grille à hauteur contrainte : l'en-tête +et les commandes restent visibles, tandis que la liste des billets et +l'éditeur se partagent l'espace disponible. Un `GridSplitter` permet +de modifier cette répartition. La liste et l'article défilent séparément. + +Dans `MarkdownEditorControl`, AvaloniaEdit gère son propre défilement ; +l'aperçu Markdown est placé dans un `ScrollViewer`. Ne pas limiter +la hauteur du `MarkdownViewer` lui-même : son contenu doit pouvoir +dépasser le viewport pour que les barres automatiques s'activent. +Ne pas placer cette grille dans un `StackPanel` vertical ou un +`ScrollViewer` de page, qui lui donnerait une hauteur de mesure illimitée. + ## Navigation Le host de navigation est un `NavigationPage x:Name="NavRoot"` diff --git a/src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs b/src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs new file mode 100644 index 000000000..1a83330f9 --- /dev/null +++ b/src/PostIt/PostIt.Tests/MarkdownEditorScrollTests.cs @@ -0,0 +1,143 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Headless.XUnit; +using Avalonia.LogicalTree; +using Avalonia.Threading; +using Avalonia.VisualTree; +using AvaloniaEdit; +using PostIt.Controls; +using PostIt.Views.Blogs; + +namespace PostIt.Tests; + +public class MarkdownEditorScrollTests +{ + private static readonly string LongArticle = + string.Join("\n\n", Enumerable.Range(1, 100).Select(i => $"Paragraph {i}: article content.")); + + [AvaloniaTheory] + [InlineData(false)] + [InlineData(true)] + public void Long_article_scrolls_to_the_end_and_adapts_to_resizing(bool preview) + { + var editor = new MarkdownEditorControl + { + MarkdownText = LongArticle, + IsPreviewVisible = preview + }; + var window = new Window { Width = 600, Height = 400, Content = editor }; + try + { + window.Show(); + Dispatcher.UIThread.RunJobs(); + window.UpdateLayout(); + + var scroller = FindArticleScroller(editor, preview); + AssertScrollable(scroller); + var initialViewport = scroller.Viewport.Height; + + window.Height = 300; + Dispatcher.UIThread.RunJobs(); + window.UpdateLayout(); + + Assert.True(scroller.Viewport.Height < initialViewport); + AssertScrollable(scroller); + } + finally + { + CloseWindow(window, editor); + } + } + + [AvaloniaTheory] + [InlineData(false)] + [InlineData(true)] + public void Blog_page_bounds_the_article_viewport(bool preview) + { + var page = new BlogsPage(); + var editor = page.GetLogicalDescendants().OfType().Single(); + editor.MarkdownText = LongArticle; + editor.IsPreviewVisible = preview; + var posts = page.FindControl("PostsListBox")!; + posts.ItemsSource = Enumerable.Range(1, 50) + .Select(i => new Yavsc.Blogspot.BlogPostDto { Title = $"Post {i}" }).ToArray(); + var navigation = new NavigationPage(); + navigation.PushAsync(page).GetAwaiter().GetResult(); + var window = new Window { Width = 800, Height = 700, Content = navigation }; + try + { + window.Show(); + Dispatcher.UIThread.RunJobs(); + window.UpdateLayout(); + + var scroller = FindArticleScroller(editor, preview); + Assert.InRange(scroller.Viewport.Height, 1, window.ClientSize.Height); + AssertScrollable(scroller); + AssertScrollable(posts.GetVisualDescendants().OfType().Single()); + var editorBottom = editor.TranslatePoint(new Point(0, editor.Bounds.Height), window); + Assert.NotNull(editorBottom); + Assert.InRange(editorBottom.Value.Y, 1, window.ClientSize.Height); + } + finally + { + CloseWindow(window, editor); + } + } + + [AvaloniaTheory] + [InlineData(false)] + [InlineData(true)] + public void Short_article_needs_no_vertical_scrollbar(bool preview) + { + var editor = new MarkdownEditorControl + { + MarkdownText = "Short article", + IsPreviewVisible = preview + }; + var window = new Window { Width = 600, Height = 400, Content = editor }; + try + { + window.Show(); + Dispatcher.UIThread.RunJobs(); + window.UpdateLayout(); + + var scroller = FindArticleScroller(editor, preview); + Assert.True(scroller.Extent.Height <= scroller.Viewport.Height); + Assert.DoesNotContain(scroller.GetVisualDescendants().OfType(), + bar => bar.Orientation == Avalonia.Layout.Orientation.Vertical && bar.IsVisible); + } + finally + { + CloseWindow(window, editor); + } + } + + private static void CloseWindow(Window window, MarkdownEditorControl editor) + { + // The package's shared renderer caches the visual; detach it between headless applications. + editor.FindControl("PreviewViewer")!.Content = null; + window.UpdateLayout(); + window.Close(); + } + + private static ScrollViewer FindArticleScroller(MarkdownEditorControl editor, bool preview) + { + if (!preview) + return editor.GetVisualDescendants().OfType().Single() + .GetVisualDescendants().OfType().Single(); + + return editor.FindControl("PreviewScrollViewer")!; + } + + private static void AssertScrollable(ScrollViewer scroller) + { + Assert.True(scroller.Extent.Height > scroller.Viewport.Height, + $"Expected overflow, but extent={scroller.Extent.Height}, viewport={scroller.Viewport.Height}."); + Assert.Contains(scroller.GetVisualDescendants().OfType(), + bar => bar.Orientation == Avalonia.Layout.Orientation.Vertical && bar.IsVisible); + scroller.ScrollToEnd(); + Assert.True(scroller.Offset.Y > 0); + Assert.Equal(scroller.Extent.Height - scroller.Viewport.Height, scroller.Offset.Y, 1); + } +} diff --git a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml index 9e8df6dd1..721071d2e 100644 --- a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml +++ b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml @@ -7,35 +7,27 @@ x:Name="Root" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + - + - - + TextInputOptions.Multiline="True" + TextInputOptions.ReturnKeyType="Default" /> + Padding="8"> + VerticalAlignment="Stretch" + HorizontalScrollBarVisibility="Auto"> + HorizontalContentAlignment="Stretch" + VerticalAlignment="Top" + ClipToBounds="False" + UseLayoutRounding="True" + MarkdownText="{Binding MarkdownText, ElementName=Root}" /> diff --git a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs index 0431b726e..2538f7168 100644 --- a/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs +++ b/src/PostIt/PostIt/Controls/MarkdownEditorControl.axaml.cs @@ -38,8 +38,6 @@ public partial class MarkdownEditorControl : UserControl { InitializeComponent(); PropertyChanged += OnPropertyChanged; - SizeChanged += OnSizeChanged; - UpdateEditorLimits(); } private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) @@ -62,27 +60,4 @@ public partial class MarkdownEditorControl : UserControl } } } - - private void OnSizeChanged(object? sender, SizeChangedEventArgs e) - { - UpdateEditorLimits(); - } - - private void UpdateEditorLimits() - { - var visibleHeight = Bounds.Height > 0 ? Bounds.Height - 24 : 260; - visibleHeight = Math.Max(visibleHeight, 80); - - if (SourceEditor is not null) - { - SourceEditor.MaxHeight = visibleHeight; - SourceEditor.Height = double.NaN; - } - - if (PreviewViewer is not null) - { - PreviewViewer.MaxHeight = visibleHeight; - PreviewViewer.Height = double.NaN; - } - } } diff --git a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml index eb88fb2d8..7da9a4ce8 100644 --- a/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml +++ b/src/PostIt/PostIt/Views/Blogs/BlogsPage.axaml @@ -17,14 +17,11 @@ - - + VerticalAlignment="Stretch"> @@ -107,16 +105,14 @@ - + - - - - + + @@ -161,6 +157,11 @@ + + + @@ -217,6 +219,5 @@ - - +