Skip to content

Commit

Permalink
Make text areas use scroll panes
Browse files Browse the repository at this point in the history
Affects: #14
Affects: #13
  • Loading branch information
io7m committed Nov 26, 2023
1 parent fc61b5d commit 9017bd0
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 123 deletions.
Expand Up @@ -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
*/
Expand Down
Expand Up @@ -41,4 +41,16 @@ default List<SyThemeClassNameType> themeClassesDefaultForComponent()
*/

AttributeReadableType<List<String>> textSections();

/**
* @return Access to the horizontal scrollbar
*/

SyScrollBarHorizontalReadableType scrollbarHorizontal();

/**
* @return Access to the vertical scrollbar
*/

SyScrollBarVerticalReadableType scrollbarVertical();
}
Expand Up @@ -30,4 +30,10 @@ public interface SyTextAreaType
*/

void textSectionAppend(String section);

@Override
SyScrollBarHorizontalType scrollbarHorizontal();

@Override
SyScrollBarVerticalType scrollbarVertical();
}
Expand Up @@ -133,14 +133,16 @@ public List<SyTextSectionLineType> 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);
Expand Down
Expand Up @@ -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.
Expand All @@ -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<List<String>> textSections;
private final SyPackVertical textContainer;
private final SyPackVertical textLayout;
private final SyScrollPaneType textScroller;
private final SyLayoutMargin textLayoutMargin;
private boolean viewsUpToDate;

/**
Expand All @@ -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(
Expand Down Expand Up @@ -98,82 +114,95 @@ public PAreaSizeI<SySpaceParentRelativeType> 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<SySpaceParentRelativeType> 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.<SySpaceParentRelativeType>of(
INTERNAL_TEXT_PADDING,
INTERNAL_TEXT_PADDING
);
final var viewportSize =
this.textScroller.contentViewport()
.size()
.get()
.sizeX();

final var internalArea =
PAreaSizeI.<SySpaceParentRelativeType>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<SyComponentReadableType> 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
Expand All @@ -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();
}
}
Expand Up @@ -75,14 +75,22 @@ public PAreaSizeI<SySpaceParentRelativeType> 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);

final var limitSize =
this.sizeUpperLimit().get();

final var newSize =
constraints.<SySpaceParentRelativeType>sizeNotExceeding(
PAreaSizeI.<SySpaceParentRelativeType>of(
Math.min(requiredSize.sizeX(), limitSize.sizeX()),
Math.min(requiredSize.sizeY(), limitSize.sizeY())
);
Expand Down
Expand Up @@ -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
Expand Down

0 comments on commit 9017bd0

Please sign in to comment.