From 9017bd023c5083a008eadc6b63b647c639ea208f Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Sun, 26 Nov 2023 17:40:23 +0000 Subject: [PATCH] Make text areas use scroll panes Affects: https://github.com/io7m/jsycamore/issues/14 Affects: https://github.com/io7m/jsycamore/issues/13 --- .../components/SyScrollPaneReadableType.java | 6 + .../components/SyTextAreaReadableType.java | 12 ++ .../api/components/SyTextAreaType.java | 6 + .../jsycamore/awt/internal/SyFontAWT.java | 10 +- .../components/standard/SyTextArea.java | 183 +++++++++++------- .../components/standard/SyTextView.java | 10 +- .../internal/scrollbars/SyScrollBarH.java | 31 +-- .../internal/scrollbars/SyScrollBarV.java | 31 +-- .../internal/scrollpanes/SyScrollPane.java | 7 + .../io7m/jsycamore/tests/SyScreenTest.java | 6 +- .../io7m/jsycamore/tests/SyTextAreaDemo.java | 4 +- ...SyPrimalScrollPaneContentAreaViewport.java | 18 +- 12 files changed, 201 insertions(+), 123 deletions(-) diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollPaneReadableType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollPaneReadableType.java index aead7698..f3ab2ee9 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollPaneReadableType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollPaneReadableType.java @@ -30,6 +30,12 @@ public interface SyScrollPaneReadableType SyComponentReadableType contentArea(); + /** + * @return A readable reference to the internal content viewport + */ + + SyComponentReadableType contentViewport(); + /** * @return A readable reference to the horizontal scroll bar */ 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 c3bec719..9eb1bb8c 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 @@ -41,4 +41,16 @@ default List themeClassesDefaultForComponent() */ AttributeReadableType> textSections(); + + /** + * @return Access to the horizontal scrollbar + */ + + SyScrollBarHorizontalReadableType scrollbarHorizontal(); + + /** + * @return Access to the vertical scrollbar + */ + + SyScrollBarVerticalReadableType scrollbarVertical(); } 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 be0d520e..0dc8b223 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 @@ -30,4 +30,10 @@ public interface SyTextAreaType */ void textSectionAppend(String section); + + @Override + SyScrollBarHorizontalType scrollbarHorizontal(); + + @Override + SyScrollBarVerticalType scrollbarVertical(); } diff --git a/com.io7m.jsycamore.awt/src/main/java/com/io7m/jsycamore/awt/internal/SyFontAWT.java b/com.io7m.jsycamore.awt/src/main/java/com/io7m/jsycamore/awt/internal/SyFontAWT.java index 4ad11a06..ad1c47ae 100644 --- a/com.io7m.jsycamore.awt/src/main/java/com/io7m/jsycamore/awt/internal/SyFontAWT.java +++ b/com.io7m.jsycamore.awt/src/main/java/com/io7m/jsycamore/awt/internal/SyFontAWT.java @@ -133,14 +133,16 @@ public List textLayout( final var indexNow = breaker.getPosition(); - final var bounds = - layout.getBounds(); + final var brokenText = + text.substring(indexThen, indexNow); + final var textWidth = + this.textWidth(brokenText); final var line = new SectionLine( - PAreaSizeI.of((int) Math.ceil(bounds.getWidth()), this.textHeight()), + PAreaSizeI.of(textWidth, this.textHeight()), Optional.of(layout), - text.substring(indexThen, indexNow) + brokenText ); results.add(line); diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyTextArea.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyTextArea.java index 1b057ff4..76df5304 100644 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyTextArea.java +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyTextArea.java @@ -19,21 +19,25 @@ import com.io7m.jattribute.core.AttributeReadableType; import com.io7m.jattribute.core.AttributeType; +import com.io7m.jorchard.core.JOTreeNodeReadableType; import com.io7m.jregions.core.parameterized.sizes.PAreaSizeI; +import com.io7m.jsycamore.api.components.SyComponentReadableType; import com.io7m.jsycamore.api.components.SyConstraints; +import com.io7m.jsycamore.api.components.SyScrollBarHorizontalType; +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.events.SyEventConsumed; import com.io7m.jsycamore.api.events.SyEventType; import com.io7m.jsycamore.api.layout.SyLayoutContextType; import com.io7m.jsycamore.api.spaces.SySpaceParentRelativeType; import com.io7m.jsycamore.api.themes.SyThemeClassNameType; -import com.io7m.jtensors.core.parameterized.vectors.PVector2I; import java.util.LinkedList; import java.util.List; +import java.util.stream.Stream; import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; -import static java.lang.Math.min; /** * A text area. @@ -42,12 +46,13 @@ public final class SyTextArea extends SyComponentAbstract implements SyTextAreaType { - private static final int INTERNAL_TEXT_PADDING = 2; - private static final int INTERNAL_TEXT_PADDING_DOUBLE = - INTERNAL_TEXT_PADDING + INTERNAL_TEXT_PADDING; + private static final int EDGE_PADDING = 8; + private static final int PADDING = EDGE_PADDING * 2; private final AttributeType> textSections; - private final SyPackVertical textContainer; + private final SyPackVertical textLayout; + private final SyScrollPaneType textScroller; + private final SyLayoutMargin textLayoutMargin; private boolean viewsUpToDate; /** @@ -66,11 +71,22 @@ public SyTextArea( this.textSections = attributes.create(List.of()); - this.textContainer = new SyPackVertical(inThemeClassesExtra); - this.textSections.subscribe(this::invalidateViews); - this.size().subscribe(this::invalidateViews); - - this.childAdd(this.textContainer); + this.textScroller = + SyScrollPanes.create(inThemeClassesExtra); + this.textLayoutMargin = + new SyLayoutMargin(); + this.textLayout = + new SyPackVertical(); + + this.textLayoutMargin.childAdd(this.textLayout); + this.textLayoutMargin.setPaddingAll(EDGE_PADDING); + this.textScroller.contentArea().childAdd(this.textLayoutMargin); + this.childAdd(this.textScroller); + + this.textSections + .subscribe(this::invalidateViews); + this.size() + .subscribe(this::invalidateViews); } private void invalidateViews( @@ -98,82 +114,95 @@ public PAreaSizeI layout( final SyLayoutContextType layoutContext, final SyConstraints constraints) { + final var newSize = + super.layout(layoutContext, constraints); + /* - * Set this text area to the maximum allowed size. + * If the internal text views aren't up-to-date (perhaps because the + * text changed), then remove and create new views. This will yield a + * set of views from which we can determine the total required height + * of the content. */ - final var sizeLimit = - this.sizeUpperLimit().get(); + if (!this.viewsUpToDate) { + this.regenerateViews(layoutContext); + } - final var limitedConstraints = - new SyConstraints( - constraints.sizeMinimumX(), - constraints.sizeMinimumY(), - min(constraints.sizeMaximumX(), sizeLimit.sizeX()), - min(constraints.sizeMaximumY(), sizeLimit.sizeY()) - ); + final var textHeightRequired = + this.textViews() + .mapToInt(c -> c.size().get().sizeY()) + .sum(); + + final var viewportSize = + this.textScroller.contentViewport() + .size() + .get() + .sizeX(); - final PAreaSizeI newSize = - limitedConstraints.sizeMaximum(); + this.textScroller.setContentAreaSize( + PAreaSizeI.of(viewportSize, textHeightRequired) + ); + + this.textScroller.layout(layoutContext, constraints); + return newSize; + } - this.setSize(newSize); + /** + * @return The width at which text should be wrapped + */ + private int textWrapWidth() + { /* - * The text container is the current text area size, plus a margin. + * Wrap all text such that it is no wider than the viewport size, minus + * the padding applied to the text area. */ - final var internalAreaPosition = - PVector2I.of( - INTERNAL_TEXT_PADDING, - INTERNAL_TEXT_PADDING - ); + final var viewportSize = + this.textScroller.contentViewport() + .size() + .get() + .sizeX(); - final var internalArea = - PAreaSizeI.of( - Math.max(0, newSize.sizeX() - INTERNAL_TEXT_PADDING_DOUBLE), - Math.max(0, newSize.sizeY() - INTERNAL_TEXT_PADDING_DOUBLE) - ); + return Math.max(0, viewportSize - PADDING); + } + + private Stream textViews() + { + return this.textLayout + .nodeReadable() + .childrenReadable() + .stream() + .map(JOTreeNodeReadableType::value); + } - this.textContainer.position() - .set(internalAreaPosition); - this.textContainer.size() - .set(internalArea); + private void regenerateViews( + final SyLayoutContextType layoutContext) + { + this.textLayout.childrenClear(); - /* - * If the internal text views aren't up-to-date (perhaps because the - * text changed), then remove and create new views, and then tell the - * text container to execute a layout. - */ + final var themeComponent = + layoutContext.themeCurrent() + .findForComponent(this); - if (!this.viewsUpToDate) { - this.textContainer.childrenClear(); - - final var themeComponent = - layoutContext.themeCurrent() - .findForComponent(this); - - final var font = - themeComponent.font(layoutContext, this); - - final var lines = - font.textLayoutMultiple( - this.textSections.get(), - internalArea.sizeX() - ); - - for (final var line : lines) { - final var textView = new SyTextView(); - textView.setSize(line.size()); - textView.setText(line.text()); - textView.setMouseQueryAccepting(false); - this.textContainer.childAdd(textView); - } - - this.textContainer.layout(layoutContext, limitedConstraints); - this.viewsUpToDate = true; + final var font = + themeComponent.font(layoutContext, this); + + final var lines = + font.textLayoutMultiple( + this.textSections.get(), + this.textWrapWidth() + ); + + for (final var line : lines) { + final var textView = new SyTextView(); + textView.setSize(line.size()); + textView.setText(line.text()); + textView.setMouseQueryAccepting(false); + this.textLayout.childAdd(textView); } - return newSize; + this.viewsUpToDate = true; } @Override @@ -184,4 +213,16 @@ public void textSectionAppend( appended.add(section); this.textSections.set(List.copyOf(appended)); } + + @Override + public SyScrollBarHorizontalType scrollbarHorizontal() + { + return this.textScroller.scrollBarHorizontal(); + } + + @Override + public SyScrollBarVerticalType scrollbarVertical() + { + return this.textScroller.scrollBarVertical(); + } } diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyTextView.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyTextView.java index 1ceefac7..7e707912 100644 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyTextView.java +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyTextView.java @@ -75,6 +75,14 @@ public PAreaSizeI layout( final SyLayoutContextType layoutContext, final SyConstraints constraints) { + /* + * Note that text views are a special case when it comes to layout + * sizes. Text views must be dynamically sized according to their + * actual text content, and so cannot realistically be sized at the + * whim of whatever component is passing in constraints. They can, as + * a last resort, be hard clipped by setting a maximum size limit. + */ + final var requiredSize = this.minimumSizeRequired(layoutContext); @@ -82,7 +90,7 @@ public PAreaSizeI layout( this.sizeUpperLimit().get(); final var newSize = - constraints.sizeNotExceeding( + PAreaSizeI.of( Math.min(requiredSize.sizeX(), limitSize.sizeX()), Math.min(requiredSize.sizeY(), limitSize.sizeY()) ); diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarH.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarH.java index ffd258b8..580d940d 100644 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarH.java +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarH.java @@ -282,21 +282,22 @@ public void setScrollAmountShown( { this.track.setScrollAmountShown(amount); - switch (this.presencePolicy.get()) { - case ALWAYS_ENABLED -> { - final var active = ACTIVE; - this.buttonLeft.setActive(active); - this.buttonRight.setActive(active); - this.track.setActive(active); - } - case DISABLED_IF_ENTIRE_RANGE_SHOWN -> { - final var active = - this.track.scrollAmountShown() >= 1.0 ? INACTIVE : ACTIVE; - this.buttonLeft.setActive(active); - this.buttonRight.setActive(active); - this.track.setActive(active); - } - } + final var all = + this.track.scrollAmountShown() >= 1.0; + + final var active = + switch (this.presencePolicy.get()) { + case ALWAYS_ENABLED -> { + yield ACTIVE; + } + case DISABLED_IF_ENTIRE_RANGE_SHOWN -> { + yield all ? INACTIVE : ACTIVE; + } + }; + + this.buttonLeft.setActive(active); + this.buttonRight.setActive(active); + this.track.setActive(active); } @Override diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarV.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarV.java index 0705d1ed..db350562 100644 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarV.java +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarV.java @@ -250,21 +250,22 @@ public void setScrollAmountShown( { this.track.setScrollAmountShown(amount); - switch (this.presencePolicy.get()) { - case ALWAYS_ENABLED -> { - final var active = ACTIVE; - this.buttonUp.setActive(active); - this.buttonDown.setActive(active); - this.track.setActive(active); - } - case DISABLED_IF_ENTIRE_RANGE_SHOWN -> { - final var active = - this.track.scrollAmountShown() >= 1.0 ? INACTIVE : ACTIVE; - this.buttonUp.setActive(active); - this.buttonDown.setActive(active); - this.track.setActive(active); - } - } + final var all = + this.track.scrollAmountShown() >= 1.0; + + final var active = + switch (this.presencePolicy.get()) { + case ALWAYS_ENABLED -> { + yield ACTIVE; + } + case DISABLED_IF_ENTIRE_RANGE_SHOWN -> { + yield all ? INACTIVE : ACTIVE; + } + }; + + this.buttonUp.setActive(active); + this.buttonDown.setActive(active); + this.track.setActive(active); } @Override diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollpanes/SyScrollPane.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollpanes/SyScrollPane.java index fd574977..9b8ae849 100644 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollpanes/SyScrollPane.java +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollpanes/SyScrollPane.java @@ -19,6 +19,7 @@ import com.io7m.jattribute.core.AttributeType; import com.io7m.jregions.core.parameterized.sizes.PAreaSizeI; +import com.io7m.jsycamore.api.components.SyComponentReadableType; import com.io7m.jsycamore.api.components.SyComponentType; import com.io7m.jsycamore.api.components.SyConstraints; import com.io7m.jsycamore.api.components.SyScrollBarHorizontalType; @@ -263,6 +264,12 @@ public SyComponentType contentArea() return this.contentArea; } + @Override + public SyComponentReadableType contentViewport() + { + return this.contentAreaViewport; + } + @Override public void setContentAreaSize( final PAreaSizeI size) diff --git a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScreenTest.java b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScreenTest.java index 69f9237b..c3072499 100644 --- a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScreenTest.java +++ b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScreenTest.java @@ -1251,7 +1251,7 @@ public void testMouseEventsOutsideDown( final var position = PVector2I.of(x, y); final var insideWindow = - x > windowX && x < windowXMax && y > windowY && y < windowYMax; + x >= windowX && x < windowXMax && y >= windowY && y < windowYMax; final var result = this.screen.mouseDown(position, MOUSE_BUTTON_LEFT); @@ -1298,8 +1298,10 @@ public void testMouseEventsOutsideUp( final var position = PVector2I.of(x, y); + final var insideWindow = + x >= windowX && x < windowXMax && y >= windowY && y < windowYMax; final var outsideWindow = - x < windowX || x > windowXMax || y < windowY || y > windowYMax; + !insideWindow; this.screen.mouseDown(position, MOUSE_BUTTON_LEFT); diff --git a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextAreaDemo.java b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextAreaDemo.java index 856b8f5c..89011d04 100644 --- a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextAreaDemo.java +++ b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyTextAreaDemo.java @@ -27,7 +27,6 @@ import com.io7m.jsycamore.awt.internal.SyAWTRenderer; import com.io7m.jsycamore.awt.internal.SyFontAWT; import com.io7m.jsycamore.awt.internal.SyFontDirectoryAWT; -import com.io7m.jsycamore.awt.internal.SyRendererType; import com.io7m.jsycamore.components.standard.SyLayoutMargin; import com.io7m.jsycamore.components.standard.SyTextArea; import com.io7m.jsycamore.theme.primal.SyThemePrimalFactory; @@ -88,7 +87,7 @@ public static void main(final String[] args) private static final class Canvas extends JPanel { private final SyFontDirectoryType fontDirectory; - private final SyRendererType renderer; + private final SyAWTRenderer renderer; private final SyScreenType screen; private final SyThemeType theme; private final SyWindowType window0; @@ -268,6 +267,7 @@ public void paint(final Graphics g) for (int index = windows.size() - 1; index >= 0; --index) { final var window = windows.get(index); window.layout(layoutContext); + // this.renderer.nodeRenderer().setDebugBoundsRendering(true); this.renderer.render(g2, this.screen, window); } } diff --git a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollPaneContentAreaViewport.java b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollPaneContentAreaViewport.java index a47070ae..e6cc4eaf 100644 --- a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollPaneContentAreaViewport.java +++ b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollPaneContentAreaViewport.java @@ -23,13 +23,10 @@ import com.io7m.jsycamore.api.rendering.SyShapeRectangle; import com.io7m.jsycamore.api.spaces.SySpaceComponentRelativeType; import com.io7m.jsycamore.api.themes.SyThemeContextType; -import com.io7m.jsycamore.api.themes.SyThemeValueException; import java.util.Objects; import java.util.Optional; -import static com.io7m.jsycamore.theme.primal.internal.SyPrimalValues.PRIMARY_OVER; - /** * A theme component for scroll pane content area viewports. */ @@ -64,15 +61,10 @@ public SyRenderNodeType render( PAreaI.of(0, area.sizeX(), 0, area.sizeY()) ); - try { - final var values = this.theme().values(); - return new SyRenderNodeShape( - Optional.of(values.edgeFlat(PRIMARY_OVER)), - Optional.empty(), - rectangle - ); - } catch (final SyThemeValueException e) { - throw new IllegalStateException(e); - } + return new SyRenderNodeShape( + Optional.empty(), + Optional.empty(), + rectangle + ); } }