From 86dd089a5f9b42085d693a5a95e5cff269c519cb Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Sat, 25 Nov 2023 20:32:05 +0000 Subject: [PATCH] Implement vertical scrollbars Refactor snapping. Affects: https://github.com/io7m/jsycamore/issues/14 Affects: https://github.com/io7m/jsycamore/issues/13 --- .../components/SyScrollBarHorizontalType.java | 18 - .../api/components/SyScrollBarType.java | 18 + .../SyScrollBarVerticalReadableType.java | 49 +++ .../components/SyScrollBarVerticalType.java | 57 +++ .../io7m/jsycamore/api/layout/SySnapping.java | 108 +++++ .../api/themes/SyThemeClassNameStandard.java | 51 +++ .../standard/SyScrollBarsVertical.java | 61 +++ .../scrollbars/SyScrollBarHTrack.java | 31 +- .../internal/scrollbars/SyScrollBarV.java | 337 ++++++++++++++++ .../scrollbars/SyScrollBarVButtonDown.java | 64 +++ .../scrollbars/SyScrollBarVButtonThumb.java | 236 +++++++++++ .../scrollbars/SyScrollBarVButtonUp.java | 64 +++ .../scrollbars/SyScrollBarVTrack.java | 162 ++++++++ .../io7m/jsycamore/tests/SyScreenTest.java | 2 +- .../tests/SyScrollBarVerticalTest.java | 377 ++++++++++++++++++ .../tests/SyScrollbarVerticalDemo.java | 278 +++++++++++++ .../primal/internal/SyPrimalImageView.java | 11 +- .../primal/internal/SyPrimalScrollbarV.java | 54 +++ .../internal/SyPrimalScrollbarVButton.java | 70 ++++ .../SyPrimalScrollbarVButtonIcon.java | 70 ++++ .../internal/SyPrimalScrollbarVTrack.java | 91 +++++ .../theme/primal/internal/SyThemePrimal.java | 25 ++ .../jsycamore/theme/primal/scroll_v_thumb.png | Bin 0 -> 87 bytes .../jsycamore/theme/primal/scroll_v_thumb.xcf | Bin 0 -> 727 bytes .../jsycamore/vanilla/internal/SyWindow.java | 50 +-- 25 files changed, 2202 insertions(+), 82 deletions(-) create mode 100644 com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarVerticalReadableType.java create mode 100644 com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarVerticalType.java create mode 100644 com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/layout/SySnapping.java create mode 100644 com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyScrollBarsVertical.java create mode 100644 com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarV.java create mode 100644 com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonDown.java create mode 100644 com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonThumb.java create mode 100644 com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonUp.java create mode 100644 com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVTrack.java create mode 100644 com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScrollBarVerticalTest.java create mode 100644 com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScrollbarVerticalDemo.java create mode 100644 com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarV.java create mode 100644 com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVButton.java create mode 100644 com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVButtonIcon.java create mode 100644 com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVTrack.java create mode 100644 com.io7m.jsycamore.theme.primal/src/main/resources/com/io7m/jsycamore/theme/primal/scroll_v_thumb.png create mode 100644 com.io7m.jsycamore.theme.primal/src/main/resources/com/io7m/jsycamore/theme/primal/scroll_v_thumb.xcf diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarHorizontalType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarHorizontalType.java index b490a72c..bc139f66 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarHorizontalType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarHorizontalType.java @@ -16,8 +16,6 @@ package com.io7m.jsycamore.api.components; -import java.util.function.Consumer; - /** * Write access to scrollbars. */ @@ -56,20 +54,4 @@ public interface SyScrollBarHorizontalType */ void removeOnClickRightListener(); - - /** - * Set a listener that will be executed when the left scroll button is clicked. - * - * @param listener The listener - */ - - void setOnThumbDragListener(Consumer listener); - - /** - * Remove any listeners that are executed when the left button is clicked. - * - * @see #setOnThumbDragListener(Consumer) - */ - - void removeOnThumbDragListener(); } diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarType.java index 957cefa3..46cf8315 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarType.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarType.java @@ -18,6 +18,8 @@ import com.io7m.jattribute.core.AttributeType; +import java.util.function.Consumer; + /** * Write access to scrollbars. */ @@ -59,4 +61,20 @@ public interface SyScrollBarType */ void setScrollAmountShown(double amount); + + /** + * Set a listener that will be executed when the thumb is dragged. + * + * @param listener The listener + */ + + void setOnThumbDragListener(Consumer listener); + + /** + * Remove any listeners that are executed when the thumb is dragged. + * + * @see #setOnThumbDragListener(Consumer) + */ + + void removeOnThumbDragListener(); } diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarVerticalReadableType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarVerticalReadableType.java new file mode 100644 index 00000000..17e6c67f --- /dev/null +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarVerticalReadableType.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2021 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.api.components; + +import com.io7m.jsycamore.api.themes.SyThemeClassNameType; + +import java.util.List; + +import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.SCROLLBAR_VERTICAL; + +/** + * Read-only access to scrollbars. + */ + +public interface SyScrollBarVerticalReadableType + extends SyScrollBarReadableType +{ + @Override + default List themeClassesDefaultForComponent() + { + return List.of(SCROLLBAR_VERTICAL); + } + + /** + * @return The scrollbar up button + */ + + SyButtonReadableType buttonUp(); + + /** + * @return The scrollbar down button + */ + + SyButtonReadableType buttonDown(); +} diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarVerticalType.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarVerticalType.java new file mode 100644 index 00000000..928d29b3 --- /dev/null +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/components/SyScrollBarVerticalType.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2021 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.api.components; + +/** + * Write access to scrollbars. + */ + +public interface SyScrollBarVerticalType + extends SyScrollBarVerticalReadableType, SyScrollBarType +{ + /** + * Set a listener that will be executed when the up scroll button is clicked. + * + * @param runnable The listener + */ + + void setOnClickUpListener(Runnable runnable); + + /** + * Remove any listeners that are executed when the up button is clicked. + * + * @see #setOnClickUpListener(Runnable) + */ + + void removeOnClickUpListener(); + + /** + * Set a listener that will be executed when the down scroll button is clicked. + * + * @param runnable The listener + */ + + void setOnClickDownListener(Runnable runnable); + + /** + * Remove any listeners that are executed when the down button is clicked. + * + * @see #setOnClickDownListener(Runnable) + */ + + void removeOnClickDownListener(); +} diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/layout/SySnapping.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/layout/SySnapping.java new file mode 100644 index 00000000..4b3fc6bd --- /dev/null +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/layout/SySnapping.java @@ -0,0 +1,108 @@ +/* + * 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.api.layout; + +import com.io7m.jregions.core.parameterized.sizes.PAreaSizeI; +import com.io7m.jsycamore.api.spaces.SySpaceType; +import com.io7m.jtensors.core.parameterized.vectors.PVector2I; + +import static java.lang.StrictMath.round; + +/** + * Snapping functions. + */ + +public final class SySnapping +{ + private SySnapping() + { + + } + + /** + * Snap a real number {@code x} to a snapping value. The value of {@code f} + * is a fractional value in the range {@code [0, 1]}. + * + * @param x The number to be snapped + * @param f The snapping value + * + * @return The snapped value + */ + + public static double snapDouble( + final double x, + final double f) + { + if (f == 0.0) { + return x; + } + + final var p = 1.0 / f; + var r = x * p; + r = (double) round(r); + return r / p; + } + + /** + * Snap the given vector. Each element of the vector will be snapped to the + * nearest multiple of {@code snapping}. + * + * @param v The vector + * @param snapping The snapping value + * @param The vector space + * + * @return The vector + */ + + public static PVector2I snapVector( + final PVector2I v, + final int snapping) + { + if (snapping > 1) { + return PVector2I.of( + (v.x() / snapping) * snapping, + (v.y() / snapping) * snapping + ); + } + return v; + } + + /** + * Snap the given size. Each element of the size will be snapped to the + * nearest multiple of {@code snapping}. + * + * @param v The vector + * @param snapping The snapping value + * @param The vector space + * + * @return The vector + */ + + public static PAreaSizeI snapSize( + final PAreaSizeI v, + final int snapping) + { + if (snapping > 1) { + return PAreaSizeI.of( + (v.sizeX() / snapping) * snapping, + (v.sizeY() / snapping) * snapping + ); + } + return v; + } +} diff --git a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/themes/SyThemeClassNameStandard.java b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/themes/SyThemeClassNameStandard.java index a35cbcad..dd0d8f93 100644 --- a/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/themes/SyThemeClassNameStandard.java +++ b/com.io7m.jsycamore.api/src/main/java/com/io7m/jsycamore/api/themes/SyThemeClassNameStandard.java @@ -169,6 +169,57 @@ public enum SyThemeClassNameStandard SCROLLBAR_HORIZONTAL_BUTTON_THUMB_ICON("ScrollbarHorizontalButtonThumbIcon"), + + + + /** + * A horizontal scrollbar class. + */ + + SCROLLBAR_VERTICAL("ScrollbarVertical"), + + /** + * A horizontal scrollbar track class. + */ + + SCROLLBAR_VERTICAL_TRACK("ScrollbarVerticalTrack"), + + /** + * A scrollbar button class. + */ + + SCROLLBAR_VERTICAL_BUTTON_UP("ScrollbarVerticalButtonUp"), + + /** + * A scrollbar button icon class. + */ + + SCROLLBAR_VERTICAL_BUTTON_UP_ICON("ScrollbarVerticalButtonUpIcon"), + + /** + * A scrollbar button class. + */ + + SCROLLBAR_VERTICAL_BUTTON_DOWN("ScrollbarVerticalButtonDown"), + + /** + * A scrollbar button icon class. + */ + + SCROLLBAR_VERTICAL_BUTTON_DOWN_ICON("ScrollbarVerticalButtonDownIcon"), + + /** + * A scrollbar button class. + */ + + SCROLLBAR_VERTICAL_BUTTON_THUMB("ScrollbarVerticalButtonThumb"), + + /** + * A scrollbar button icon class. + */ + + SCROLLBAR_VERTICAL_BUTTON_THUMB_ICON("ScrollbarVerticalButtonThumbIcon"), + /** * A text area class. */ diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyScrollBarsVertical.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyScrollBarsVertical.java new file mode 100644 index 00000000..f1ac82d6 --- /dev/null +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/SyScrollBarsVertical.java @@ -0,0 +1,61 @@ +/* + * 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; + +import com.io7m.jsycamore.api.components.SyScrollBarVerticalType; +import com.io7m.jsycamore.api.themes.SyThemeClassNameType; +import com.io7m.jsycamore.components.standard.internal.scrollbars.SyScrollBarV; + +import java.util.List; + +/** + * Functions to create vertical scroll bars. + */ + +public final class SyScrollBarsVertical +{ + private SyScrollBarsVertical() + { + + } + + /** + * Create a vertical scrollbar. + * + * @param themeClassesExtra The extra theme classes + * + * @return A scrollbar + */ + + public static SyScrollBarVerticalType create( + final List themeClassesExtra) + { + return new SyScrollBarV(themeClassesExtra); + } + + /** + * Create a vertical scrollbar. + * + * @return A scrollbar + */ + + public static SyScrollBarVerticalType create() + { + return create(List.of()); + } +} diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarHTrack.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarHTrack.java index d6def16e..5eb39001 100644 --- a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarHTrack.java +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarHTrack.java @@ -34,8 +34,8 @@ import java.util.function.Consumer; import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; +import static com.io7m.jsycamore.api.layout.SySnapping.snapDouble; import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.SCROLLBAR_HORIZONTAL_TRACK; -import static java.lang.StrictMath.round; final class SyScrollBarHTrack extends SyComponentAbstract { @@ -52,30 +52,6 @@ final class SyScrollBarHTrack extends SyComponentAbstract this.childAdd(this.thumb); } - /** - * Snap a real number {@code x} to a snapping value. The value of {@code c} - * is a fractional value in the range {@code [0, 1]}. - * - * @param x The number to be snapped - * @param c The snapping value - * - * @return The snapped value - */ - - private static double snap( - final double x, - final double c) - { - if (c == 0.0) { - return x; - } - - final var p = 1.0 / c; - var r = x * p; - r = (double) round(r); - return r / p; - } - @Override public List themeClassesDefaultForComponent() { @@ -92,7 +68,8 @@ public PAreaSizeI layout( /* * The minimum size on the Y axis that would yield a square is the value - * of the X axis. + * of the X axis. The smallest the thumb can be is a square. The largest + * it can be is "fill the entire track". */ final var thumbWidthMinimum = @@ -147,7 +124,7 @@ void setScrollPosition( final var clampedPosition = Math.clamp(position, 0.0, 1.0); this.scrollPosition = - snap(clampedPosition, this.scrollPositionSnap); + snapDouble(clampedPosition, this.scrollPositionSnap); } void setScrollAmountShown( 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 new file mode 100644 index 00000000..adb279e2 --- /dev/null +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarV.java @@ -0,0 +1,337 @@ +/* + * 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.internal.scrollbars; + +import com.io7m.jattribute.core.AttributeType; +import com.io7m.jregions.core.parameterized.areas.PAreasI; +import com.io7m.jregions.core.parameterized.sizes.PAreaSizeI; +import com.io7m.jsycamore.api.components.SyButtonReadableType; +import com.io7m.jsycamore.api.components.SyComponentReadableType; +import com.io7m.jsycamore.api.components.SyConstraints; +import com.io7m.jsycamore.api.components.SyScrollBarDrag; +import com.io7m.jsycamore.api.components.SyScrollBarPresencePolicy; +import com.io7m.jsycamore.api.components.SyScrollBarVerticalType; +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.jsycamore.components.standard.SyComponentAbstract; +import com.io7m.jsycamore.components.standard.SyComponentAttributes; +import com.io7m.jtensors.core.parameterized.vectors.PVector2I; + +import java.util.List; +import java.util.function.Consumer; + +import static com.io7m.jsycamore.api.active.SyActive.ACTIVE; +import static com.io7m.jsycamore.api.active.SyActive.INACTIVE; +import static com.io7m.jsycamore.api.components.SyScrollBarPresencePolicy.DISABLED_IF_ENTIRE_RANGE_SHOWN; +import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; + +/** + * A vertical scrollbar. + */ + +public final class SyScrollBarV + extends SyComponentAbstract + implements SyScrollBarVerticalType +{ + private static final PAreaSizeI BUTTON_SIZE_DEFAULT = + PAreaSizeI.of(16, 16); + + private final SyScrollBarVButtonUp buttonUp; + private final SyScrollBarVButtonDown buttonDown; + private final SyScrollBarVTrack track; + private final AttributeType presencePolicy; + + /** + * A vertical scrollbar. + * + * @param inThemeClassesExtra The extra theme classes, if any + */ + + public SyScrollBarV( + final List inThemeClassesExtra) + { + super(inThemeClassesExtra); + + this.presencePolicy = + SyComponentAttributes.get().create(DISABLED_IF_ENTIRE_RANGE_SHOWN); + + this.buttonUp = + new SyScrollBarVButtonUp(); + this.buttonDown = + new SyScrollBarVButtonDown(); + this.track = + new SyScrollBarVTrack(); + + this.childAdd(this.buttonUp); + this.childAdd(this.track); + this.childAdd(this.buttonDown); + } + + @Override + public PAreaSizeI layout( + final SyLayoutContextType layoutContext, + final SyConstraints constraints) + { + var limitedConstraints = + layoutContext.deriveThemeConstraints(constraints, this); + + /* + * Then, limit the derived constraints further by the component's size + * limit. + */ + + final var sizeLimit = + this.sizeUpperLimit().get(); + + limitedConstraints = limitedConstraints.deriveLimitedBy(sizeLimit); + + /* + * Set the desired sizes of the various buttons based on the passed + * in constraints, and then execute a layout on each of the buttons. + */ + + final var buttonThemeSizeUp = + layoutContext.themeCurrent() + .findForComponent(this.buttonUp) + .size(layoutContext, this.buttonUp) + .orElse(BUTTON_SIZE_DEFAULT); + + final var buttonSizeUpConstraints = + limitedConstraints.deriveLimitedBy(buttonThemeSizeUp); + + final var buttonThemeSizeDown = + layoutContext.themeCurrent() + .findForComponent(this.buttonDown) + .size(layoutContext, this.buttonDown) + .orElse(BUTTON_SIZE_DEFAULT); + + final var buttonSizeDownConstraints = + limitedConstraints.deriveLimitedBy(buttonThemeSizeDown); + + this.buttonUp.layout( + layoutContext, buttonSizeUpConstraints); + this.buttonDown.layout( + layoutContext, buttonSizeDownConstraints); + + /* + * Now that the buttons have their correct sizes, set the size of the + * entire scrollbar based on the button size and the passed in constraints. + */ + + final PAreaSizeI overallSize = + limitedConstraints.sizeWithin( + buttonThemeSizeUp.sizeX(), + constraints.sizeMaximumY() + ); + + this.setSize(overallSize); + + /* + * Now, based on all the calculated sizes so far, set the button + * positions. + */ + + final var buttonUpSize = + this.buttonUp.size().get(); + final var buttonDownSize = + this.buttonDown.size().get(); + + final var buttonUpAreaStart = + PAreasI.create( + 0, + 0, + buttonUpSize.sizeX(), + buttonUpSize.sizeY()); + final var buttonDownAreaStart = + PAreasI.create( + 0, + 0, + buttonDownSize.sizeX(), + buttonDownSize.sizeY()); + final var thisArea = + PAreasI.create( + 0, + 0, + overallSize.sizeX(), + overallSize.sizeY()); + final var buttonUpArea = + PAreasI.alignOnYMinY(thisArea, buttonUpAreaStart); + final var buttonDownArea = + PAreasI.alignOnYMaxY(thisArea, buttonDownAreaStart); + + this.buttonUp.setPosition( + PVector2I.of(buttonUpArea.minimumX(), buttonUpArea.minimumY()) + ); + this.buttonDown.setPosition( + PVector2I.of(buttonDownArea.minimumX(), buttonDownArea.minimumY()) + ); + + /* + * Now fit the track between the two buttons. + */ + + final var trackArea = + PAreasI.create( + 0, + 0, + overallSize.sizeX(), + overallSize.sizeY()); + final var trackAreaFitted = + PAreasI.fitBetweenOnY(trackArea, buttonUpArea, buttonDownArea); + + final var trackConstraints = + limitedConstraints.deriveLimitedBy( + PAreaSizeI.of(trackAreaFitted.sizeX(), trackAreaFitted.sizeY()) + ); + + this.track.layout(layoutContext, trackConstraints); + this.track.setPosition( + PVector2I.of(trackAreaFitted.minimumX(), trackAreaFitted.minimumY()) + ); + + return overallSize; + } + + @Override + protected SyEventConsumed onEvent( + final SyEventType event) + { + return EVENT_NOT_CONSUMED; + } + + @Override + public void setOnThumbDragListener( + final Consumer listener) + { + this.track.setOnThumbDragListener(listener); + } + + @Override + public void removeOnThumbDragListener() + { + this.track.removeOnThumbDragListener(); + } + + @Override + public void setScrollPosition( + final double position) + { + this.track.setScrollPosition(position); + } + + @Override + public void setScrollPositionSnapping( + final double fraction) + { + this.track.setScrollPositionSnap(fraction); + } + + @Override + public void setScrollAmountShown( + final double amount) + { + 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); + } + } + } + + @Override + public SyButtonReadableType buttonUp() + { + return this.buttonUp; + } + + @Override + public SyButtonReadableType buttonDown() + { + return this.buttonDown; + } + + @Override + public SyComponentReadableType thumb() + { + return this.track.thumb(); + } + + @Override + public SyComponentReadableType track() + { + return this.track; + } + + @Override + public double scrollPosition() + { + return this.track.scrollPosition(); + } + + @Override + public double scrollPositionSnapping() + { + return this.track.scrollPositionSnapping(); + } + + @Override + public AttributeType presencePolicy() + { + return this.presencePolicy; + } + + @Override + public void setOnClickUpListener( + final Runnable runnable) + { + this.buttonUp.setOnClickListener(runnable); + } + + @Override + public void removeOnClickUpListener() + { + this.buttonUp.removeOnClickListener(); + } + + @Override + public void setOnClickDownListener( + final Runnable runnable) + { + this.buttonDown.setOnClickListener(runnable); + } + + @Override + public void removeOnClickDownListener() + { + this.buttonDown.removeOnClickListener(); + } +} diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonDown.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonDown.java new file mode 100644 index 00000000..f227e1e5 --- /dev/null +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonDown.java @@ -0,0 +1,64 @@ +/* + * 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.internal.scrollbars; + +import com.io7m.jsycamore.api.events.SyEventConsumed; +import com.io7m.jsycamore.api.events.SyEventType; +import com.io7m.jsycamore.components.standard.SyAlign; +import com.io7m.jsycamore.components.standard.SyButtonAbstract; +import com.io7m.jsycamore.components.standard.SyImageView; + +import java.util.List; + +import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; +import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.SCROLLBAR_VERTICAL_BUTTON_DOWN; +import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.SCROLLBAR_VERTICAL_BUTTON_DOWN_ICON; +import static com.io7m.jsycamore.components.standard.SyAlignmentHorizontal.ALIGN_HORIZONTAL_CENTER; +import static com.io7m.jsycamore.components.standard.SyAlignmentVertical.ALIGN_VERTICAL_CENTER; + +final class SyScrollBarVButtonDown extends SyButtonAbstract +{ + private final SyImageView image; + private final SyAlign align; + + SyScrollBarVButtonDown() + { + super(List.of(SCROLLBAR_VERTICAL_BUTTON_DOWN)); + + this.image = new SyImageView(List.of(SCROLLBAR_VERTICAL_BUTTON_DOWN_ICON)); + this.image.setImageURI("jsycamore:icon:scroll_down"); + + this.align = new SyAlign(); + this.align.alignmentHorizontal().set(ALIGN_HORIZONTAL_CENTER); + this.align.alignmentVertical().set(ALIGN_VERTICAL_CENTER); + this.align.childAdd(this.image); + this.childAdd(this.align); + } + + @Override + protected SyEventConsumed onOtherEvent( + final SyEventType event) + { + return EVENT_NOT_CONSUMED; + } + + @Override + protected void onClicked() + { + + } +} diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonThumb.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonThumb.java new file mode 100644 index 00000000..eb03111a --- /dev/null +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonThumb.java @@ -0,0 +1,236 @@ +/* + * 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.internal.scrollbars; + +import com.io7m.jsycamore.api.components.SyButtonReadableType; +import com.io7m.jsycamore.api.components.SyScrollBarDrag; +import com.io7m.jsycamore.api.events.SyEventConsumed; +import com.io7m.jsycamore.api.events.SyEventType; +import com.io7m.jsycamore.api.mouse.SyMouseEventOnHeld; +import com.io7m.jsycamore.api.mouse.SyMouseEventOnNoLongerOver; +import com.io7m.jsycamore.api.mouse.SyMouseEventOnOver; +import com.io7m.jsycamore.api.mouse.SyMouseEventOnPressed; +import com.io7m.jsycamore.api.mouse.SyMouseEventOnReleased; +import com.io7m.jsycamore.api.mouse.SyMouseEventType; +import com.io7m.jsycamore.api.themes.SyThemeClassNameType; +import com.io7m.jsycamore.components.standard.SyAlign; +import com.io7m.jsycamore.components.standard.SyComponentAbstract; +import com.io7m.jsycamore.components.standard.SyImageView; +import com.io7m.jtensors.core.parameterized.vectors.PVectors2I; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import static com.io7m.jsycamore.api.components.SyScrollBarDrag.Kind.DRAG_CONTINUED; +import static com.io7m.jsycamore.api.components.SyScrollBarDrag.Kind.DRAG_ENDED; +import static com.io7m.jsycamore.api.components.SyScrollBarDrag.Kind.DRAG_STARTED; +import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_CONSUMED; +import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; +import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.SCROLLBAR_VERTICAL_BUTTON_THUMB; +import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.SCROLLBAR_VERTICAL_BUTTON_THUMB_ICON; +import static com.io7m.jsycamore.components.standard.SyAlignmentHorizontal.ALIGN_HORIZONTAL_CENTER; +import static com.io7m.jsycamore.components.standard.SyAlignmentVertical.ALIGN_VERTICAL_CENTER; + +final class SyScrollBarVButtonThumb + extends SyComponentAbstract + implements SyButtonReadableType +{ + private static final Logger LOG = + LoggerFactory.getLogger(SyScrollBarVButtonThumb.class); + + private final SyImageView image; + private final SyAlign align; + private final SyScrollBarVTrack track; + private boolean pressed; + private Consumer onDragListener; + private double dragScrollStart; + private double scrollThen; + + SyScrollBarVButtonThumb( + final SyScrollBarVTrack inTrack) + { + super(List.of()); + + this.track = + Objects.requireNonNull(inTrack, "track"); + + this.image = + new SyImageView(List.of(SCROLLBAR_VERTICAL_BUTTON_THUMB_ICON)); + this.image.setImageURI("jsycamore:icon:scroll_v_thumb"); + + this.align = new SyAlign(); + this.align.alignmentHorizontal().set(ALIGN_HORIZONTAL_CENTER); + this.align.alignmentVertical().set(ALIGN_VERTICAL_CENTER); + this.align.childAdd(this.image); + this.childAdd(this.align); + + this.onDragListener = (ignored) -> { + + }; + } + + void setOnThumbDragListener( + final Consumer listener) + { + this.onDragListener = + Objects.requireNonNull(listener, "listener"); + } + + void removeOnThumbDragListener() + { + this.onDragListener = (ignored) -> { + + }; + } + + @Override + public List themeClassesDefaultForComponent() + { + return List.of(SCROLLBAR_VERTICAL_BUTTON_THUMB); + } + + @Override + public boolean isPressed() + { + return this.pressed; + } + + @Override + protected SyEventConsumed onEvent( + final SyEventType event) + { + return switch (event) { + case final SyMouseEventType e -> { + yield this.onMouseEvent(e); + } + default -> EVENT_NOT_CONSUMED; + }; + } + + private SyEventConsumed onMouseEvent( + final SyMouseEventType event) + { + return switch (event) { + case final SyMouseEventOnHeld e -> { + yield switch (e.button()) { + case MOUSE_BUTTON_LEFT -> { + yield this.onMouseDragged(e); + } + case MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT -> { + yield EVENT_NOT_CONSUMED; + } + }; + } + + case final SyMouseEventOnNoLongerOver e -> { + yield EVENT_NOT_CONSUMED; + } + + case final SyMouseEventOnOver e -> { + yield EVENT_NOT_CONSUMED; + } + + case final SyMouseEventOnPressed e -> { + yield switch (e.button()) { + case MOUSE_BUTTON_LEFT -> { + yield this.onMousePressed(); + } + case MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT -> { + yield EVENT_NOT_CONSUMED; + } + }; + } + + case final SyMouseEventOnReleased e -> { + yield switch (e.button()) { + case MOUSE_BUTTON_LEFT -> { + yield this.onMouseReleased(); + } + case MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT -> { + yield EVENT_NOT_CONSUMED; + } + }; + } + }; + } + + private SyEventConsumed onMouseReleased() + { + this.pressed = false; + this.publishToDragListener(DRAG_ENDED); + return EVENT_CONSUMED; + } + + private SyEventConsumed onMousePressed() + { + this.pressed = true; + this.dragScrollStart = this.track.scrollPosition(); + this.publishToDragListener(DRAG_STARTED); + return EVENT_CONSUMED; + } + + private SyEventConsumed onMouseDragged( + final SyMouseEventOnHeld e) + { + this.pressed = true; + + final var mouseThen = + e.mousePositionFirst(); + final var mouseNow = + e.mousePositionNow(); + final var mouseDelta = + PVectors2I.subtract(mouseNow, mouseThen); + + final var trackSize = + this.track.size().get().sizeY(); + final var trackDelta = + (double) mouseDelta.y() / (double) trackSize; + + this.track.setScrollPosition(this.dragScrollStart + trackDelta); + this.publishToDragListener(DRAG_CONTINUED); + this.scrollThen = this.track.scrollPosition(); + return EVENT_CONSUMED; + } + + private void publishToDragListener( + final SyScrollBarDrag.Kind dragKind) + { + try { + final var shouldPublish = switch (dragKind) { + case DRAG_STARTED -> true; + case DRAG_CONTINUED -> this.scrollThen != this.track.scrollPosition(); + case DRAG_ENDED -> true; + }; + + if (shouldPublish) { + this.onDragListener.accept( + new SyScrollBarDrag( + dragKind, + this.dragScrollStart, + this.track.scrollPosition() + ) + ); + } + } catch (final Throwable ex) { + LOG.debug("Ignored listener exception: ", ex); + } + } +} diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonUp.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonUp.java new file mode 100644 index 00000000..8acd1732 --- /dev/null +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVButtonUp.java @@ -0,0 +1,64 @@ +/* + * 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.internal.scrollbars; + +import com.io7m.jsycamore.api.events.SyEventConsumed; +import com.io7m.jsycamore.api.events.SyEventType; +import com.io7m.jsycamore.components.standard.SyAlign; +import com.io7m.jsycamore.components.standard.SyButtonAbstract; +import com.io7m.jsycamore.components.standard.SyImageView; + +import java.util.List; + +import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; +import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.SCROLLBAR_VERTICAL_BUTTON_UP; +import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.SCROLLBAR_VERTICAL_BUTTON_UP_ICON; +import static com.io7m.jsycamore.components.standard.SyAlignmentHorizontal.ALIGN_HORIZONTAL_CENTER; +import static com.io7m.jsycamore.components.standard.SyAlignmentVertical.ALIGN_VERTICAL_CENTER; + +final class SyScrollBarVButtonUp extends SyButtonAbstract +{ + private final SyImageView image; + private final SyAlign align; + + SyScrollBarVButtonUp() + { + super(List.of(SCROLLBAR_VERTICAL_BUTTON_UP)); + + this.image = new SyImageView(List.of(SCROLLBAR_VERTICAL_BUTTON_UP_ICON)); + this.image.setImageURI("jsycamore:icon:scroll_up"); + + this.align = new SyAlign(); + this.align.alignmentHorizontal().set(ALIGN_HORIZONTAL_CENTER); + this.align.alignmentVertical().set(ALIGN_VERTICAL_CENTER); + this.align.childAdd(this.image); + this.childAdd(this.align); + } + + @Override + protected SyEventConsumed onOtherEvent( + final SyEventType event) + { + return EVENT_NOT_CONSUMED; + } + + @Override + protected void onClicked() + { + + } +} diff --git a/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVTrack.java b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVTrack.java new file mode 100644 index 00000000..23255dc1 --- /dev/null +++ b/com.io7m.jsycamore.components.standard/src/main/java/com/io7m/jsycamore/components/standard/internal/scrollbars/SyScrollBarVTrack.java @@ -0,0 +1,162 @@ +/* + * 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.internal.scrollbars; + +import com.io7m.jinterp.InterpolationD; +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.SyScrollBarDrag; +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.jsycamore.components.standard.SyComponentAbstract; +import com.io7m.jtensors.core.parameterized.vectors.PVector2I; + +import java.util.List; +import java.util.function.Consumer; + +import static com.io7m.jsycamore.api.events.SyEventConsumed.EVENT_NOT_CONSUMED; +import static com.io7m.jsycamore.api.layout.SySnapping.snapDouble; +import static com.io7m.jsycamore.api.themes.SyThemeClassNameStandard.SCROLLBAR_VERTICAL_TRACK; + +final class SyScrollBarVTrack extends SyComponentAbstract +{ + private final SyScrollBarVButtonThumb thumb; + private double scrollAmount; + private double scrollPosition; + private double scrollPositionSnap; + + SyScrollBarVTrack() + { + super(List.of()); + + this.thumb = new SyScrollBarVButtonThumb(this); + this.childAdd(this.thumb); + } + + @Override + public List themeClassesDefaultForComponent() + { + return List.of(SCROLLBAR_VERTICAL_TRACK); + } + + @Override + public PAreaSizeI layout( + final SyLayoutContextType layoutContext, + final SyConstraints constraints) + { + final var size = + super.layout(layoutContext, constraints); + + /* + * The minimum size on the Y axis that would yield a square is the value + * of the X axis. The smallest the thumb can be is a square. The largest + * it can be is "fill the entire track". + */ + + final var thumbHeightMinimum = + size.sizeX(); + final var thumbHeightMaximum = + size.sizeY(); + + final var thumbHeight = + (int) InterpolationD.interpolateLinear( + thumbHeightMinimum, + thumbHeightMaximum, + this.scrollAmount + ); + + final var limitedConstraints = + new SyConstraints( + constraints.sizeMinimumX(), + Math.max(constraints.sizeMinimumY(), thumbHeight), + constraints.sizeMaximumX(), + Math.min(constraints.sizeMaximumY(), thumbHeight) + ); + + this.thumb.layout(layoutContext, limitedConstraints); + + final var thumbPosition = + this.scrollPosition * (size.sizeY() - this.thumb.size().get().sizeY()); + + this.thumb.setPosition(PVector2I.of(0, (int) thumbPosition)); + return size; + } + + void setOnThumbDragListener( + final Consumer listener) + { + this.thumb.setOnThumbDragListener(listener); + } + + void removeOnThumbDragListener() + { + this.thumb.removeOnThumbDragListener(); + } + + void setScrollPositionSnap( + final double fraction) + { + this.scrollPositionSnap = Math.clamp(fraction, 0.0, 1.0); + } + + void setScrollPosition( + final double position) + { + final var clampedPosition = + Math.clamp(position, 0.0, 1.0); + this.scrollPosition = + snapDouble(clampedPosition, this.scrollPositionSnap); + } + + void setScrollAmountShown( + final double extent) + { + this.scrollAmount = Math.clamp(extent, 0.0, 1.0); + } + + @Override + protected SyEventConsumed onEvent( + final SyEventType event) + { + return EVENT_NOT_CONSUMED; + } + + double scrollPosition() + { + return this.scrollPosition; + } + + SyComponentReadableType thumb() + { + return this.thumb; + } + + double scrollPositionSnapping() + { + return this.scrollPositionSnap; + } + + double scrollAmountShown() + { + return this.scrollAmount; + } +} 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 1872743e..69f9237b 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 @@ -1204,7 +1204,7 @@ public void testMouseEventsOutsideMoved( 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.mouseMoved(position); diff --git a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScrollBarVerticalTest.java b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScrollBarVerticalTest.java new file mode 100644 index 00000000..0a296567 --- /dev/null +++ b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScrollBarVerticalTest.java @@ -0,0 +1,377 @@ +/* + * 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.components.SyScrollBarDrag; +import com.io7m.jsycamore.api.components.SyScrollBarVerticalType; +import com.io7m.jsycamore.api.spaces.SySpaceViewportType; +import com.io7m.jsycamore.components.standard.SyScrollBarsVertical; +import com.io7m.jtensors.core.parameterized.vectors.PVector2I; +import com.io7m.jtensors.core.parameterized.vectors.PVectors2I; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.LinkedList; + +import static com.io7m.jsycamore.api.components.SyScrollBarDrag.Kind.DRAG_CONTINUED; +import static com.io7m.jsycamore.api.components.SyScrollBarDrag.Kind.DRAG_ENDED; +import static com.io7m.jsycamore.api.components.SyScrollBarDrag.Kind.DRAG_STARTED; +import static com.io7m.jsycamore.api.components.SyScrollBarPresencePolicy.ALWAYS_ENABLED; +import static com.io7m.jsycamore.api.components.SyScrollBarPresencePolicy.DISABLED_IF_ENTIRE_RANGE_SHOWN; +import static com.io7m.jsycamore.api.mouse.SyMouseButton.MOUSE_BUTTON_LEFT; +import static com.io7m.jsycamore.api.mouse.SyMouseButton.MOUSE_BUTTON_RIGHT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class SyScrollBarVerticalTest + extends SyComponentContract +{ + private static final PVector2I Z = + PVectors2I.zero(); + private static final PVector2I ELSEWHERE = + PVector2I.of(8192, 8192); + + private int clicks; + private LinkedList drags; + + @BeforeEach + public void buttonSetup() + { + this.clicks = 0; + this.drags = new LinkedList(); + } + + @Override + protected SyScrollBarVerticalType newComponent() + { + return SyScrollBarsVertical.create(); + } + + /** + * Pressing scrollbar buttons works. + */ + + @Test + public void testClickUp() + { + final var c = this.newComponent(); + final var b = c.buttonUp(); + + this.windowContentArea().childAdd(c); + this.window().layout(this.layoutContext); + this.screen().mouseMoved(Z); + + c.setOnClickUpListener(() -> this.clicks++); + + this.screen().mouseDown(Z, MOUSE_BUTTON_LEFT); + assertEquals(0, this.clicks); + + this.screen().mouseUp(Z, MOUSE_BUTTON_LEFT); + assertEquals(1, this.clicks); + + c.removeOnClickUpListener(); + + this.screen().mouseDown(Z, MOUSE_BUTTON_LEFT); + assertEquals(1, this.clicks); + + this.screen().mouseUp(Z, MOUSE_BUTTON_LEFT); + assertEquals(1, this.clicks); + } + + /** + * Pressing scrollbar buttons with the wrong mouse buttons does nothing. + */ + + @Test + public void testClickUpNotButton() + { + final var c = this.newComponent(); + final var b = c.buttonUp(); + + this.windowContentArea().childAdd(c); + this.window().layout(this.layoutContext); + this.screen().mouseMoved(Z); + + c.setOnClickUpListener(() -> this.clicks++); + + this.screen().mouseDown(Z, MOUSE_BUTTON_RIGHT); + assertEquals(0, this.clicks); + + this.screen().mouseUp(Z, MOUSE_BUTTON_RIGHT); + assertEquals(0, this.clicks); + } + + /** + * Pressing scrollbar buttons works. + */ + + @Test + public void testClickDown() + { + final var c = this.newComponent(); + final var b = c.buttonDown(); + + this.windowContentArea().childAdd(c); + this.window().layout(this.layoutContext); + this.screen().mouseMoved(Z); + + c.setOnClickDownListener(() -> this.clicks++); + + final var p = + c.viewportPositionOf(b.position().get()); + final var q = + PVector2I.of(p.x() + 4, p.y() + 4); + + this.screen().mouseDown(q, MOUSE_BUTTON_LEFT); + assertEquals(0, this.clicks); + + this.screen().mouseUp(q, MOUSE_BUTTON_LEFT); + assertEquals(1, this.clicks); + + c.removeOnClickDownListener(); + + this.screen().mouseDown(q, MOUSE_BUTTON_LEFT); + assertEquals(1, this.clicks); + + this.screen().mouseUp(q, MOUSE_BUTTON_LEFT); + assertEquals(1, this.clicks); + } + + /** + * Pressing scrollbar buttons with the wrong mouse buttons does nothing. + */ + + @Test + public void testClickDownNotButton() + { + final var c = this.newComponent(); + final var b = c.buttonDown(); + + this.windowContentArea().childAdd(c); + this.window().layout(this.layoutContext); + this.screen().mouseMoved(Z); + + c.setOnClickDownListener(() -> this.clicks++); + + final var p = + c.viewportPositionOf(b.position().get()); + final var q = + PVector2I.of(p.x() + 4, p.y() + 4); + + this.screen().mouseDown(q, MOUSE_BUTTON_RIGHT); + assertEquals(0, this.clicks); + + this.screen().mouseUp(q, MOUSE_BUTTON_RIGHT); + assertEquals(0, this.clicks); + } + + /** + * Setting the scroll position works. + */ + + @Test + public void testScrollPosition() + { + final var c = this.newComponent(); + + c.setScrollPosition(-1.0); + assertEquals(0.0, c.scrollPosition()); + c.setScrollPosition(0.0); + assertEquals(0.0, c.scrollPosition()); + c.setScrollPosition(1.0); + assertEquals(1.0, c.scrollPosition()); + c.setScrollPosition(2.0); + assertEquals(1.0, c.scrollPosition()); + } + + /** + * Setting the scroll snapping works. + */ + + @Test + public void testScrollSnapping() + { + final var c = this.newComponent(); + + c.setScrollPositionSnapping(-1.0); + assertEquals(0.0, c.scrollPositionSnapping()); + c.setScrollPositionSnapping(0.0); + assertEquals(0.0, c.scrollPositionSnapping()); + c.setScrollPositionSnapping(1.0); + assertEquals(1.0, c.scrollPositionSnapping()); + c.setScrollPositionSnapping(2.0); + assertEquals(1.0, c.scrollPositionSnapping()); + } + + /** + * Dragging scrollbar thumbs works. + */ + + @Test + public void testThumbDrag() + { + final var c = this.newComponent(); + final var t = c.thumb(); + + this.windowContentArea().childAdd(c); + this.window().layout(this.layoutContext); + this.screen().mouseMoved(Z); + + c.setOnThumbDragListener(drag -> this.drags.add(drag)); + + final var p0 = + t.viewportPositionOf(t.position().get()); + final var p1 = + PVector2I.of(p0.x() + 4, p0.y() + 4); + final var p2 = + PVector2I.of(p1.x(), p1.y() + 128); + + this.screen().mouseDown(p1, MOUSE_BUTTON_LEFT); + this.screen().mouseMoved(p2); + this.screen().mouseUp(p2, MOUSE_BUTTON_LEFT); + + { + final var d = this.drags.remove(0); + assertEquals(DRAG_STARTED, d.dragKind()); + assertEquals(0.0, d.dragStart()); + assertEquals(0.0, d.dragNow()); + } + + { + final var d = this.drags.remove(0); + assertEquals(DRAG_CONTINUED, d.dragKind()); + assertEquals(0.0, d.dragStart()); + assertNotEquals(0.0, d.dragNow()); + } + + { + final var d = this.drags.remove(0); + assertEquals(DRAG_ENDED, d.dragKind()); + assertEquals(0.0, d.dragStart()); + assertNotEquals(0.0, d.dragNow()); + } + + c.removeOnThumbDragListener(); + + this.screen().mouseDown(p1, MOUSE_BUTTON_LEFT); + this.screen().mouseMoved(p2); + this.screen().mouseUp(p2, MOUSE_BUTTON_LEFT); + + assertEquals(0, this.drags.size()); + } + + /** + * Dragging scrollbar thumbs with the wrong mouse buttons does nothing. + */ + + @Test + public void testThumbDragNotButton() + { + final var c = this.newComponent(); + final var t = c.thumb(); + + this.windowContentArea().childAdd(c); + this.window().layout(this.layoutContext); + this.screen().mouseMoved(Z); + + c.setOnThumbDragListener(drag -> this.drags.add(drag)); + + final var p0 = + t.viewportPositionOf(t.position().get()); + final var p1 = + PVector2I.of(p0.x() + 4, p0.y() + 4); + final var p2 = + PVector2I.of(p1.x() + 128, p1.y()); + + this.screen().mouseDown(p1, MOUSE_BUTTON_RIGHT); + this.screen().mouseMoved(p2); + this.screen().mouseUp(p2, MOUSE_BUTTON_RIGHT); + + assertEquals(0, this.drags.size()); + } + + /** + * The ALWAYS_ENABLED presence policy works. + */ + + @Test + public void testPresencePolicyAlwaysActive() + { + final var c = this.newComponent(); + c.presencePolicy().set(ALWAYS_ENABLED); + + final var t = c.thumb(); + final var l = c.buttonUp(); + final var r = c.buttonDown(); + + this.windowContentArea().childAdd(c); + this.window().layout(this.layoutContext); + this.screen().mouseMoved(Z); + + c.setScrollAmountShown(0.0); + assertTrue(t.isActive()); + assertTrue(l.isActive()); + assertTrue(r.isActive()); + + c.setScrollAmountShown(1.0); + assertTrue(t.isActive()); + assertTrue(l.isActive()); + assertTrue(r.isActive()); + + c.setScrollAmountShown(0.0); + assertTrue(t.isActive()); + assertTrue(l.isActive()); + assertTrue(r.isActive()); + } + + /** + * The DISABLED_IF_ENTIRE_RANGE_SHOWN presence policy works. + */ + + @Test + public void testPresencePolicyDisabledIfFullyShown() + { + final var c = this.newComponent(); + c.presencePolicy().set(DISABLED_IF_ENTIRE_RANGE_SHOWN); + + final var t = c.thumb(); + final var l = c.buttonUp(); + final var r = c.buttonDown(); + + this.windowContentArea().childAdd(c); + this.window().layout(this.layoutContext); + this.screen().mouseMoved(Z); + + c.setScrollAmountShown(0.0); + assertTrue(t.isActive()); + assertTrue(l.isActive()); + assertTrue(r.isActive()); + + c.setScrollAmountShown(1.0); + assertFalse(t.isActive()); + assertFalse(l.isActive()); + assertFalse(r.isActive()); + + c.setScrollAmountShown(0.0); + assertTrue(t.isActive()); + assertTrue(l.isActive()); + assertTrue(r.isActive()); + } +} diff --git a/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScrollbarVerticalDemo.java b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScrollbarVerticalDemo.java new file mode 100644 index 00000000..ae8e9d02 --- /dev/null +++ b/com.io7m.jsycamore.tests/src/main/java/com/io7m/jsycamore/tests/SyScrollbarVerticalDemo.java @@ -0,0 +1,278 @@ +/* + * Copyright © 2021 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.jregions.core.parameterized.sizes.PAreaSizeI; +import com.io7m.jsycamore.api.mouse.SyMouseButton; +import com.io7m.jsycamore.api.screens.SyScreenType; +import com.io7m.jsycamore.api.spaces.SySpaceViewportType; +import com.io7m.jsycamore.api.text.SyFontDescription; +import com.io7m.jsycamore.api.text.SyFontDirectoryType; +import com.io7m.jsycamore.api.text.SyFontStyle; +import com.io7m.jsycamore.api.themes.SyThemeType; +import com.io7m.jsycamore.api.windows.SyWindowType; +import com.io7m.jsycamore.awt.internal.SyAWTImageLoader; +import com.io7m.jsycamore.awt.internal.SyAWTKeyCodeAdapter; +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.components.standard.SyLayoutMargin; +import com.io7m.jsycamore.components.standard.SyScrollBarsHorizontal; +import com.io7m.jsycamore.components.standard.SyScrollBarsVertical; +import com.io7m.jsycamore.theme.primal.SyThemePrimalFactory; +import com.io7m.jsycamore.vanilla.SyScreenFactory; +import com.io7m.jsycamore.vanilla.internal.SyLayoutContext; +import com.io7m.jtensors.core.parameterized.vectors.PVector2I; +import com.io7m.junreachable.UnreachableCodeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.WindowConstants; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.concurrent.Executors; + +import static com.io7m.jsycamore.api.windows.SyWindowCloseBehaviour.HIDE_ON_CLOSE_BUTTON; +import static java.lang.Boolean.TRUE; +import static java.util.concurrent.TimeUnit.SECONDS; + +public final class SyScrollbarVerticalDemo +{ + private static final Logger LOG = + LoggerFactory.getLogger(SyScrollbarVerticalDemo.class); + + private SyScrollbarVerticalDemo() + { + throw new UnreachableCodeException(); + } + + public static void main(final String[] args) + { + SwingUtilities.invokeLater(() -> { + try { + final var frame = new JFrame("WindowDemo"); + frame.setPreferredSize(new Dimension(800, 600)); + frame.getContentPane().add(new Canvas()); + frame.pack(); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + frame.setVisible(true); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }); + } + + private static final class Canvas extends JPanel + { + private final SyFontDirectoryType fontDirectory; + private final SyAWTRenderer renderer; + private final SyScreenType screen; + private final SyThemeType theme; + private final SyWindowType window0; + private final SyAWTImageLoader imageLoader; + + Canvas() + throws Exception + { + this.setFocusable(true); + + this.fontDirectory = + SyFontDirectoryAWT.createFromServiceLoader(); + this.imageLoader = + new SyAWTImageLoader(); + + this.theme = + new SyThemePrimalFactory() + .create(); + + this.theme.values() + .setFont("text_font", new SyFontDescription( + "York Sans", + SyFontStyle.REGULAR, + 12 + )); + + this.theme.values() + .setFont("window_title_text_font", new SyFontDescription( + "York Sans", + SyFontStyle.REGULAR, + 12 + )); + + this.screen = + new SyScreenFactory().create( + this.theme, + this.fontDirectory, + PAreaSizeI.of(800, 600) + ); + + this.window0 = + this.screen.windowCreate(640, 480); + + this.window0.closeButtonBehaviour() + .set(HIDE_ON_CLOSE_BUTTON); + + this.renderer = new SyAWTRenderer(this.fontDirectory, this.imageLoader); + this.renderer.nodeRenderer() + .setTextAntialiasing(false); + + final var executor = + Executors.newSingleThreadScheduledExecutor(runnable -> { + final var thread = new Thread(runnable); + thread.setName("SyWindowDemo.scheduler"); + thread.setDaemon(true); + return thread; + }); + + final var mouseAdapter = new MouseAdapter() + { + @Override + public void mousePressed( + final MouseEvent e) + { + SyScrollbarVerticalDemo.Canvas.this.screen.mouseDown( + PVector2I.of(e.getX(), e.getY()), + SyMouseButton.ofIndex(e.getButton() - 1)); + SyScrollbarVerticalDemo.Canvas.this.repaint(); + } + + @Override + public void mouseDragged( + final MouseEvent e) + { + SyScrollbarVerticalDemo.Canvas.this.screen.mouseMoved(PVector2I.of( + e.getX(), + e.getY())); + SyScrollbarVerticalDemo.Canvas.this.repaint(); + } + + @Override + public void mouseReleased( + final MouseEvent e) + { + SyScrollbarVerticalDemo.Canvas.this.screen.mouseUp( + PVector2I.of(e.getX(), e.getY()), + SyMouseButton.ofIndex(e.getButton() - 1)); + SyScrollbarVerticalDemo.Canvas.this.repaint(); + } + + @Override + public void mouseMoved( + final MouseEvent e) + { + final PVector2I position = + PVector2I.of(e.getX(), e.getY()); + SyScrollbarVerticalDemo.Canvas.this.screen.mouseMoved(position); + SyScrollbarVerticalDemo.Canvas.this.repaint(); + } + }; + + final var keyAdapter = new SyAWTKeyCodeAdapter(this.screen); + this.addMouseMotionListener(mouseAdapter); + this.addMouseListener(mouseAdapter); + this.addKeyListener(keyAdapter); + + this.addComponentListener(new ComponentAdapter() + { + @Override + public void componentResized(final ComponentEvent e) + { + SyScrollbarVerticalDemo.Canvas.this.screen.setSize( + PAreaSizeI.of( + e.getComponent().getWidth(), + e.getComponent().getHeight()) + ); + } + }); + + { + final var margin = new SyLayoutMargin(); + margin.setPaddingAll(8); + + final var scrollbar = SyScrollBarsVertical.create(); + scrollbar.setScrollPositionSnapping(1.0 / 64.0); + scrollbar.setScrollAmountShown(0.5); + scrollbar.setOnThumbDragListener(drag -> { + LOG.debug("Drag: {}", drag); + }); + scrollbar.setOnClickUpListener(() -> { + LOG.debug("Click Up!"); + }); + scrollbar.setOnClickDownListener(() -> { + LOG.debug("Click Down!"); + }); + + margin.childAdd(scrollbar); + + this.window0.decorated().set(TRUE); + this.window0.contentArea().childAdd(margin); + this.window0.title().set("Window Title"); + this.window0.positionSnapping().set(16); + this.window0.sizeSnapping().set(16); + } + + executor.scheduleAtFixedRate(() -> { + SwingUtilities.invokeLater(() -> { + if (!this.screen.windowIsVisible(this.window0)) { + this.screen.windowShow(this.window0); + } + }); + }, 0L, 2L, SECONDS); + } + + @Override + public void paint(final Graphics g) + { + final var g2 = (Graphics2D) g; + + g2.setColor(Color.GRAY); + g2.fillRect(0, 0, this.getWidth(), this.getHeight()); + + g2.setPaint(new Color(144, 144, 144)); + + for (int x = 0; x < this.getWidth(); x += 32) { + g2.drawLine(x, 0, x, this.getHeight()); + } + for (int y = 0; y < this.getHeight(); y += 32) { + g2.drawLine(0, y, this.getWidth(), y); + } + + final var layoutContext = + new SyLayoutContext( + this.fontDirectory, + this.screen.theme() + ); + + final var windows = this.screen.windowsVisibleOrdered(); + 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); + } + } + } +} \ No newline at end of file diff --git a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalImageView.java b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalImageView.java index 12d239b3..40a57a03 100644 --- a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalImageView.java +++ b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalImageView.java @@ -73,9 +73,18 @@ private static SyRenderNodeType imageOf( case "icon:scroll_right" -> { yield iconOf(size, "scroll_right.png"); } + case "icon:scroll_up" -> { + yield iconOf(size, "scroll_up.png"); + } + case "icon:scroll_down" -> { + yield iconOf(size, "scroll_down.png"); + } case "icon:scroll_h_thumb" -> { yield iconOf(size, "scroll_h_thumb.png"); } + case "icon:scroll_v_thumb" -> { + yield iconOf(size, "scroll_v_thumb.png"); + } default -> SyRenderNodeNoop.noop(); }; } @@ -113,7 +122,7 @@ public SyRenderNodeType render( final var area = component.boundingArea(); - if (component instanceof SyImageViewType imageView) { + if (component instanceof final SyImageViewType imageView) { return imageView.imageURI() .get() .map(uri -> imageOf(area, uri)) diff --git a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarV.java b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarV.java new file mode 100644 index 00000000..7903d912 --- /dev/null +++ b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarV.java @@ -0,0 +1,54 @@ +/* + * 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.theme.primal.internal; + +import com.io7m.jsycamore.api.components.SyComponentReadableType; +import com.io7m.jsycamore.api.rendering.SyRenderNodeNoop; +import com.io7m.jsycamore.api.rendering.SyRenderNodeType; +import com.io7m.jsycamore.api.themes.SyThemeContextType; + +import java.util.Objects; + +/** + * A theme component for scrollbars. + */ + +public final class SyPrimalScrollbarV extends SyPrimalAbstract +{ + /** + * A theme component for scrollbars. + * + * @param inTheme The theme + */ + + public SyPrimalScrollbarV( + final SyThemePrimal inTheme) + { + super(inTheme); + } + + @Override + public SyRenderNodeType render( + final SyThemeContextType context, + final SyComponentReadableType component) + { + Objects.requireNonNull(context, "context"); + Objects.requireNonNull(component, "component"); + + return SyRenderNodeNoop.noop(); + } +} diff --git a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVButton.java b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVButton.java new file mode 100644 index 00000000..918f947a --- /dev/null +++ b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVButton.java @@ -0,0 +1,70 @@ +/* + * 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.theme.primal.internal; + +import com.io7m.jregions.core.parameterized.sizes.PAreaSizeI; +import com.io7m.jsycamore.api.components.SyComponentReadableType; +import com.io7m.jsycamore.api.rendering.SyRenderNodeType; +import com.io7m.jsycamore.api.spaces.SySpaceParentRelativeType; +import com.io7m.jsycamore.api.themes.SyThemeContextType; + +import java.util.Objects; +import java.util.Optional; + +/** + * A theme component for scrollbar buttons. + */ + +public final class SyPrimalScrollbarVButton extends SyPrimalAbstract +{ + private final SyPrimalButton button; + + /** + * A theme component for scrollbar buttons. + * + * @param inTheme The theme + * @param inButton The button theme + */ + + public SyPrimalScrollbarVButton( + final SyThemePrimal inTheme, + final SyPrimalButton inButton) + { + super(inTheme); + this.button = + Objects.requireNonNull(inButton, "inButton"); + } + + @Override + public Optional> size( + final SyThemeContextType context, + final SyComponentReadableType component) + { + return Optional.of(PAreaSizeI.of(16, 16)); + } + + @Override + public SyRenderNodeType render( + final SyThemeContextType context, + final SyComponentReadableType component) + { + Objects.requireNonNull(context, "context"); + Objects.requireNonNull(component, "component"); + + return this.button.render(context, component); + } +} diff --git a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVButtonIcon.java b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVButtonIcon.java new file mode 100644 index 00000000..672bb4b3 --- /dev/null +++ b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVButtonIcon.java @@ -0,0 +1,70 @@ +/* + * 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.theme.primal.internal; + +import com.io7m.jregions.core.parameterized.sizes.PAreaSizeI; +import com.io7m.jsycamore.api.components.SyComponentReadableType; +import com.io7m.jsycamore.api.rendering.SyRenderNodeType; +import com.io7m.jsycamore.api.spaces.SySpaceParentRelativeType; +import com.io7m.jsycamore.api.themes.SyThemeContextType; + +import java.util.Objects; +import java.util.Optional; + +/** + * A theme component for scrollbar buttons. + */ + +public final class SyPrimalScrollbarVButtonIcon extends SyPrimalAbstract +{ + private final SyPrimalImageView imageView; + + /** + * A theme component for scrollbar buttons. + * + * @param inTheme The theme + * @param inImageView The image view + */ + + public SyPrimalScrollbarVButtonIcon( + final SyThemePrimal inTheme, + final SyPrimalImageView inImageView) + { + super(inTheme); + + this.imageView = + Objects.requireNonNull(inImageView, "inImageView"); + } + + @Override + public Optional> size( + final SyThemeContextType context, + final SyComponentReadableType component) + { + return Optional.of(PAreaSizeI.of(8, 8)); + } + + @Override + public SyRenderNodeType render( + final SyThemeContextType context, + final SyComponentReadableType component) + { + Objects.requireNonNull(context, "context"); + Objects.requireNonNull(component, "component"); + return this.imageView.render(context, component); + } +} diff --git a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVTrack.java b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVTrack.java new file mode 100644 index 00000000..883bed50 --- /dev/null +++ b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyPrimalScrollbarVTrack.java @@ -0,0 +1,91 @@ +/* + * 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.theme.primal.internal; + +import com.io7m.jregions.core.parameterized.areas.PAreasI; +import com.io7m.jsycamore.api.components.SyComponentReadableType; +import com.io7m.jsycamore.api.rendering.SyRenderNodeComposite; +import com.io7m.jsycamore.api.rendering.SyRenderNodeShape; +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.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_BACKGROUND; +import static com.io7m.jsycamore.theme.primal.internal.SyPrimalValues.PRIMARY_EDGE; + +/** + * A theme component for scrollbar tracks. + */ + +public final class SyPrimalScrollbarVTrack extends SyPrimalAbstract +{ + /** + * A theme component for scrollbar tracks. + * + * @param inTheme The theme + */ + + public SyPrimalScrollbarVTrack( + final SyThemePrimal inTheme) + { + super(inTheme); + } + + @Override + public SyRenderNodeType render( + final SyThemeContextType context, + final SyComponentReadableType component) + { + Objects.requireNonNull(context, "context"); + Objects.requireNonNull(component, "component"); + + try { + final var area = + component.boundingArea(); + final var rectangle = + new SyShapeRectangle( + PAreasI.cast(PAreasI.moveToOrigin(area)) + ); + + final var values = + this.theme().values(); + + final var mainFill = + new SyRenderNodeShape( + Optional.empty(), + Optional.of(values.fillFlat(PRIMARY_BACKGROUND)), + rectangle + ); + + final var mainEdge = + new SyRenderNodeShape( + Optional.of(values.edgeFlat(PRIMARY_EDGE)), + Optional.empty(), + rectangle + ); + + return SyRenderNodeComposite.composite(mainFill, mainEdge); + } catch (final SyThemeValueException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyThemePrimal.java b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyThemePrimal.java index 6877a870..777f5906 100644 --- a/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyThemePrimal.java +++ b/com.io7m.jsycamore.theme.primal/src/main/java/com/io7m/jsycamore/theme/primal/internal/SyThemePrimal.java @@ -173,6 +173,31 @@ public SyThemePrimal() this.standards.put(className, new SyPrimalScrollbarHButtonIcon(this, imageView)); } + case SCROLLBAR_VERTICAL -> { + this.standards.put(className, new SyPrimalScrollbarV(this)); + } + case SCROLLBAR_VERTICAL_TRACK -> { + this.standards.put(className, new SyPrimalScrollbarVTrack(this)); + } + case SCROLLBAR_VERTICAL_BUTTON_UP -> { + this.standards.put(className, new SyPrimalScrollbarVButton(this, button)); + } + case SCROLLBAR_VERTICAL_BUTTON_UP_ICON -> { + this.standards.put(className, new SyPrimalScrollbarVButtonIcon(this, imageView)); + } + case SCROLLBAR_VERTICAL_BUTTON_DOWN -> { + this.standards.put(className, new SyPrimalScrollbarVButton(this, button)); + } + case SCROLLBAR_VERTICAL_BUTTON_DOWN_ICON -> { + this.standards.put(className, new SyPrimalScrollbarVButtonIcon(this, imageView)); + } + case SCROLLBAR_VERTICAL_BUTTON_THUMB -> { + this.standards.put(className, new SyPrimalScrollbarVButton(this, button)); + } + case SCROLLBAR_VERTICAL_BUTTON_THUMB_ICON -> { + this.standards.put(className, new SyPrimalScrollbarVButtonIcon(this, imageView)); + } + case TEXT_AREA -> { this.standards.put(className, new SyPrimalTextArea(this)); } diff --git a/com.io7m.jsycamore.theme.primal/src/main/resources/com/io7m/jsycamore/theme/primal/scroll_v_thumb.png b/com.io7m.jsycamore.theme.primal/src/main/resources/com/io7m/jsycamore/theme/primal/scroll_v_thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..1fdc65ba0da5d6da286b84c5fea75d8e59518f0f GIT binary patch literal 87 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqa-J@ZArhC9|NQ^|-^WK jiHz%;fIMTKN@0fn3q0RvW;M72RWf+G`njxgN@xNA%f}l2 literal 0 HcmV?d00001 diff --git a/com.io7m.jsycamore.theme.primal/src/main/resources/com/io7m/jsycamore/theme/primal/scroll_v_thumb.xcf b/com.io7m.jsycamore.theme.primal/src/main/resources/com/io7m/jsycamore/theme/primal/scroll_v_thumb.xcf new file mode 100644 index 0000000000000000000000000000000000000000..4990d3752fdbd0d0f412a86eade13c1e8b06509c GIT binary patch literal 727 zcmb7?%T9wp6ovtf)N141m5cF~&;6S6)A+85z0 TSGm1wkNTgw?>4UWAB~z{{xxmS literal 0 HcmV?d00001 diff --git a/com.io7m.jsycamore.vanilla/src/main/java/com/io7m/jsycamore/vanilla/internal/SyWindow.java b/com.io7m.jsycamore.vanilla/src/main/java/com/io7m/jsycamore/vanilla/internal/SyWindow.java index c7c607f7..e3c7fc86 100644 --- a/com.io7m.jsycamore.vanilla/src/main/java/com/io7m/jsycamore/vanilla/internal/SyWindow.java +++ b/com.io7m.jsycamore.vanilla/src/main/java/com/io7m/jsycamore/vanilla/internal/SyWindow.java @@ -26,8 +26,8 @@ import com.io7m.jsycamore.api.components.SyComponentType; import com.io7m.jsycamore.api.components.SyConstraints; import com.io7m.jsycamore.api.layout.SyLayoutContextType; +import com.io7m.jsycamore.api.layout.SySnapping; import com.io7m.jsycamore.api.screens.SyScreenType; -import com.io7m.jsycamore.api.spaces.SySpaceType; import com.io7m.jsycamore.api.spaces.SySpaceViewportType; import com.io7m.jsycamore.api.spaces.SySpaceWindowType; import com.io7m.jsycamore.api.visibility.SyVisibility; @@ -185,37 +185,12 @@ public void setPosition( { Objects.requireNonNull(newPosition, "position"); + final var snapping = this.positionSnapping.get().intValue(); if (this.maximized.get()) { - this.positionMaximized = this.snapPosition(newPosition); + this.positionMaximized = SySnapping.snapVector(newPosition, snapping); } else { - this.position = this.snapPosition(newPosition); - } - } - - private PVector2I snapPosition( - final PVector2I newPosition) - { - final var snapping = this.positionSnapping.get().intValue(); - if (snapping > 0) { - return PVector2I.of( - (newPosition.x() / snapping) * snapping, - (newPosition.y() / snapping) * snapping - ); + this.position = SySnapping.snapVector(newPosition, snapping); } - return newPosition; - } - - private PAreaSizeI snapSize( - final PAreaSizeI newSize) - { - final var snapping = this.sizeSnapping.get().intValue(); - if (snapping > 0) { - return PAreaSizeI.of( - (newSize.sizeX() / snapping) * snapping, - (newSize.sizeY() / snapping) * snapping - ); - } - return newSize; } @Override @@ -225,19 +200,24 @@ public void setSize( Objects.requireNonNull(newSize, "size"); final var clampedSize = - this.snapSize(PAreaSizeI.of( + PAreaSizeI.of( Math.max(0, newSize.sizeX()), Math.max(0, newSize.sizeY()) - )); + ); + + final var snapping = + this.sizeSnapping.get().intValue(); + final var snappedSize = + SySnapping.snapSize(clampedSize, snapping); if (this.maximized.get()) { - this.sizeMaximized = clampedSize; + this.sizeMaximized = snappedSize; } else { - this.size = clampedSize; + this.size = snappedSize; } - final var sizeX = clampedSize.sizeX(); - final var sizeY = clampedSize.sizeY(); + final var sizeX = snappedSize.sizeX(); + final var sizeY = snappedSize.sizeY(); this.root.size().set(PAreaSizeI.of(sizeX, sizeY)); this.viewportAccumulator.reset(sizeX, sizeY); this.constraints =