diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextAreaReadableType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextAreaReadableType.java index 793ca7c2..ef95fe12 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextAreaReadableType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextAreaReadableType.java @@ -16,8 +16,7 @@ package com.io7m.jsycamore.api.components; -import com.io7m.jattribute.core.AttributeReadableType; -import com.io7m.jsycamore.api.text.SyText; +import com.io7m.jsycamore.api.text.SyTextMultiLineModelReadableType; import com.io7m.jsycamore.api.themes.SyThemeClassNameType; import java.util.List; @@ -38,10 +37,10 @@ default List themeClassesDefaultForComponent() } /** - * @return The text sections within the area + * @return The underlying text model */ - AttributeReadableType> textSections(); + SyTextMultiLineModelReadableType model(); /** * @return Access to the horizontal scrollbar diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextAreaType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextAreaType.java index 4f2b56ab..6df49f5e 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextAreaType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextAreaType.java @@ -16,7 +16,7 @@ package com.io7m.jsycamore.api.components; -import com.io7m.jsycamore.api.text.SyText; +import com.io7m.jsycamore.api.text.SyTextMultiLineModelType; /** * Write access to text areas. @@ -25,13 +25,8 @@ public interface SyTextAreaType extends SyTextAreaReadableType, SyComponentType { - /** - * Append a text section to the end of the text area. - * - * @param section The section - */ - - void textSectionAppend(SyText section); + @Override + SyTextMultiLineModelType model(); @Override SyScrollBarHorizontalType scrollbarHorizontal(); diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextMultiLineViewReadableType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextMultiLineViewReadableType.java index 03d7a138..9b6395e3 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextMultiLineViewReadableType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextMultiLineViewReadableType.java @@ -18,12 +18,10 @@ import com.io7m.jattribute.core.AttributeReadableType; import com.io7m.jsycamore.api.layout.SyLayoutContextType; -import com.io7m.jsycamore.api.text.SyTextLineMeasuredType; -import com.io7m.jsycamore.api.text.SyTextLinePositioned; +import com.io7m.jsycamore.api.text.SyTextMultiLineModelReadableType; import com.io7m.jsycamore.api.themes.SyThemeClassNameType; import java.util.List; -import java.util.Optional; import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.TEXT_MULTILINE_VIEW; @@ -56,10 +54,10 @@ default List themeClassesDefaultForComponent() } /** - * @return A read-only snapshot of the positioned lines of text + * @return The text model */ - Iterable textLinesPositioned(); + SyTextMultiLineModelReadableType model(); /** * Determine the minimum size on the Y axis required to display the @@ -70,13 +68,6 @@ default List themeClassesDefaultForComponent() * @return The minimum size on the Y axis */ - int minimumSizeYRequired(SyLayoutContextType layoutContext); - - /** - * @param y The Y offset - * - * @return The line starting at Y offset {@code y} - */ - - Optional textByYOffset(int y); + int minimumSizeYRequired( + SyLayoutContextType layoutContext); } diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextMultiLineViewType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextMultiLineViewType.java index 6c97bbb4..334f989d 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextMultiLineViewType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyTextMultiLineViewType.java @@ -17,12 +17,8 @@ package com.io7m.jsycamore.api.components; import com.io7m.jattribute.core.AttributeType; -import com.io7m.jsycamore.api.text.SyText; import com.io7m.jsycamore.api.text.SyTextMultiLineModelType; -import java.util.List; -import java.util.Objects; - /** * Write access to multi-line text views. */ @@ -49,30 +45,6 @@ default void setTextSelectable( this.textSelectable().set(Boolean.valueOf(selectable)); } - /** - * Append a section of text. - * - * @param section The text section - * - * @see SyTextMultiLineModelType#textSectionAppend(SyText) - */ - - default void textSectionAppend( - final SyText section) - { - this.textSectionsAppend( - List.of(Objects.requireNonNull(section, "section")) - ); - } - - /** - * Append sections of text. - * - * @param sections The text sections - * - * @see SyTextMultiLineModelType#textSectionsAppend(List) - */ - - void textSectionsAppend( - List sections); + @Override + SyTextMultiLineModelType model(); } diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/screens/SyScreenType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/screens/SyScreenType.java index f149f40b..b93f3444 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/screens/SyScreenType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/screens/SyScreenType.java @@ -24,6 +24,7 @@ import com.io7m.jsycamore.api.sized.SySizedType; import com.io7m.jsycamore.api.spaces.SySpaceViewportType; import com.io7m.jsycamore.api.text.SyTextSelectionServiceType; +import com.io7m.jsycamore.api.themes.SyThemeContextType; import com.io7m.jsycamore.api.themes.SyThemeType; import com.io7m.jsycamore.api.windows.SyWindowServiceType; import com.io7m.jtensors.core.parameterized.vectors.PVector2I; @@ -74,6 +75,12 @@ default SyTextSelectionServiceType textSelectionService() return this.services().requireService(SyTextSelectionServiceType.class); } + /** + * @return The theme context + */ + + SyThemeContextType themeContext(); + /** * @return The current theme used by the screen */ diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextID.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextID.java index 4ee9e261..e502ab88 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextID.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextID.java @@ -31,6 +31,15 @@ public record SyTextID(BigInteger value) private static final SyTextID FIRST_ID = new SyTextID(BigInteger.ZERO); + /** + * @return The first text ID + */ + + public static SyTextID first() + { + return FIRST_ID; + } + @Override public int compareTo( final SyTextID other) @@ -38,21 +47,18 @@ public int compareTo( return this.value.compareTo(other.value); } - /** - * @return The next text ID - */ - - public SyTextID next() + @Override + public String toString() { - return new SyTextID(this.value.add(BigInteger.ONE)); + return this.value.toString(); } /** - * @return The first text ID + * @return The next text ID */ - public static SyTextID first() + public SyTextID next() { - return FIRST_ID; + return new SyTextID(this.value.add(BigInteger.ONE)); } } diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextLineNumber.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextLineNumber.java index c4deaf91..29409751 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextLineNumber.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextLineNumber.java @@ -59,4 +59,16 @@ public SyTextLineNumber next() { return new SyTextLineNumber(this.value + 1); } + + /** + * @param delta The delta + * + * @return The line number adjusted by the given delta + */ + + public SyTextLineNumber adjust( + final int delta) + { + return new SyTextLineNumber(this.value + delta); + } } diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextMultiLineModelReadableType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextMultiLineModelReadableType.java index e151cf1c..39777c0a 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextMultiLineModelReadableType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextMultiLineModelReadableType.java @@ -47,17 +47,6 @@ public interface SyTextMultiLineModelReadableType SortedMap textSections(); - /** - * The text section that contains the given line number. - * - * @param lineNumber The line number - * - * @return The text section, if any - */ - - Optional textSectionContainingLine( - SyTextLineNumber lineNumber); - /** * Inspect the text at the given position. The information returned includes * details such as the index of the character within the string at the given @@ -82,16 +71,17 @@ Optional inspectAt( int minimumSizeYRequired(); /** - * @return A read-only snapshot of the positioned lines of text + * @return The current number of lines */ - Iterable textLinesPositioned(); + int lineCount(); /** - * @param y The y offset + * @param line The line number * - * @return The measured line that starts at Y offset {@code y} + * @return The line at the given number */ - Optional textByYOffset(int y); + Optional lineAt( + SyTextLineNumber line); } diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextMultiLineModelType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextMultiLineModelType.java index dc472256..4cda1160 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextMultiLineModelType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/text/SyTextMultiLineModelType.java @@ -49,6 +49,17 @@ void setFont( void setPageWidth( int width); + /** + * Replace an existing text section. + * + * @param textID The text ID + * @param text The new text section + */ + + void textSectionReplace( + SyTextID textID, + SyText text); + /** * Append a section of text at the end of the model. * diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyLineMap.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyLineMap.java deleted file mode 100644 index 56040bb4..00000000 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyLineMap.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright © 2023 Mark Raynsford https://www.io7m.com - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR - * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - - -package com.io7m.jsycamore.components.standard.text; - -import com.io7m.jsycamore.api.text.SyTextID; -import com.io7m.jsycamore.api.text.SyTextLineNumber; - -import java.util.Collections; -import java.util.Objects; -import java.util.Optional; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -/** - * A multimap from text IDs to sets of line numbers. - */ - -final class SyLineMap -{ - private final TreeMap> items; - - SyLineMap() - { - this.items = new TreeMap<>(); - } - - SortedSet linesForText( - final SyTextID id) - { - return Optional.ofNullable(this.items.get(id)) - .orElse(Collections.emptySortedSet()); - } - - void clear() - { - this.items.clear(); - } - - void lineAssociate( - final SyTextID id, - final SyTextLineNumber lineNumber) - { - Objects.requireNonNull(id, "id"); - Objects.requireNonNull(lineNumber, "lineNumber"); - - var lines = this.items.get(id); - if (lines == null) { - lines = new TreeSet<>(); - } - lines.add(lineNumber); - this.items.put(id, lines); - } - - void lineDisassociate( - final SyTextID id, - final SyTextLineNumber lineNumber) - { - Objects.requireNonNull(id, "id"); - Objects.requireNonNull(lineNumber, "lineNumber"); - - final var lines = this.items.get(id); - if (lines == null) { - return; - } - lines.remove(lineNumber); - this.items.put(id, lines); - } - - SortedSet lineDisassociateAll( - final SyTextID id) - { - Objects.requireNonNull(id, "id"); - return Optional.ofNullable(this.items.remove(id)) - .orElse(Collections.emptySortedSet()); - } -} diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyLineYBiMap.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyLineYBiMap.java deleted file mode 100644 index ec6e345b..00000000 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyLineYBiMap.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright © 2023 Mark Raynsford https://www.io7m.com - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY - * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR - * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - - -package com.io7m.jsycamore.components.standard.text; - -import com.io7m.jsycamore.api.text.SyTextLineNumber; - -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Optional; -import java.util.TreeMap; - -/** - * A bidirectional map between line numbers and Y offsets. - */ - -final class SyLineYBiMap -{ - private final TreeMap lineToY; - private final TreeMap yToLine; - - SyLineYBiMap() - { - this.lineToY = new TreeMap<>(); - this.yToLine = new TreeMap<>(); - } - - void clear() - { - this.lineToY.clear(); - this.yToLine.clear(); - } - - void set( - final SyTextLineNumber lineNumber, - final int y) - { - Objects.requireNonNull(lineNumber, "lineNumber"); - - final var boxY = Integer.valueOf(y); - this.removeByLine(lineNumber); - this.removeByY(y); - - this.lineToY.put(lineNumber, boxY); - this.yToLine.put(boxY, lineNumber); - } - - Optional lineContainingY( - final int y) - { - return Optional.ofNullable(this.yToLine.floorEntry(Integer.valueOf(y))) - .map(Map.Entry::getValue); - } - - Optional highestY() - { - try { - return Optional.of(this.yToLine.lastKey()); - } catch (final NoSuchElementException e) { - return Optional.empty(); - } - } - - Optional removeByLine( - final SyTextLineNumber line) - { - Objects.requireNonNull(line, "line"); - - final var y = this.lineToY.remove(line); - if (y != null) { - this.yToLine.remove(y); - } - return Optional.ofNullable(y); - } - - Optional removeByY( - final int y) - { - final var line = this.yToLine.remove(Integer.valueOf(y)); - if (line != null) { - this.lineToY.remove(line); - } - return Optional.ofNullable(line); - } - - Optional line( - final int y) - { - return Optional.ofNullable(this.yToLine.get(Integer.valueOf(y))); - } - - Optional y( - final SyTextLineNumber lineNumber) - { - Objects.requireNonNull(lineNumber, "lineNumber"); - return Optional.ofNullable(this.lineToY.get(lineNumber)); - } -} diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextArea.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextArea.java index fc9eac03..88fbe6d4 100644 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextArea.java +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextArea.java @@ -17,7 +17,6 @@ package com.io7m.jsycamore.components.standard.text; -import com.io7m.jattribute.core.AttributeReadableType; import com.io7m.jattribute.core.AttributeType; import com.io7m.jregions.core.parameterized.sizes.PAreaSizeI; import com.io7m.jsycamore.api.components.SyConstraints; @@ -25,12 +24,14 @@ import com.io7m.jsycamore.api.components.SyScrollBarVerticalType; import com.io7m.jsycamore.api.components.SyScrollPaneType; import com.io7m.jsycamore.api.components.SyTextAreaType; +import com.io7m.jsycamore.api.components.SyTextMultiLineViewType; import com.io7m.jsycamore.api.events.SyEventConsumed; import com.io7m.jsycamore.api.events.SyEventInputType; import com.io7m.jsycamore.api.layout.SyLayoutContextType; import com.io7m.jsycamore.api.screens.SyScreenType; import com.io7m.jsycamore.api.spaces.SySpaceParentRelativeType; import com.io7m.jsycamore.api.text.SyText; +import com.io7m.jsycamore.api.text.SyTextMultiLineModelType; import com.io7m.jsycamore.api.themes.SyThemeClassNameType; import com.io7m.jsycamore.components.standard.SyComponentAbstract; import com.io7m.jsycamore.components.standard.SyComponentAttributes; @@ -38,7 +39,6 @@ import com.io7m.jsycamore.components.standard.SyScrollPanes; import java.util.List; -import java.util.Objects; import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; @@ -55,7 +55,7 @@ public final class SyTextArea private final AttributeType> textSections; private final SyScrollPaneType textScroller; private final SyLayoutMargin textLayoutMargin; - private final SyTextMultiLineView textMultiLine; + private final SyTextMultiLineViewType textMultiLine; /** * A text area. @@ -76,7 +76,7 @@ public SyTextArea( attributes.create(List.of()); this.textMultiLine = - new SyTextMultiLineView(screen, List.of()); + SyTextMultiLineView.multiLineTextView(screen, List.of()); this.textLayoutMargin = new SyLayoutMargin(screen); this.textScroller = @@ -95,12 +95,6 @@ protected SyEventConsumed onEventInput( return EVENT_NOT_CONSUMED; } - @Override - public AttributeReadableType> textSections() - { - return this.textSections; - } - @Override public PAreaSizeI layout( final SyLayoutContextType layoutContext, @@ -126,8 +120,7 @@ public PAreaSizeI layout( .sizeX(); final var contentSizeY = - this.textMultiLine.minimumSizeYRequired(layoutContext) - + PADDING; + this.textMultiLine.minimumSizeYRequired(layoutContext) + PADDING; this.textScroller.setContentAreaSize( PAreaSizeI.of(contentSizeX, contentSizeY) @@ -137,11 +130,9 @@ public PAreaSizeI layout( } @Override - public void textSectionAppend( - final SyText section) + public SyTextMultiLineModelType model() { - Objects.requireNonNull(section, "section"); - this.textMultiLine.textSectionAppend(section); + return this.textMultiLine.model(); } @Override diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextMultiLineModel.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextMultiLineModel.java index 0f01878f..b1febc67 100644 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextMultiLineModel.java +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextMultiLineModel.java @@ -36,10 +36,8 @@ import com.io7m.jtensors.core.parameterized.vectors.PVector2I; import java.util.Collections; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; @@ -54,13 +52,65 @@ public final class SyTextMultiLineModel implements SyTextMultiLineModelType { private final AttributeType pageWidth; private final TreeMap textSections; - private final TreeMap textLines; - private final SyLineYBiMap textLinesY; + private final TreeMap textSectionsFormatted; + private final TreeMap textSectionsFormattedByLine; + private final TreeMap textSectionsFormattedByY; private final AttributeType font; - private final SyLineMap textSectionsToLines; private final SortedMap textSectionsReadable; private SySelectionState selectionState; + private static final class SyTextFormatted + { + private final SyTextID textID; + private SyTextLineNumber lineNumber; + private final List lines; + private int yOffset; + + SyTextFormatted( + final SyTextID inTextID, + final SyTextLineNumber inLineNumber, + final List inLines, + final int inYOffset) + { + this.textID = + Objects.requireNonNull(inTextID, "textID"); + this.lineNumber = + Objects.requireNonNull(inLineNumber, "lineNumber"); + this.lines = + Objects.requireNonNull(inLines, "inLines"); + this.yOffset = + inYOffset; + } + + public int height() + { + return this.lines.stream() + .mapToInt(SyTextLineMeasuredType::height) + .sum(); + } + + public Optional lineAt( + final SyTextLineNumber targetLineNumber) + { + var y = this.yOffset; + final var lineOffset = targetLineNumber.value() - this.lineNumber.value(); + for (int index = 0; index <= lineOffset; ++index) { + final var line = this.lines.get(index); + if (index == lineOffset) { + return Optional.of( + new SyTextLinePositioned( + y, + this.lineNumber.adjust(index), + this.lines.get(index) + ) + ); + } + y += line.height(); + } + return Optional.empty(); + } + } + private SyTextMultiLineModel( final SyFontType inFont, final int inPageWidth) @@ -76,12 +126,12 @@ private SyTextMultiLineModel( new TreeMap<>(); this.textSectionsReadable = Collections.unmodifiableSortedMap(this.textSections); - this.textSectionsToLines = - new SyLineMap(); - this.textLines = + this.textSectionsFormatted = + new TreeMap<>(); + this.textSectionsFormattedByLine = + new TreeMap<>(); + this.textSectionsFormattedByY = new TreeMap<>(); - this.textLinesY = - new SyLineYBiMap(); /* * Changing the page width or the font will require re-measuring all @@ -90,12 +140,12 @@ private SyTextMultiLineModel( this.pageWidth.subscribe((oldValue, newValue) -> { if (!Objects.equals(oldValue, newValue)) { - this.edit(SyEditOpRegenerate.SY_EDIT_OP_REGENERATE); + this.edit(SyEditOpRegenerateAll.SY_EDIT_OP_REGENERATE); } }); this.font.subscribe((oldValue, newValue) -> { if (!Objects.equals(oldValue, newValue)) { - this.edit(SyEditOpRegenerate.SY_EDIT_OP_REGENERATE); + this.edit(SyEditOpRegenerateAll.SY_EDIT_OP_REGENERATE); } }); } @@ -142,72 +192,229 @@ private void edit( final SyEditOpType edit) { switch (edit) { - case final SyEditOpRegenerate op -> this.editRegenerate(); - case final SyEditOpAppend op -> this.editAppend(op); + case final SyEditOpRegenerateAll op -> { + this.editRegenerateAll(); + } + case final SyEditOpAppend op -> { + this.editAppend(op); + } + case final SyEditOpReplace op -> { + this.editReplace(op); + } + } + } + + private void editReplace( + final SyEditOpReplace op) + { + /* + * Replacing a section means removing the lines associated with the + * existing section, and then renumbering all lines that followed those + * removed lines. + */ + + if (!this.textSections.containsKey(op.textID)) { + throw new NoSuchElementException( + "No text present with ID %s".formatted(op.textID) + ); + } + + final var fontNow = + this.font.get(); + final var wrapNow = + this.pageWidth.get().intValue(); + + final var newLines = + fontNow.textLayout(op.textID, op.text, wrapNow); + final var existingFormatted = + this.textSectionsFormatted.get(op.textID); + + final var newFormatted = + new SyTextFormatted( + existingFormatted.textID, + existingFormatted.lineNumber, + newLines, + existingFormatted.yOffset + ); + + /* + * The number of lines that all the other lines need to be shifted + * is the difference between the number of new lines for this text, + * and the number of old lines. It's perfectly fine for the delta + * to be negative (we might be removing lines). + */ + + final var lineDelta = + newLines.size() - existingFormatted.lines.size(); + final var lowerLineNumber = + existingFormatted.lineNumber; + final var lowerY = + newFormatted.yOffset + newFormatted.height(); + + this.shiftLines(lowerLineNumber, lineDelta, lowerY); + + this.textSections.replace(op.textID, op.text); + this.textSectionsFormatted.put(existingFormatted.textID, newFormatted); + this.textSectionsFormattedByY.put(newFormatted.yOffset, newFormatted); + this.textSectionsFormattedByLine.put(newFormatted.lineNumber, newFormatted); + } + + /** + * Shift all lines greater than the given lower line number by the given + * line delta. All Y values will be recalculated assuming that the new + * starting Y value is {@code yStart}. + */ + + private void shiftLines( + final SyTextLineNumber lowerLineNumber, + final int delta, + final int yStart) + { + final var above = + List.copyOf( + this.textSectionsFormattedByLine.tailMap(lowerLineNumber, false) + .values() + ); + + for (final var aboveFormatted : above) { + this.textSectionsFormattedByLine.remove( + aboveFormatted.lineNumber + ); + this.textSectionsFormattedByY.remove( + Integer.valueOf(aboveFormatted.yOffset) + ); + aboveFormatted.lineNumber = aboveFormatted.lineNumber.adjust(delta); + } + + var y = yStart; + for (final var aboveFormatted : above) { + this.textSectionsFormattedByLine.put( + aboveFormatted.lineNumber, + aboveFormatted + ); + aboveFormatted.yOffset = y; + this.textSectionsFormattedByY.put( + Integer.valueOf(aboveFormatted.yOffset), + aboveFormatted + ); + y += aboveFormatted.height(); } } private void editAppend( final SyEditOpAppend op) { + /* + * Appending new sections means creating new measured lines for the + * appended text sections. + */ + + if (op.texts.isEmpty()) { + return; + } + + SyTextID nextID; + try { + nextID = this.textSections.lastKey().next(); + } catch (final NoSuchElementException e) { + nextID = SyTextID.first(); + } + + final SyTextID regenerateFrom = nextID; + for (final var section : op.texts) { + this.textSections.put(nextID, section); + nextID = nextID.next(); + } + final var fontNow = this.font.get(); final var wrapNow = this.pageWidth.get().intValue(); final var newSections = - this.textSections.tailMap(op.textID, true); - final var newMeasured = - fontNow.textLayoutMultiple(newSections, wrapNow); + this.textSections.tailMap(regenerateFrom, true); var lineNumber = this.nextFreeLineNumber(); var y = this.highestYOffset(); - for (final var measured : newMeasured) { - this.textLines.put(lineNumber, measured); - this.textLinesY.set(lineNumber, y); - this.textSectionsToLines.lineAssociate( - measured.textOriginal(), - lineNumber); - - y += measured.height(); - lineNumber = lineNumber.next(); + + for (final var entry : newSections.entrySet()) { + final var textId = + entry.getKey(); + final var text = + entry.getValue(); + final var lines = + fontNow.textLayout(textId, text, wrapNow); + + final var textFormatted = + new SyTextFormatted( + textId, + lineNumber, + lines, + y + ); + + this.textSectionsFormatted.put(textId, textFormatted); + this.textSectionsFormattedByLine.put(lineNumber, textFormatted); + this.textSectionsFormattedByY.put(Integer.valueOf(y), textFormatted); + + for (final var line : lines) { + lineNumber = lineNumber.next(); + y += line.height(); + } } } private SyTextLineNumber nextFreeLineNumber() { - try { - return this.textLines.lastKey().next(); - } catch (final NoSuchElementException e) { + final var lastEntry = + this.textSectionsFormatted.lastEntry(); + + if (lastEntry == null) { return SyTextLineNumber.first(); } + + final var formatted = lastEntry.getValue(); + return formatted.lineNumber.adjust(formatted.lines.size()); } - private void editRegenerate() + private void editRegenerateAll() { - this.textSectionsToLines.clear(); - this.textLines.clear(); - this.textLinesY.clear(); + this.textSectionsFormatted.clear(); + this.textSectionsFormattedByLine.clear(); + this.textSectionsFormattedByY.clear(); final var fontNow = this.font.get(); final var wrapNow = this.pageWidth.get().intValue(); - final var newMeasured = - fontNow.textLayoutMultiple(this.textSections, wrapNow); - var lineNumber = SyTextLineNumber.first(); var y = 0; - for (final var measured : newMeasured) { - this.textLines.put(lineNumber, measured); - this.textLinesY.set(lineNumber, y); - this.textSectionsToLines.lineAssociate( - measured.textOriginal(), - lineNumber); - - y += measured.height(); - lineNumber = lineNumber.next(); + + for (final var entry : this.textSections.entrySet()) { + final var textId = + entry.getKey(); + final var text = + entry.getValue(); + final var lines = + fontNow.textLayout(textId, text, wrapNow); + + final var textFormatted = + new SyTextFormatted( + textId, + lineNumber, + lines, + y + ); + + this.textSectionsFormatted.put(textId, textFormatted); + this.textSectionsFormattedByLine.put(lineNumber, textFormatted); + this.textSectionsFormattedByY.put(Integer.valueOf(y), textFormatted); + + for (final var line : lines) { + lineNumber = lineNumber.next(); + y += line.height(); + } } } @@ -226,27 +433,21 @@ public void setPageWidth( } @Override - public void textSectionsAppend( - final List sections) + public void textSectionReplace( + final SyTextID textID, + final SyText text) { - if (sections.isEmpty()) { - return; - } - - SyTextID nextID; - try { - nextID = this.textSections.lastKey().next(); - } catch (final NoSuchElementException e) { - nextID = SyTextID.first(); - } + Objects.requireNonNull(textID, "textID"); + Objects.requireNonNull(text, "text"); - final SyTextID regenerateFrom = nextID; - for (final var section : sections) { - this.textSections.put(nextID, section); - nextID = nextID.next(); - } + this.edit(new SyEditOpReplace(textID, text)); + } - this.edit(new SyEditOpAppend(regenerateFrom)); + @Override + public void textSectionsAppend( + final List sections) + { + this.edit(new SyEditOpAppend(sections)); } private List> buildRegions( @@ -301,9 +502,7 @@ private List> buildRegions( final var textDirection = line.textAsWrapped().direction(); final var y = - this.textLinesY.y(lineNumber) - .orElseThrow() - .intValue(); + this.textYForLine(lineNumber); final var textSizeX = line.textWidth(); @@ -470,6 +669,23 @@ yield sanitizedArea( return List.copyOf(results); } + private int textYForLine( + final SyTextLineNumber lineNumber) + { + final var entry = + this.textSectionsFormattedByLine.floorEntry(lineNumber); + final var formatted = + entry.getValue(); + final var lineOffset = + lineNumber.value() - formatted.lineNumber.value(); + + var y = formatted.yOffset; + for (int index = 0; index < lineOffset; ++index) { + y += formatted.lines.get(index).height(); + } + return y; + } + private List> buildRegionsForSelectionForward( final SyTextLocationType lowerInclusive, @@ -493,9 +709,7 @@ yield sanitizedArea( final var textDirection = line.textAsWrapped().direction(); final var y = - this.textLinesY.y(lineNumber) - .orElseThrow() - .intValue(); + this.textYForLine(lineNumber); final var textSizeX = line.textWidth(); final var textSizeY = @@ -664,7 +878,13 @@ yield sanitizedArea( private SyTextLineMeasuredType textForLine( final SyTextLineNumber lineNumber) { - return this.textLines.get(lineNumber); + final var entry = + this.textSectionsFormattedByLine.floorEntry(lineNumber); + final var formatted = + entry.getValue(); + final var lineOffset = + lineNumber.value() - formatted.lineNumber.value(); + return formatted.lines.get(lineOffset); } @Override @@ -756,39 +976,38 @@ public Optional selectionFinish( } @Override - public Iterable textLinesPositioned() + public int lineCount() { - final Iterator> baseIterator = - this.textLines.entrySet().iterator(); - - return () -> new MappedIterator(this, baseIterator); + return this.textSectionsFormatted.values() + .stream() + .mapToInt(f -> f.lines.size()) + .sum(); } @Override - public Optional textByYOffset( - final int y) + public Optional lineAt( + final SyTextLineNumber line) { - return this.textLinesY.line(y) - .flatMap(n -> Optional.ofNullable(this.textLines.get(n))); + final var floor = + this.textSectionsFormattedByLine.floorEntry(line); + + if (floor == null) { + return Optional.empty(); + } + + return floor.getValue().lineAt(line); } private int highestYOffset() { - final var highestYOpt = this.textLinesY.highestY(); - if (highestYOpt.isEmpty()) { + final var lastText = + this.textSectionsFormatted.lastEntry(); + if (lastText == null) { return 0; } - final var highestY = - highestYOpt.get().intValue(); - final var lineNumber = - this.textLinesY.line(highestY) - .orElseThrow(); - - final var line = - this.textLines.get(lineNumber); - - return highestY + line.height(); + final var formatted = lastText.getValue(); + return formatted.yOffset + formatted.height(); } @Override @@ -809,19 +1028,6 @@ public SortedMap textSections() return this.textSectionsReadable; } - @Override - public Optional textSectionContainingLine( - final SyTextLineNumber lineNumber) - { - Objects.requireNonNull(lineNumber, "lineNumber"); - - final var line = this.textLines.get(lineNumber); - if (line == null) { - return Optional.empty(); - } - return Optional.ofNullable(this.textSections.get(line.textOriginal())); - } - @Override public Optional inspectAt( final PVector2I position) @@ -830,36 +1036,45 @@ public Optional inspectAt( final var y = Math.max(0, position.y()); - final var lineNumberOpt = - this.textLinesY.lineContainingY(y); - if (lineNumberOpt.isEmpty()) { + final var entry = + this.textSectionsFormattedByY.floorEntry(Integer.valueOf(y)); + + if (entry == null) { return Optional.empty(); } - final var lineNumber = - lineNumberOpt.get(); - final var line = - Optional.ofNullable(this.textLines.get(lineNumber)) - .orElseThrow(); + final var formatted = entry.getValue(); + final var yLocal = y - formatted.yOffset; + var yLine = 0; + var lineNumber = formatted.lineNumber; + for (final var line : formatted.lines) { + final var yNext = yLine + line.height(); + if (yLocal >= yLine && yLocal < yNext) { + return Optional.of(line.inspectAtParentRelative(lineNumber, position)); + } + lineNumber = lineNumber.next(); + yLine += line.height(); + } - return Optional.of(line.inspectAtParentRelative(lineNumber, position)); + return Optional.empty(); } @Override public int minimumSizeYRequired() { - final var fontNow = - this.font.get(); + final var lastEntry = + this.textSectionsFormatted.lastEntry(); - var sum = fontNow.textHeight(); - for (final var value : this.textLines.values()) { - sum += value.height(); + if (lastEntry == null) { + return 0; } - return sum; + + final var formatted = lastEntry.getValue(); + return formatted.yOffset + formatted.height(); } - private enum SyEditOpRegenerate + private enum SyEditOpRegenerateAll implements SyEditOpType { SY_EDIT_OP_REGENERATE @@ -871,53 +1086,18 @@ private sealed interface SyEditOpType } private record SyEditOpAppend( - SyTextID textID) + List texts) implements SyEditOpType { } - private static final class MappedIterator - implements Iterator + private record SyEditOpReplace( + SyTextID textID, + SyText text) + implements SyEditOpType { - private final Iterator> baseIterator; - private final SyTextMultiLineModel model; - MappedIterator( - final SyTextMultiLineModel inModel, - final Iterator> inBaseIterator) - { - this.model = - Objects.requireNonNull(inModel, "inModel"); - this.baseIterator = - Objects.requireNonNull(inBaseIterator, "baseIterator"); - } - - @Override - public boolean hasNext() - { - return this.baseIterator.hasNext(); - } - - @Override - public SyTextLinePositioned next() - { - final var nextEntry = - this.baseIterator.next(); - - final var lineNumber = - nextEntry.getKey(); - - final var line = - this.model.textLines.get(lineNumber); - - final var y = - this.model.textLinesY - .y(lineNumber) - .orElseThrow(); - - return new SyTextLinePositioned(y.intValue(), lineNumber, line); - } } private record SySelectionState( diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextMultiLineView.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextMultiLineView.java index f85bd9ba..f0e51ca5 100644 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextMultiLineView.java +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/text/SyTextMultiLineView.java @@ -33,17 +33,12 @@ import com.io7m.jsycamore.api.mouse.SyMouseEventType; import com.io7m.jsycamore.api.screens.SyScreenType; import com.io7m.jsycamore.api.spaces.SySpaceParentRelativeType; -import com.io7m.jsycamore.api.text.SyText; -import com.io7m.jsycamore.api.text.SyTextLineMeasuredType; -import com.io7m.jsycamore.api.text.SyTextLinePositioned; import com.io7m.jsycamore.api.text.SyTextMultiLineModelType; import com.io7m.jsycamore.api.themes.SyThemeClassNameType; import com.io7m.jsycamore.components.standard.SyComponentAttributes; -import java.util.LinkedList; import java.util.List; import java.util.Objects; -import java.util.Optional; import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_CONSUMED; import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; @@ -57,16 +52,8 @@ public final class SyTextMultiLineView { private final AttributeType textSelectable; private SyTextMultiLineModelType textModel; - private List textSectionsDeferred; - /** - * A multi-line text view. - * - * @param screen The screen that owns the component - * @param themeClasses The extra theme classes, if any - */ - - public SyTextMultiLineView( + private SyTextMultiLineView( final SyScreenType screen, final List themeClasses) { @@ -77,19 +64,46 @@ public SyTextMultiLineView( this.textSelectable = components.create(Boolean.TRUE); - this.textSectionsDeferred = - new LinkedList<>(); + } + + /** + * Create a multi-line text view. + * + * @param screen The screen + * @param themeClasses The theme classes + * + * @return The text view + */ + + public static SyTextMultiLineViewType multiLineTextView( + final SyScreenType screen, + final List themeClasses) + { + final var textView = + new SyTextMultiLineView(screen, themeClasses); + + final var font = + screen.theme() + .findForComponent(textView) + .font(screen.themeContext(), textView); + + final var textModel = + SyTextMultiLineModel.create(font, 1024); + + textView.textModel = textModel; /* * A text view moving from selectable to not selectable will invalidate * its selection. */ - this.textSelectable.subscribe((oldValue, newValue) -> { + textView.textSelectable.subscribe((oldValue, newValue) -> { if (!Objects.equals(oldValue, newValue) && !newValue.booleanValue()) { - this.textSelectionInvalidateIfSelected(); + textView.textSelectionInvalidateIfSelected(); } }); + + return textView; } @Override @@ -126,16 +140,11 @@ private SyEventConsumed onMouseEvent( } case final SyMouseEventOnPressed e -> { - final var model = this.textModel; - if (model == null) { - yield EVENT_NOT_CONSUMED; - } - final var relative = this.relativePositionOf(e.mousePosition()); final var selection = - model.selectionStart(relative); + this.textModel.selectionStart(relative); if (selection.isEmpty()) { yield EVENT_NOT_CONSUMED; } @@ -145,16 +154,11 @@ private SyEventConsumed onMouseEvent( } case final SyMouseEventOnHeld e -> { - final var model = this.textModel; - if (model == null) { - yield EVENT_NOT_CONSUMED; - } - final var relative = this.relativePositionOf(e.mousePositionNow()); final var selection = - model.selectionContinue(relative); + this.textModel.selectionContinue(relative); if (selection.isEmpty()) { yield EVENT_NOT_CONSUMED; } @@ -164,16 +168,11 @@ private SyEventConsumed onMouseEvent( } case final SyMouseEventOnReleased e -> { - final var model = this.textModel; - if (model == null) { - yield EVENT_NOT_CONSUMED; - } - final var relative = this.relativePositionOf(e.mousePosition()); final var selection = - model.selectionFinish(relative); + this.textModel.selectionFinish(relative); if (selection.isEmpty()) { yield EVENT_NOT_CONSUMED; } @@ -202,51 +201,17 @@ public PAreaSizeI layout( this.textSelectionInvalidateIfSelected(); } - this.createModelIfRequired(layoutContext); this.textModel.setPageWidth(newSize.sizeX()); return newSize; } - private void createModelIfRequired( - final SyLayoutContextType layoutContext) - { - final var model = this.textModel; - if (model == null) { - final var themeComponent = - layoutContext.themeCurrent() - .findForComponent(this); - - final var font = - themeComponent.font(layoutContext, this); - final var sizeX = - this.size().get().sizeX(); - - this.textModel = SyTextMultiLineModel.create(font, sizeX); - this.textModel.textSectionsAppend(this.textSectionsDeferred); - this.textSectionsDeferred.clear(); - } - } - @Override public int minimumSizeYRequired( final SyLayoutContextType layoutContext) { - this.createModelIfRequired(layoutContext); return this.textModel.minimumSizeYRequired(); } - @Override - public Optional textByYOffset( - final int y) - { - final var model = this.textModel; - if (model == null) { - return Optional.empty(); - } else { - return model.textByYOffset(y); - } - } - @Override public AttributeType textSelectable() { @@ -254,27 +219,8 @@ public AttributeType textSelectable() } @Override - public Iterable textLinesPositioned() + public SyTextMultiLineModelType model() { - final var model = this.textModel; - if (model == null) { - return List.of(); - } else { - return model.textLinesPositioned(); - } - } - - @Override - public void textSectionsAppend( - final List sections) - { - this.textSelectionInvalidateIfSelected(); - - final var model = this.textModel; - if (model == null) { - this.textSectionsDeferred.addAll(sections); - } else { - model.textSectionsAppend(sections); - } + return this.textModel; } } diff --git a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextMultiLineModelTest.java b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextMultiLineModelTest.java new file mode 100644 index 00000000..abf8cbf0 --- /dev/null +++ b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextMultiLineModelTest.java @@ -0,0 +1,219 @@ +/* + * Copyright © 2022 Mark Raynsford https://www.io7m.com + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +package com.io7m.jsycamore.tests; + +import com.io7m.jsycamore.api.text.SyFontDescription; +import com.io7m.jsycamore.api.text.SyFontDirectoryServiceType; +import com.io7m.jsycamore.api.text.SyFontStyle; +import com.io7m.jsycamore.api.text.SyTextID; +import com.io7m.jsycamore.api.text.SyTextLineNumber; +import com.io7m.jsycamore.api.text.SyTextMultiLineModelType; +import com.io7m.jsycamore.awt.internal.SyAWTFont; +import com.io7m.jsycamore.awt.internal.SyAWTFontDirectoryService; +import com.io7m.jsycamore.components.standard.text.SyTextMultiLineModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.stream.StreamSupport; + +import static com.io7m.jsycamore.api.text.SyText.text; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class SyTextMultiLineModelTest +{ + private static final Logger LOG = + LoggerFactory.getLogger(SyTextMultiLineModelTest.class); + + private SyFontDirectoryServiceType fontsAWT; + private SyAWTFont font; + + @BeforeEach + public void setup() + throws Exception + { + this.fontsAWT = + SyAWTFontDirectoryService.createFromServiceLoader(); + this.font = + this.fontsAWT.get( + new SyFontDescription("DejaVu Sans", SyFontStyle.REGULAR, 11) + ); + } + + /** + * Inserting texts causes renumbering. + */ + + @Test + public void testInsertRenumbers0() + { + final var m = + SyTextMultiLineModel.create(this.font, 128); + + m.textSectionAppend(text("Hello line A.")); + m.textSectionAppend(text("Hello line B.")); + m.textSectionAppend(text("Hello line C.")); + + assertEquals(3, m.lineCount()); + assertEquals("Hello line A.", lineTextOf(m, 0)); + assertEquals("Hello line B.", lineTextOf(m, 1)); + assertEquals("Hello line C.", lineTextOf(m, 2)); + assertEquals(0, lineNumberOf(m, 0)); + assertEquals(1, lineNumberOf(m, 1)); + assertEquals(2, lineNumberOf(m, 2)); + assertEquals(3 * 14, m.minimumSizeYRequired()); + + /* + * The first line is removed, and then replaced with five lines, yielding + * a total of seven lines. + */ + + m.textSectionReplace( + SyTextID.first(), + text("Expressions moisturisers filtrate rumouring apportioned treachery.") + ); + + assertEquals(7, m.lineCount()); + assertEquals("Expressions ", lineTextOf(m, 0)); + assertEquals("moisturisers filtrate ", lineTextOf(m, 1)); + assertEquals("rumouring ", lineTextOf(m, 2)); + assertEquals("apportioned ", lineTextOf(m, 3)); + assertEquals("treachery.", lineTextOf(m, 4)); + assertEquals("Hello line B.", lineTextOf(m, 5)); + assertEquals("Hello line C.", lineTextOf(m, 6)); + + assertEquals(0, lineNumberOf(m, 0)); + assertEquals(1, lineNumberOf(m, 1)); + assertEquals(2, lineNumberOf(m, 2)); + assertEquals(3, lineNumberOf(m, 3)); + assertEquals(4, lineNumberOf(m, 4)); + assertEquals(5, lineNumberOf(m, 5)); + assertEquals(6, lineNumberOf(m, 6)); + assertEquals(7 * 14, m.minimumSizeYRequired()); + } + + /** + * Inserting texts causes renumbering. + */ + + @Test + public void testInsertRenumbers1() + { + final var m = + SyTextMultiLineModel.create(this.font, 128); + + m.textSectionAppend(text("Expressions moisturisers filtrate rumouring apportioned treachery.")); + m.textSectionAppend(text("Hello line B.")); + m.textSectionAppend(text("Hello line C.")); + + assertEquals(7, m.lineCount()); + assertEquals("Expressions ", lineTextOf(m, 0)); + assertEquals("moisturisers filtrate ", lineTextOf(m, 1)); + assertEquals("rumouring ", lineTextOf(m, 2)); + assertEquals("apportioned ", lineTextOf(m, 3)); + assertEquals("treachery.", lineTextOf(m, 4)); + assertEquals("Hello line B.", lineTextOf(m, 5)); + assertEquals("Hello line C.", lineTextOf(m, 6)); + + assertEquals(0, lineNumberOf(m, 0)); + assertEquals(1, lineNumberOf(m, 1)); + assertEquals(2, lineNumberOf(m, 2)); + assertEquals(3, lineNumberOf(m, 3)); + assertEquals(4, lineNumberOf(m, 4)); + assertEquals(5, lineNumberOf(m, 5)); + assertEquals(6, lineNumberOf(m, 6)); + assertEquals(7 * 14, m.minimumSizeYRequired()); + + /* + * The first five lines are removed, and then replaced with one lines, + * yielding a total of 3 lines. + */ + + m.textSectionReplace( + SyTextID.first(), + text("Hello line A.") + ); + + assertEquals(3, m.lineCount()); + assertEquals("Hello line A.", lineTextOf(m, 0)); + assertEquals("Hello line B.", lineTextOf(m, 1)); + assertEquals("Hello line C.", lineTextOf(m, 2)); + assertEquals(0, lineNumberOf(m, 0)); + assertEquals(1, lineNumberOf(m, 1)); + assertEquals(2, lineNumberOf(m, 2)); + assertEquals(3 * 14, m.minimumSizeYRequired()); + } + + /** + * Appending texts preserves numbering. + */ + + @Test + public void testAppendNumbers() + { + final var m = + SyTextMultiLineModel.create(this.font, 128); + + m.textSectionAppend(text("Hello line A.")); + m.textSectionAppend(text("Hello line B.")); + m.textSectionAppend(text("Hello line C.")); + + assertEquals("Hello line A.", lineTextOf(m, 0)); + assertEquals("Hello line B.", lineTextOf(m, 1)); + assertEquals("Hello line C.", lineTextOf(m, 2)); + assertEquals(0, lineNumberOf(m, 0)); + assertEquals(1, lineNumberOf(m, 1)); + assertEquals(2, lineNumberOf(m, 2)); + assertEquals(3 * 14, m.minimumSizeYRequired()); + + m.textSectionAppend(text("Hello line D.")); + + assertEquals(4, m.lineCount()); + assertEquals("Hello line A.", lineTextOf(m, 0)); + assertEquals("Hello line B.", lineTextOf(m, 1)); + assertEquals("Hello line C.", lineTextOf(m, 2)); + assertEquals("Hello line D.", lineTextOf(m, 3)); + assertEquals(0, lineNumberOf(m, 0)); + assertEquals(1, lineNumberOf(m, 1)); + assertEquals(2, lineNumberOf(m, 2)); + assertEquals(3, lineNumberOf(m, 3)); + assertEquals(4 * 14, m.minimumSizeYRequired()); + } + + private static String lineTextOf( + final SyTextMultiLineModelType m, + final int value) + { + return m.lineAt(new SyTextLineNumber(value)) + .orElseThrow() + .textLine() + .textAsWrapped() + .value(); + } + + private static int lineNumberOf( + final SyTextMultiLineModelType m, + final int value) + { + return m.lineAt(new SyTextLineNumber(value)) + .orElseThrow() + .textLineNumber() + .value(); + } +} diff --git a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextMultiLineViewTest.java b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextMultiLineViewTest.java index 3bad1b87..37c2e63b 100644 --- a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextMultiLineViewTest.java +++ b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextMultiLineViewTest.java @@ -17,10 +17,9 @@ package com.io7m.jsycamore.tests; +import com.io7m.jsycamore.api.components.SyTextMultiLineViewType; import com.io7m.jsycamore.api.keyboard.SyKeyCode; import com.io7m.jsycamore.api.keyboard.SyKeyEventPressed; -import com.io7m.jsycamore.api.menus.SyMenuClosed; -import com.io7m.jsycamore.api.menus.SyMenuType; import com.io7m.jsycamore.api.mouse.SyMouseButton; import com.io7m.jsycamore.api.mouse.SyMouseEventOnHeld; import com.io7m.jsycamore.api.mouse.SyMouseEventOnNoLongerOver; @@ -32,8 +31,6 @@ import com.io7m.jsycamore.api.text.SyText; import com.io7m.jsycamore.api.text.SyTextLineNumber; import com.io7m.jsycamore.api.text.SyTextSelectionServiceType; -import com.io7m.jsycamore.api.windows.SyWindowClosed; -import com.io7m.jsycamore.api.windows.SyWindowID; import com.io7m.jsycamore.awt.internal.SyAWTFont; import com.io7m.jsycamore.awt.internal.SyAWTFontDirectoryService; import com.io7m.jsycamore.awt.internal.SyAWTImageLoader; @@ -45,7 +42,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,10 +53,8 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.UUID; import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_CONSUMED; import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; @@ -72,7 +66,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public final class SyTextMultiLineViewTest - extends SyComponentContract + extends SyComponentContract { private static final Logger LOG = LoggerFactory.getLogger(SyTextMultiLineViewTest.class); @@ -158,6 +152,7 @@ public void testIgnoredEventsBeforeLayout() { final var c = this.newComponent(); c.setTextSelectable(true); + this.windowContentArea().childAdd(c); assertEquals( EVENT_NOT_CONSUMED, @@ -186,42 +181,6 @@ public void testIgnoredEventsBeforeLayout() ); } - /** - * Text is deferred until a layout occurs. - */ - - @Test - public void testTextDeferred() - { - final var c = this.newComponent(); - c.textSectionAppend(SyText.text("Hello!")); - - assertEquals( - Optional.empty(), - c.textByYOffset(0) - ); - - this.windowContentArea().childAdd(c); - this.window().layout(this.layoutContext); - - assertEquals( - SyText.text("Hello!"), - c.textByYOffset(0).orElseThrow().textAsWrapped() - ); - - c.textSectionAppend(SyText.text("Goodbye!")); - this.window().layout(this.layoutContext); - - assertEquals( - SyText.text("Hello!"), - c.textByYOffset(0).orElseThrow().textAsWrapped() - ); - assertEquals( - SyText.text("Goodbye!"), - c.textByYOffset(14).orElseThrow().textAsWrapped() - ); - } - /** * A text view that is not selectable, does not accept mouse events. */ @@ -268,8 +227,9 @@ public void testSelectionLTRForward0() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -350,8 +310,9 @@ public void testSelectionLTRForward1() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -436,8 +397,9 @@ public void testSelectionLTRForward2() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -526,8 +488,9 @@ public void testSelectionRTLForward0() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -608,8 +571,9 @@ public void testSelectionRTLForward1() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -695,8 +659,9 @@ public void testSelectionRTLForward2() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -787,8 +752,9 @@ public void testSelectionLTRBackward0() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -869,8 +835,9 @@ public void testSelectionLTRBackward1() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -955,8 +922,9 @@ public void testSelectionLTRBackward2() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1045,8 +1013,9 @@ public void testSelectionRTLBackward0() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1127,8 +1096,9 @@ public void testSelectionRTLBackward1() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1213,8 +1183,9 @@ public void testSelectionRTLBackward2() throws IOException { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1368,9 +1339,9 @@ public void testModelSelectionInspectAt() } @Override - protected SyTextMultiLineView newComponent() + protected SyTextMultiLineViewType newComponent() { - return new SyTextMultiLineView(this.screen(), List.of()); + return SyTextMultiLineView.multiLineTextView(this.screen(), List.of()); } private void saveImage() @@ -1443,8 +1414,9 @@ public void testSelectionRTLBackward0Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1493,8 +1465,9 @@ public void testSelectionRTLBackward1Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1532,8 +1505,9 @@ public void testSelectionRTLBackward2Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1571,8 +1545,9 @@ public void testSelectionRTLForward0Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1610,8 +1585,9 @@ public void testSelectionRTLForward1Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1649,8 +1625,9 @@ public void testSelectionRTLForward2Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("loremHebrew.txt")); + m.textSectionsAppend(textSections("loremHebrew.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1688,8 +1665,9 @@ public void testSelectionLTRBackward0Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1727,8 +1705,9 @@ public void testSelectionLTRBackward1Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1766,8 +1745,9 @@ public void testSelectionLTRBackward2Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1805,8 +1785,9 @@ public void testSelectionLTRForward0Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1844,8 +1825,9 @@ public void testSelectionLTRForward1Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); @@ -1883,8 +1865,9 @@ public void testSelectionLTRForward2Visual() throws Exception { final var c = this.newComponent(); + final var m = c.model(); - c.textSectionsAppend(textSections("lorem.txt")); + m.textSectionsAppend(textSections("lorem.txt")); c.setTextSelectable(true); this.windowContentArea().childAdd(c); this.window().layout(this.layoutContext); diff --git a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/demos/SyTextAreaDemo.java b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/demos/SyTextAreaDemo.java index 6f339cd0..62185ab9 100644 --- a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/demos/SyTextAreaDemo.java +++ b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/demos/SyTextAreaDemo.java @@ -23,6 +23,8 @@ import com.io7m.jsycamore.api.spaces.SySpaceViewportType; import com.io7m.jsycamore.api.text.SyFontDirectoryServiceType; import com.io7m.jsycamore.api.text.SyText; +import com.io7m.jsycamore.api.text.SyTextID; +import com.io7m.jsycamore.api.text.SyTextMultiLineModelType; import com.io7m.jsycamore.api.themes.SyThemeType; import com.io7m.jsycamore.api.windows.SyWindowServiceType; import com.io7m.jsycamore.api.windows.SyWindowType; @@ -54,6 +56,9 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.Executors; @@ -100,6 +105,8 @@ private static final class Canvas extends JPanel private final List sections; private final SyWindowServiceType windowService; private final SyMenuServiceType menuService; + private final SyTextArea textArea; + private final SyTextMultiLineModelType textModel; Canvas() throws Exception @@ -234,14 +241,16 @@ public void componentResized(final ComponentEvent e) final var margin = new SyLayoutMargin(this.screen); margin.setPaddingAll(8); - final var textArea = new SyTextArea(this.screen, List.of()); + this.textArea = new SyTextArea(this.screen, List.of()); + this.textModel = this.textArea.model(); + for (final var section : this.sections) { - textArea.textSectionAppend(SyText.text(section)); + this.textModel.textSectionAppend(SyText.text(section)); } - textArea.scrollbarVertical().setHideIfDisabled(HIDE_IF_DISABLED); - textArea.scrollbarHorizontal().setHideIfDisabled(HIDE_IF_DISABLED); + this.textArea.scrollbarVertical().setHideIfDisabled(HIDE_IF_DISABLED); + this.textArea.scrollbarHorizontal().setHideIfDisabled(HIDE_IF_DISABLED); - margin.childAdd(textArea); + margin.childAdd(this.textArea); this.window0.contentArea().childAdd(margin); } @@ -250,6 +259,18 @@ public void componentResized(final ComponentEvent e) if (!this.windowService.windowIsVisible(this.window0)) { this.windowService.windowShow(this.window0); } + + final var ids = + new ArrayList<>(this.textModel.textSections().keySet()); + Collections.shuffle(ids); + + final var textID = + ids.get(0); + final var text = + SyText.text(OffsetDateTime.now().toString()); + + LOG.debug("Replace {} -> {}", textID, text); + this.textModel.textSectionReplace(textID, text); }); }, 0L, 2L, SECONDS); diff --git a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/demos/SyTextMultilineViewDemo.java b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/demos/SyTextMultilineViewDemo.java index be114d8d..c3217c8f 100644 --- a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/demos/SyTextMultilineViewDemo.java +++ b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/demos/SyTextMultilineViewDemo.java @@ -17,6 +17,7 @@ package com.io7m.jsycamore.tests.demos; import com.io7m.jregions.core.parameterized.sizes.PAreaSizeI; +import com.io7m.jsycamore.api.components.SyTextMultiLineViewType; import com.io7m.jsycamore.api.menus.SyMenuServiceType; import com.io7m.jsycamore.api.mouse.SyMouseButton; import com.io7m.jsycamore.api.screens.SyScreenType; @@ -24,6 +25,7 @@ import com.io7m.jsycamore.api.text.SyFontDirectoryServiceType; import com.io7m.jsycamore.api.text.SyText; import com.io7m.jsycamore.api.text.SyTextDirection; +import com.io7m.jsycamore.api.text.SyTextMultiLineModelType; import com.io7m.jsycamore.api.themes.SyThemeType; import com.io7m.jsycamore.api.windows.SyWindowServiceType; import com.io7m.jsycamore.api.windows.SyWindowType; @@ -101,6 +103,8 @@ private static final class Canvas extends JPanel private final List sections; private final SyWindowServiceType windowService; private final SyMenuServiceType menuService; + private final SyTextMultiLineViewType textArea; + private final SyTextMultiLineModelType textModel; Canvas() throws Exception @@ -240,12 +244,16 @@ public void componentResized(final ComponentEvent e) final var margin = new SyLayoutMargin(this.screen); margin.setPaddingAll(8); - final var textArea = new SyTextMultiLineView(this.screen, List.of()); + this.textArea = + SyTextMultiLineView.multiLineTextView(this.screen, List.of()); + this.textModel = + this.textArea.model(); + for (final var section : this.sections) { - textArea.textSectionAppend(section); + this.textModel.textSectionAppend(section); } - margin.childAdd(textArea); + margin.childAdd(this.textArea); this.window0.contentArea().childAdd(margin); } diff --git a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalTextMultilineView.java b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalTextMultilineView.java index 2ea1f1d4..d9faa8e3 100644 --- a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalTextMultilineView.java +++ b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalTextMultilineView.java @@ -27,6 +27,7 @@ import com.io7m.jsycamore.api.rendering.SyRenderNodeType; import com.io7m.jsycamore.api.rendering.SyShapeRectangle; import com.io7m.jsycamore.api.spaces.SySpaceComponentRelativeType; +import com.io7m.jsycamore.api.text.SyTextLineNumber; import com.io7m.jsycamore.api.text.SyTextSelectionServiceType.SyTextSelectionIsSelected; import com.io7m.jsycamore.api.text.SyTextSelectionServiceType.SyTextSelectionNotSelected; import com.io7m.jsycamore.api.themes.SyThemeContextType; @@ -120,12 +121,18 @@ public SyRenderNodeType render( * Create a text node for each line of text. */ - for (final var linePositioned : textView.textLinesPositioned()) { + final var model = textView.model(); + + var lineNumber = SyTextLineNumber.first(); + for (var index = 0; index < model.lineCount(); ++index) { + final var linePositioned = + model.lineAt(lineNumber) + .orElseThrow(); final var line = linePositioned.textLine(); + final PVector2I position = PVector2I.of(0, linePositioned.y()); - final PAreaSizeI size = PAreaSizeI.of(textViewSize.sizeX(), line.height()); @@ -139,6 +146,8 @@ public SyRenderNodeType render( line.textAsWrapped() ) ); + + lineNumber = lineNumber.next(); } return new SyRenderNodeComposite("TextMultilineViewComposite", nodes); diff --git a/com.io7m.jsycamore.vanilla/src/main/java/com/io7m/jsycamore/vanilla/internal/SyScreen.java b/com.io7m.jsycamore.vanilla/src/main/java/com/io7m/jsycamore/vanilla/internal/SyScreen.java index 1a1f1ea7..bc6d33f8 100644 --- a/com.io7m.jsycamore.vanilla/src/main/java/com/io7m/jsycamore/vanilla/internal/SyScreen.java +++ b/com.io7m.jsycamore.vanilla/src/main/java/com/io7m/jsycamore/vanilla/internal/SyScreen.java @@ -46,6 +46,7 @@ import com.io7m.jsycamore.api.spaces.SySpaceViewportType; import com.io7m.jsycamore.api.text.SyFontDirectoryServiceType; import com.io7m.jsycamore.api.text.SyText; +import com.io7m.jsycamore.api.themes.SyThemeContextType; import com.io7m.jsycamore.api.themes.SyThemeType; import com.io7m.jsycamore.api.windows.SyWindowLayerID; import com.io7m.jsycamore.api.windows.SyWindowMaximized; @@ -81,7 +82,7 @@ * A screen. */ -public final class SyScreen implements SyScreenType +public final class SyScreen implements SyScreenType, SyThemeContextType { private final AtomicBoolean closed; private final AttributeType> viewportSize; @@ -179,6 +180,12 @@ public SyServiceDirectoryType services() return this.services; } + @Override + public SyThemeContextType themeContext() + { + return this; + } + @Override public SyThemeType theme() {