Пример #1
0
/**
 * A scroll pane to scroll another widget if it requires more space then available.
 *
 * <p>It requires the following child themes:
 *
 * <table>
 * <tr>
 * <th>Theme</th>
 * <th>Description</th>
 * </tr>
 * <tr>
 * <td>hscrollbar</td>
 * <td>The horizontal scrollbar</td>
 * </tr>
 * <tr>
 * <td>vscrollbar</td>
 * <td>The vertical scrollbar</td>
 * </tr>
 * <tr>
 * <td>dragButton</td>
 * <td>The drag button in the bottom right corner. Only needed when
 * hasDragButton is true.</td>
 * </tr>
 * </table>
 *
 * <br>
 * For the remaining theme parameters look at {@link
 * #applyThemeScrollPane(de.matthiasmann.twl.ThemeInfo) }
 *
 * @author Matthias Mann
 */
public class ScrollPane extends Widget {

  public static final StateKey STATE_DOWNARROW_ARMED = StateKey.get("downArrowArmed");
  public static final StateKey STATE_RIGHTARROW_ARMED = StateKey.get("rightArrowArmed");
  public static final StateKey STATE_HORIZONTAL_SCROLLBAR_VISIBLE =
      StateKey.get("horizontalScrollbarVisible");
  public static final StateKey STATE_VERTICAL_SCROLLBAR_VISIBLE =
      StateKey.get("verticalScrollbarVisible");
  public static final StateKey STATE_AUTO_SCROLL_UP = StateKey.get("autoScrollUp");
  public static final StateKey STATE_AUTO_SCROLL_DOWN = StateKey.get("autoScrollDown");

  /** Controls which axis of the scroll pane should be fixed */
  public enum Fixed {
    /** No axis is fixed - the scroll pane may show 2 scroll bars */
    NONE,
    /** The horizontal axis is fixed - only a vertical scroll bar may be shown */
    HORIZONTAL,
    /** The vertical axis is fixed - only a horizontal scroll bar may be shown */
    VERTICAL
  }

  /**
   * Indicates that the content handles scrolling itself.
   *
   * <p>This interfaces also allows for a larger scrollable size then the Widget size limitations.
   *
   * <p>The {@code ScrollPane} will set the size of content to the available content area.
   */
  public interface Scrollable {
    /**
     * Called when the content is scrolled either by a call to {@link
     * ScrollPane#setScrollPositionX(int) }, {@link ScrollPane#setScrollPositionY(int) } or through
     * one of the scrollbars.
     *
     * @param scrollPosX the new horizontal scroll position. Always &gt;= 0.
     * @param scrollPosY the new vertical scroll position. Always &gt;= 0.
     */
    public void setScrollPosition(int scrollPosX, int scrollPosY);
  }

  /** Custom auto scroll area checking. This is needed when the content has column headers. */
  public interface AutoScrollable {
    /**
     * Returns the auto scroll direction for the specified mouse event.
     *
     * @param evt the mouse event which could trigger an auto scroll
     * @param autoScrollArea the size of the auto scroll area. This is a theme parameter of the
     *     {@link ScrollPane}
     * @return the auto scroll direction. -1 for upwards 0 for no auto scrolling +1 for downwards
     * @see ScrollPane#checkAutoScroll(de.matthiasmann.twl.Event)
     */
    public int getAutoScrollDirection(Event evt, int autoScrollArea);
  }

  /**
   * Custom page sizes for page scrolling and scroll bar thumb sizing. This is needed when the
   * content has column or row headers.
   */
  public interface CustomPageSize {
    /**
     * Computes the horizontal page size based on the available width.
     *
     * @param availableWidth the available width (the visible area)
     * @return the page size. Must be &gt; 0 and &lt;= availableWidth
     */
    public int getPageSizeX(int availableWidth);

    /**
     * Computes the vertical page size based on the available height.
     *
     * @param availableHeight the available height (the visible area)
     * @return the page size. Must be &gt; 0 and &lt;= availableHeight
     */
    public int getPageSizeY(int availableHeight);
  }

  private static final int AUTO_SCROLL_DELAY = 50;

  final Scrollbar scrollbarH;
  final Scrollbar scrollbarV;
  private final Widget contentArea;
  private DraggableButton dragButton;
  private Widget content;
  private Fixed fixed = Fixed.NONE;
  private Dimension hscrollbarOffset = Dimension.ZERO;
  private Dimension vscrollbarOffset = Dimension.ZERO;
  private Dimension contentScrollbarSpacing = Dimension.ZERO;
  private boolean inLayout;
  private boolean expandContentSize;
  private boolean scrollbarsAlwaysVisible;
  private int scrollbarsToggleFlags;
  private int autoScrollArea;
  private int autoScrollSpeed;
  private Timer autoScrollTimer;
  private int autoScrollDirection;

  public ScrollPane() {
    this(null);
  }

  @SuppressWarnings("OverridableMethodCallInConstructor")
  public ScrollPane(Widget content) {
    this.scrollbarH = new Scrollbar(Scrollbar.Orientation.HORIZONTAL);
    this.scrollbarV = new Scrollbar(Scrollbar.Orientation.VERTICAL);
    this.contentArea = new Widget();

    Runnable cb =
        new Runnable() {
          public void run() {
            scrollContent();
          }
        };

    scrollbarH.addCallback(cb);
    scrollbarH.setVisible(false);
    scrollbarV.addCallback(cb);
    scrollbarV.setVisible(false);
    contentArea.setClip(true);
    contentArea.setTheme("");

    super.insertChild(contentArea, 0);
    super.insertChild(scrollbarH, 1);
    super.insertChild(scrollbarV, 2);
    setContent(content);
    setCanAcceptKeyboardFocus(true);
  }

  public Fixed getFixed() {
    return fixed;
  }

  /**
   * Controls if this scroll pane has a fixed axis which will not show a scrollbar.
   *
   * <p>Default is {@link Fixed#NONE}
   *
   * @param fixed the fixed axis.
   */
  public void setFixed(Fixed fixed) {
    if (fixed == null) {
      throw new NullPointerException("fixed");
    }
    if (this.fixed != fixed) {
      this.fixed = fixed;
      invalidateLayout();
    }
  }

  public Widget getContent() {
    return content;
  }

  /**
   * Sets the widget which should be scrolled.
   *
   * <p>The following interfaces change the behavior of the scroll pane when they are implemented by
   * the content:
   *
   * <ul>
   *   <li>{@link Scrollable}
   *   <li>{@link AutoScrollable}
   *   <li>{@link CustomPageSize}
   * </ul>
   *
   * @param content the new scroll pane content
   */
  public void setContent(Widget content) {
    if (this.content != null) {
      contentArea.removeAllChildren();
      this.content = null;
    }
    if (content != null) {
      this.content = content;
      contentArea.add(content);
    }
  }

  public boolean isExpandContentSize() {
    return expandContentSize;
  }

  /**
   * Control if the content size.
   *
   * <p>If set to true then the content size will be the larger of it's preferred size and the size
   * of the content area. If set to false then the content size will be it's preferred area.
   *
   * <p>Default is false
   *
   * @param expandContentSize true if the content should always cover the content area
   */
  public void setExpandContentSize(boolean expandContentSize) {
    if (this.expandContentSize != expandContentSize) {
      this.expandContentSize = expandContentSize;
      invalidateLayoutLocally();
    }
  }

  /**
   * Forces a layout of the scroll pane content to update the ranges of the scroll bars.
   *
   * <p>This method should be called after changes to the content which might affect it's size and
   * before computing a new scroll position.
   *
   * @see #scrollToAreaX(int, int, int)
   * @see #scrollToAreaY(int, int, int)
   */
  public void updateScrollbarSizes() {
    invalidateLayoutLocally();
    validateLayout();
  }

  public int getScrollPositionX() {
    return scrollbarH.getValue();
  }

  public int getMaxScrollPosX() {
    return scrollbarH.getMaxValue();
  }

  public void setScrollPositionX(int pos) {
    scrollbarH.setValue(pos);
  }

  /**
   * Tries to make the specified horizontal area completely visible. If it is larger then the
   * horizontal page size then it scrolls to the start of the area.
   *
   * @param start the position of the area
   * @param size size of the area
   * @param extra the extra space which should be visible around the area
   * @see Scrollbar#scrollToArea(int, int, int)
   */
  public void scrollToAreaX(int start, int size, int extra) {
    scrollbarH.scrollToArea(start, size, extra);
  }

  public int getScrollPositionY() {
    return scrollbarV.getValue();
  }

  public int getMaxScrollPosY() {
    return scrollbarV.getMaxValue();
  }

  public void setScrollPositionY(int pos) {
    scrollbarV.setValue(pos);
  }

  /**
   * Tries to make the specified vertical area completely visible. If it is larger then the vertical
   * page size then it scrolls to the start of the area.
   *
   * @param start the position of the area
   * @param size size of the area
   * @param extra the extra space which should be visible around the area
   * @see Scrollbar#scrollToArea(int, int, int)
   */
  public void scrollToAreaY(int start, int size, int extra) {
    scrollbarV.scrollToArea(start, size, extra);
  }

  public int getContentAreaWidth() {
    return contentArea.getWidth();
  }

  public int getContentAreaHeight() {
    return contentArea.getHeight();
  }

  /**
   * Returns the horizontal scrollbar widget, be very careful with changes to it.
   *
   * @return the horizontal scrollbar
   */
  public Scrollbar getHorizontalScrollbar() {
    return scrollbarH;
  }

  /**
   * Returns the vertical scrollbar widget, be very careful with changes to it.
   *
   * @return the vertical scrollbar
   */
  public Scrollbar getVerticalScrollbar() {
    return scrollbarV;
  }

  /**
   * Creates a DragListener which can be used to drag the content of this ScrollPane around.
   *
   * @return a DragListener to scroll this this ScrollPane.
   */
  public DraggableButton.DragListener createDragListener() {
    return new DraggableButton.DragListener() {
      int startScrollX;
      int startScrollY;

      public void dragStarted() {
        startScrollX = getScrollPositionX();
        startScrollY = getScrollPositionY();
      }

      public void dragged(int deltaX, int deltaY) {
        setScrollPositionX(startScrollX - deltaX);
        setScrollPositionY(startScrollY - deltaY);
      }

      public void dragStopped() {}
    };
  }

  /**
   * Checks for an auto scroll event. This should be called when a drag & drop operation is in
   * progress and the drop target is inside a scroll pane.
   *
   * @param evt the mouse event which should be checked.
   * @return true if auto scrolling is started/active.
   * @see #stopAutoScroll()
   */
  public boolean checkAutoScroll(Event evt) {
    GUI gui = getGUI();
    if (gui == null) {
      stopAutoScroll();
      return false;
    }

    autoScrollDirection = getAutoScrollDirection(evt);
    if (autoScrollDirection == 0) {
      stopAutoScroll();
      return false;
    }

    setAutoScrollMarker();

    if (autoScrollTimer == null) {
      autoScrollTimer = gui.createTimer();
      autoScrollTimer.setContinuous(true);
      autoScrollTimer.setDelay(AUTO_SCROLL_DELAY);
      autoScrollTimer.setCallback(
          new Runnable() {
            public void run() {
              doAutoScroll();
            }
          });
      doAutoScroll();
    }
    autoScrollTimer.start();
    return true;
  }

  /**
   * Stops an activate auto scroll. This must be called when the drag & drop operation is finished.
   *
   * @see #checkAutoScroll(de.matthiasmann.twl.Event)
   */
  public void stopAutoScroll() {
    if (autoScrollTimer != null) {
      autoScrollTimer.stop();
    }
    autoScrollDirection = 0;
    setAutoScrollMarker();
  }

  /**
   * Returns the ScrollPane instance which has the specified widget as content.
   *
   * @param widget the widget to retrieve the containing ScrollPane for.
   * @return the ScrollPane or null if that widget is not directly in a ScrollPane.
   * @see #setContent(de.matthiasmann.twl.Widget)
   */
  public static ScrollPane getContainingScrollPane(Widget widget) {
    Widget ca = widget.getParent();
    if (ca != null) {
      Widget sp = ca.getParent();
      if (sp instanceof ScrollPane) {
        ScrollPane scrollPane = (ScrollPane) sp;
        assert scrollPane.getContent() == widget;
        return scrollPane;
      }
    }
    return null;
  }

  @Override
  public int getMinWidth() {
    int minWidth = super.getMinWidth();
    int border = getBorderHorizontal();
    // minWidth = Math.max(minWidth, scrollbarH.getMinWidth() + border);
    if (fixed == Fixed.HORIZONTAL && content != null) {
      int sbWidth = scrollbarV.isVisible() ? scrollbarV.getMinWidth() : 0;
      minWidth = Math.max(minWidth, content.getMinWidth() + border + sbWidth);
    }
    return minWidth;
  }

  @Override
  public int getMinHeight() {
    int minHeight = super.getMinHeight();
    int border = getBorderVertical();
    // minHeight = Math.max(minHeight, scrollbarV.getMinHeight() + border);
    if (fixed == Fixed.VERTICAL && content != null) {
      int sbHeight = scrollbarH.isVisible() ? scrollbarH.getMinHeight() : 0;
      minHeight = Math.max(minHeight, content.getMinHeight() + border + sbHeight);
    }
    return minHeight;
  }

  @Override
  public int getPreferredInnerWidth() {
    if (content != null) {
      switch (fixed) {
        case HORIZONTAL:
          int prefWidth =
              computeSize(
                  content.getMinWidth(), content.getPreferredWidth(), content.getMaxWidth());
          if (scrollbarV.isVisible()) {
            prefWidth += scrollbarV.getPreferredWidth();
          }
          return prefWidth;
        case VERTICAL:
          return content.getPreferredWidth();
      }
    }
    return 0;
  }

  @Override
  public int getPreferredInnerHeight() {
    if (content != null) {
      switch (fixed) {
        case HORIZONTAL:
          return content.getPreferredHeight();
        case VERTICAL:
          int prefHeight =
              computeSize(
                  content.getMinHeight(), content.getPreferredHeight(), content.getMaxHeight());
          if (scrollbarH.isVisible()) {
            prefHeight += scrollbarH.getPreferredHeight();
          }
          return prefHeight;
      }
    }
    return 0;
  }

  @Override
  public void insertChild(Widget child, int index) {
    throw new UnsupportedOperationException("use setContent");
  }

  @Override
  public void removeAllChildren() {
    throw new UnsupportedOperationException("use setContent");
  }

  @Override
  public Widget removeChild(int index) {
    throw new UnsupportedOperationException("use setContent");
  }

  @Override
  protected void applyTheme(ThemeInfo themeInfo) {
    super.applyTheme(themeInfo);
    applyThemeScrollPane(themeInfo);
  }

  /**
   * The following theme parameters are required by the scroll pane:
   *
   * <table>
   * <tr>
   * <th>Parameter name</th>
   * <th>Type</th>
   * <th>Description</th>
   * </tr>
   * <tr>
   * <td>autoScrollArea</td>
   * <td>integer</td>
   * <td>The size of the auto scroll area</td>
   * </tr>
   * <tr>
   * <td>autoScrollSpeed</td>
   * <td>integer</td>
   * <td>The speed in pixels to scroll every 50 ms</td>
   * </tr>
   * <tr>
   * <td>hasDragButton</td>
   * <td>boolean</td>
   * <td>If the dragButton should be shown or not</td>
   * </tr>
   * <tr>
   * <td>scrollbarsAlwaysVisible</td>
   * <td>boolean</td>
   * <td>Show scrollbars always (true) or only when needed (false)</td>
   * </tr>
   * </table>
   *
   * <br>
   * The following optional parameters can be used to change the appearance of the scroll pane:
   *
   * <table>
   * <tr>
   * <th>Parameter name</th>
   * <th>Type</th>
   * <th>Description</th>
   * </tr>
   * <tr>
   * <td>hscrollbarOffset</td>
   * <td>Dimension</td>
   * <td>Moves the horizontal scrollbar but does not change the available area
   * for the scroll content.</td>
   * </tr>
   * <tr>
   * <td>vscrollbarOffset</td>
   * <td>Dimension</td>
   * <td>Moves the vertical scrollbar but does not change the available area
   * for the scroll content.</td>
   * </tr>
   * <tr>
   * <td>contentScrollbarSpacing</td>
   * <td>Dimension</td>
   * <td>An optional spacing between the scrollbar and the content area. This
   * is only applied when the corresponding scrollbar is visible. It should be
   * &gt;= 0.</td>
   * </tr>
   * </table>
   *
   * @param themeInfo the theme info
   */
  protected void applyThemeScrollPane(ThemeInfo themeInfo) {
    autoScrollArea = themeInfo.getParameter("autoScrollArea", 5);
    autoScrollSpeed = themeInfo.getParameter("autoScrollSpeed", autoScrollArea * 2);
    hscrollbarOffset =
        themeInfo.getParameterValue("hscrollbarOffset", false, Dimension.class, Dimension.ZERO);
    vscrollbarOffset =
        themeInfo.getParameterValue("vscrollbarOffset", false, Dimension.class, Dimension.ZERO);
    contentScrollbarSpacing =
        themeInfo.getParameterValue(
            "contentScrollbarSpacing", false, Dimension.class, Dimension.ZERO);
    scrollbarsAlwaysVisible = themeInfo.getParameter("scrollbarsAlwaysVisible", false);

    boolean hasDragButton = themeInfo.getParameter("hasDragButton", false);
    if (hasDragButton && dragButton == null) {
      dragButton = new DraggableButton();
      dragButton.setTheme("dragButton");
      dragButton.setListener(
          new DraggableButton.DragListener() {
            public void dragStarted() {
              scrollbarH.externalDragStart();
              scrollbarV.externalDragStart();
            }

            public void dragged(int deltaX, int deltaY) {
              scrollbarH.externalDragged(deltaX, deltaY);
              scrollbarV.externalDragged(deltaX, deltaY);
            }

            public void dragStopped() {
              scrollbarH.externalDragStopped();
              scrollbarV.externalDragStopped();
            }
          });
      super.insertChild(dragButton, 3);
    } else if (!hasDragButton && dragButton != null) {
      assert super.getChild(3) == dragButton;
      super.removeChild(3);
      dragButton = null;
    }
  }

  protected int getAutoScrollDirection(Event evt) {
    if (content instanceof AutoScrollable) {
      return ((AutoScrollable) content).getAutoScrollDirection(evt, autoScrollArea);
    }
    if (contentArea.isMouseInside(evt)) {
      int mouseY = evt.getMouseY();
      int areaY = contentArea.getY();
      if ((mouseY - areaY) <= autoScrollArea
          || (contentArea.getBottom() - mouseY) <= autoScrollArea) {
        // use a 2nd check to decide direction in case the
        // autoScrollAreas overlap
        if (mouseY < (areaY + contentArea.getHeight() / 2)) {
          return -1;
        } else {
          return +1;
        }
      }
    }
    return 0;
  }

  @Override
  public void validateLayout() {
    if (!inLayout) {
      try {
        inLayout = true;
        if (content != null) {
          content.validateLayout();
        }
        super.validateLayout();
      } finally {
        inLayout = false;
      }
    }
  }

  @Override
  protected void childInvalidateLayout(Widget child) {
    if (child == contentArea) {
      // stop invalidate layout chain here when it comes from contentArea
      invalidateLayoutLocally();
    } else {
      super.childInvalidateLayout(child);
    }
  }

  @Override
  protected void paintWidget(GUI gui) {
    // clear flags - used to detect layout loops
    scrollbarsToggleFlags = 0;
  }

  @Override
  protected void layout() {
    if (content != null) {
      int innerWidth = getInnerWidth();
      int innerHeight = getInnerHeight();
      int availWidth = innerWidth;
      int availHeight = innerHeight;
      innerWidth += vscrollbarOffset.getX();
      innerHeight += hscrollbarOffset.getY();
      int scrollbarHX = hscrollbarOffset.getX();
      int scrollbarHY = innerHeight;
      int scrollbarVX = innerWidth;
      int scrollbarVY = vscrollbarOffset.getY();
      int requiredWidth;
      int requiredHeight;
      boolean repeat;
      boolean visibleH = false;
      boolean visibleV = false;

      switch (fixed) {
        case HORIZONTAL:
          requiredWidth = availWidth;
          requiredHeight = content.getPreferredHeight();
          break;
        case VERTICAL:
          requiredWidth = content.getPreferredWidth();
          requiredHeight = availHeight;
          break;
        default:
          requiredWidth = content.getPreferredWidth();
          requiredHeight = content.getPreferredHeight();
          break;
      }

      // System.out.println("required="+requiredWidth+","+requiredHeight+"
      // avail="+availWidth+","+availHeight);

      int hScrollbarMax = 0;
      int vScrollbarMax = 0;

      // don't add scrollbars if we have zero size
      if (availWidth > 0 && availHeight > 0) {
        do {
          repeat = false;

          if (fixed != Fixed.HORIZONTAL) {
            hScrollbarMax = Math.max(0, requiredWidth - availWidth);
            if (hScrollbarMax > 0
                || scrollbarsAlwaysVisible
                || ((scrollbarsToggleFlags & 3) == 3)) {
              repeat |= !visibleH;
              visibleH = true;
              int prefHeight = scrollbarH.getPreferredHeight();
              scrollbarHY = innerHeight - prefHeight;
              availHeight = Math.max(0, scrollbarHY - contentScrollbarSpacing.getY());
            }
          } else {
            hScrollbarMax = 0;
            requiredWidth = availWidth;
          }

          if (fixed != Fixed.VERTICAL) {
            vScrollbarMax = Math.max(0, requiredHeight - availHeight);
            if (vScrollbarMax > 0
                || scrollbarsAlwaysVisible
                || ((scrollbarsToggleFlags & 12) == 12)) {
              repeat |= !visibleV;
              visibleV = true;
              int prefWidth = scrollbarV.getPreferredWidth();
              scrollbarVX = innerWidth - prefWidth;
              availWidth = Math.max(0, scrollbarVX - contentScrollbarSpacing.getX());
            }
          } else {
            vScrollbarMax = 0;
            requiredHeight = availHeight;
          }
        } while (repeat);
      }

      // if a scrollbar visibility state has changed set it's flag to
      // detect layout loops
      if (visibleH && !scrollbarH.isVisible()) {
        scrollbarsToggleFlags |= 1;
      }
      if (!visibleH && scrollbarH.isVisible()) {
        scrollbarsToggleFlags |= 2;
      }
      if (visibleV && !scrollbarV.isVisible()) {
        scrollbarsToggleFlags |= 4;
      }
      if (!visibleV && scrollbarV.isVisible()) {
        scrollbarsToggleFlags |= 8;
      }

      boolean changedH = visibleH ^ scrollbarH.isVisible();
      boolean changedV = visibleV ^ scrollbarV.isVisible();
      if (changedH || changedV) {
        if ((changedH && fixed == Fixed.VERTICAL) || (changedV && fixed == Fixed.HORIZONTAL)) {
          invalidateLayout();
        } else {
          invalidateLayoutLocally();
        }
      }

      int pageSizeX, pageSizeY;
      if (content instanceof CustomPageSize) {
        CustomPageSize customPageSize = (CustomPageSize) content;
        pageSizeX = customPageSize.getPageSizeX(availWidth);
        pageSizeY = customPageSize.getPageSizeY(availHeight);
      } else {
        pageSizeX = availWidth;
        pageSizeY = availHeight;
      }

      scrollbarH.setVisible(visibleH);
      scrollbarH.setMinMaxValue(0, hScrollbarMax);
      scrollbarH.setSize(
          Math.max(0, scrollbarVX - scrollbarHX), Math.max(0, innerHeight - scrollbarHY));
      scrollbarH.setPosition(getInnerX() + scrollbarHX, getInnerY() + scrollbarHY);
      scrollbarH.setPageSize(Math.max(1, pageSizeX));
      scrollbarH.setStepSize(Math.max(1, pageSizeX / 10));

      scrollbarV.setVisible(visibleV);
      scrollbarV.setMinMaxValue(0, vScrollbarMax);
      scrollbarV.setSize(
          Math.max(0, innerWidth - scrollbarVX), Math.max(0, scrollbarHY - scrollbarVY));
      scrollbarV.setPosition(getInnerX() + scrollbarVX, getInnerY() + scrollbarVY);
      scrollbarV.setPageSize(Math.max(1, pageSizeY));
      scrollbarV.setStepSize(Math.max(1, pageSizeY / 10));

      if (dragButton != null) {
        dragButton.setVisible(visibleH && visibleV);
        dragButton.setSize(
            Math.max(0, innerWidth - scrollbarVX), Math.max(0, innerHeight - scrollbarHY));
        dragButton.setPosition(getInnerX() + scrollbarVX, getInnerY() + scrollbarHY);
      }

      contentArea.setPosition(getInnerX(), getInnerY());
      contentArea.setSize(availWidth, availHeight);
      if (content instanceof Scrollable) {
        content.setPosition(contentArea.getX(), contentArea.getY());
        content.setSize(availWidth, availHeight);
      } else if (expandContentSize) {
        content.setSize(Math.max(availWidth, requiredWidth), Math.max(availHeight, requiredHeight));
      } else {
        content.setSize(Math.max(0, requiredWidth), Math.max(0, requiredHeight));
      }

      AnimationState animationState = getAnimationState();
      animationState.setAnimationState(STATE_HORIZONTAL_SCROLLBAR_VISIBLE, visibleH);
      animationState.setAnimationState(STATE_VERTICAL_SCROLLBAR_VISIBLE, visibleV);

      scrollContent();
    } else {
      scrollbarH.setVisible(false);
      scrollbarV.setVisible(false);
    }
  }

  @Override
  protected boolean handleEvent(Event evt) {
    if (evt.isKeyEvent() && content != null && content.canAcceptKeyboardFocus()) {
      if (content.handleEvent(evt)) {
        content.requestKeyboardFocus();
        return true;
      }
    }
    if (super.handleEvent(evt)) {
      return true;
    }
    switch (evt.getType()) {
      case KEY_PRESSED:
      case KEY_RELEASED:
        {
          int keyCode = evt.getKeyCode();
          if (keyCode == Event.KEY_LEFT || keyCode == Event.KEY_RIGHT) {
            return scrollbarH.handleEvent(evt);
          }
          if (keyCode == Event.KEY_UP
              || keyCode == Event.KEY_DOWN
              || keyCode == Event.KEY_PRIOR
              || keyCode == Event.KEY_NEXT) {
            return scrollbarV.handleEvent(evt);
          }
          break;
        }
      case MOUSE_WHEEL:
        if (scrollbarV.isVisible()) {
          return scrollbarV.handleEvent(evt);
        }
        return false;
    }
    return evt.isMouseEvent() && contentArea.isMouseInside(evt);
  }

  @Override
  protected void paint(GUI gui) {
    if (dragButton != null) {
      AnimationState as = dragButton.getAnimationState();
      as.setAnimationState(STATE_DOWNARROW_ARMED, scrollbarV.isDownRightButtonArmed());
      as.setAnimationState(STATE_RIGHTARROW_ARMED, scrollbarH.isDownRightButtonArmed());
    }
    super.paint(gui);
  }

  void scrollContent() {
    if (content instanceof Scrollable) {
      Scrollable scrollable = (Scrollable) content;
      scrollable.setScrollPosition(scrollbarH.getValue(), scrollbarV.getValue());
    } else {
      content.setPosition(
          contentArea.getX() - scrollbarH.getValue(), contentArea.getY() - scrollbarV.getValue());
    }
  }

  void setAutoScrollMarker() {
    int scrollPos = scrollbarV.getValue();
    AnimationState animationState = getAnimationState();
    animationState.setAnimationState(
        STATE_AUTO_SCROLL_UP, autoScrollDirection < 0 && scrollPos > 0);
    animationState.setAnimationState(
        STATE_AUTO_SCROLL_DOWN, autoScrollDirection > 0 && scrollPos < scrollbarV.getMaxValue());
  }

  void doAutoScroll() {
    scrollbarV.setValue(scrollbarV.getValue() + autoScrollDirection * autoScrollSpeed);
    setAutoScrollMarker();
  }
}
Пример #2
0
/** @author Matthias Mann */
public class ItemSlot extends Widget {

  public static final StateKey STATE_DRAG_ACTIVE = StateKey.get("dragActive");
  public static final StateKey STATE_DROP_OK = StateKey.get("dropOk");
  public static final StateKey STATE_DROP_BLOCKED = StateKey.get("dropBlocked");

  public interface DragListener {
    public void dragStarted(ItemSlot slot, Event evt);

    public void dragging(ItemSlot slot, Event evt);

    public void dragStopped(ItemSlot slot, Event evt);
  }

  private String item;
  private Image icon;
  private DragListener listener;
  private boolean dragActive;
  private ParameterMap icons;
  private GunMode gun_mode;

  public ItemSlot() {}

  public String getItem() {
    return item;
  }

  public void setItem(String item) {
    this.item = item;
    findIcon();
  }

  public void setItemAndGunMode(String item, GunMode mode) {
    this.item = item;
    this.gun_mode = mode;
    findIcon();
  }

  public GunMode getGunMode() {
    return gun_mode;
  }

  public boolean canDrop() {
    return item == null;
  }

  public Image getIcon() {
    return icon;
  }

  public DragListener getListener() {
    return listener;
  }

  public void setListener(DragListener listener) {
    this.listener = listener;
  }

  public void setDropState(boolean drop, boolean ok) {
    AnimationState as = getAnimationState();
    as.setAnimationState(STATE_DROP_OK, drop && ok);
    as.setAnimationState(STATE_DROP_BLOCKED, drop && !ok);
  }

  @Override
  protected boolean handleEvent(Event evt) {
    if (evt.isMouseEventNoWheel()) {
      if (dragActive) {
        if (evt.isMouseDragEnd()) {
          if (listener != null) {
            listener.dragStopped(this, evt);
          }
          dragActive = false;
          getAnimationState().setAnimationState(STATE_DRAG_ACTIVE, false);
        } else if (listener != null) {
          listener.dragging(this, evt);
        }
      } else if (evt.isMouseDragEvent()) {
        dragActive = true;
        getAnimationState().setAnimationState(STATE_DRAG_ACTIVE, true);
        if (listener != null) {
          listener.dragStarted(this, evt);
        }
      }
      return true;
    }

    return super.handleEvent(evt);
  }

  @Override
  protected void paintWidget(GUI gui) {
    if (!dragActive && icon != null) {
      icon.draw(getAnimationState(), getInnerX(), getInnerY(), getInnerWidth(), getInnerHeight());
    }
  }

  @Override
  protected void paintDragOverlay(GUI gui, int mouseX, int mouseY, int modifier) {
    if (icon != null) {
      final int innerWidth = getInnerWidth();
      final int innerHeight = getInnerHeight();
      icon.draw(
          getAnimationState(),
          mouseX - innerWidth / 2,
          mouseY - innerHeight / 2,
          innerWidth,
          innerHeight);
    }
  }

  @Override
  protected void applyTheme(ThemeInfo themeInfo) {
    super.applyTheme(themeInfo);
    icons = themeInfo.getParameterMap("icons");
    findIcon();
  }

  private void findIcon() {
    if (item == null || icons == null) {
      icon = null;
    } else {
      icon = icons.getImage(item);
    }
  }

  public String toString() {
    return "Item slot: " + item + ", " + gun_mode;
  }
}
Пример #3
0
/**
 * A renderer using only GL11 features.
 *
 * <p>For correct rendering the OpenGL viewport size must be synchronized.
 *
 * @author Matthias Mann
 * @see #syncViewportSize()
 */
public class LWJGLRenderer implements Renderer, LineRenderer {

  public static final StateKey STATE_LEFT_MOUSE_BUTTON = StateKey.get("leftMouseButton");
  public static final StateKey STATE_MIDDLE_MOUSE_BUTTON = StateKey.get("middleMouseButton");
  public static final StateKey STATE_RIGHT_MOUSE_BUTTON = StateKey.get("rightMouseButton");

  public static final FontParameter.Parameter<Integer> FONTPARAM_OFFSET_X =
      FontParameter.newParameter("offsetX", 0);
  public static final FontParameter.Parameter<Integer> FONTPARAM_OFFSET_Y =
      FontParameter.newParameter("offsetY", 0);
  public static final FontParameter.Parameter<Integer> FONTPARAM_UNDERLINE_OFFSET =
      FontParameter.newParameter("underlineOffset", 0);

  private final IntBuffer ib16;
  final int maxTextureSize;

  private int viewportX;
  private int viewportBottom;
  private int width;
  private int height;
  private boolean hasScissor;
  private final TintStack tintStateRoot;
  private final Cursor emptyCursor;
  private boolean useQuadsForLines;
  private boolean useSWMouseCursors;
  private SWCursor swCursor;
  private int mouseX;
  private int mouseY;
  private LWJGLCacheContext cacheContext;
  private FontMapper fontMapper;

  final SWCursorAnimState swCursorAnimState;
  final ArrayList<TextureArea> textureAreas;
  final ArrayList<TextureAreaRotated> rotatedTextureAreas;
  final ArrayList<LWJGLDynamicImage> dynamicImages;

  protected TintStack tintStack;
  protected final ClipStack clipStack;
  protected final Rect clipRectTemp;

  @SuppressWarnings("OverridableMethodCallInConstructor")
  public LWJGLRenderer() throws LWJGLException {
    this.ib16 = BufferUtils.createIntBuffer(16);
    this.textureAreas = new ArrayList<TextureArea>();
    this.rotatedTextureAreas = new ArrayList<TextureAreaRotated>();
    this.dynamicImages = new ArrayList<LWJGLDynamicImage>();
    this.tintStateRoot = new TintStack();
    this.tintStack = tintStateRoot;
    this.clipStack = new ClipStack();
    this.clipRectTemp = new Rect();
    syncViewportSize();

    GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE, ib16);
    maxTextureSize = ib16.get(0);

    if (Mouse.isCreated()) {
      int minCursorSize = Cursor.getMinCursorSize();
      IntBuffer tmp = BufferUtils.createIntBuffer(minCursorSize * minCursorSize);
      emptyCursor =
          new Cursor(
              minCursorSize, minCursorSize, minCursorSize / 2, minCursorSize / 2, 1, tmp, null);
    } else {
      emptyCursor = null;
    }

    swCursorAnimState = new SWCursorAnimState();
  }

  public boolean isUseQuadsForLines() {
    return useQuadsForLines;
  }

  public void setUseQuadsForLines(boolean useQuadsForLines) {
    this.useQuadsForLines = useQuadsForLines;
  }

  public boolean isUseSWMouseCursors() {
    return useSWMouseCursors;
  }

  /**
   * Controls if the mouse cursor is rendered via SW or HW cursors. HW cursors have reduced support
   * for transparency and cursor size.
   *
   * <p>This must be set before loading a theme !
   *
   * @param useSWMouseCursors
   */
  public void setUseSWMouseCursors(boolean useSWMouseCursors) {
    this.useSWMouseCursors = useSWMouseCursors;
  }

  public CacheContext createNewCacheContext() {
    return new LWJGLCacheContext(this);
  }

  private LWJGLCacheContext activeCacheContext() {
    if (cacheContext == null) {
      setActiveCacheContext(createNewCacheContext());
    }
    return cacheContext;
  }

  public CacheContext getActiveCacheContext() {
    return activeCacheContext();
  }

  public void setActiveCacheContext(CacheContext cc) throws IllegalStateException {
    if (cc == null) {
      throw new NullPointerException();
    }
    if (!cc.isValid()) {
      throw new IllegalStateException("CacheContext is invalid");
    }
    if (!(cc instanceof LWJGLCacheContext)) {
      throw new IllegalArgumentException("CacheContext object not from this renderer");
    }
    LWJGLCacheContext lwjglCC = (LWJGLCacheContext) cc;
    if (lwjglCC.renderer != this) {
      throw new IllegalArgumentException("CacheContext object not from this renderer");
    }
    this.cacheContext = lwjglCC;
    try {
      for (TextureArea ta : textureAreas) {
        ta.destroyRepeatCache();
      }
      for (TextureAreaRotated tar : rotatedTextureAreas) {
        tar.destroyRepeatCache();
      }
    } finally {
      textureAreas.clear();
      rotatedTextureAreas.clear();
    }
  }

  /**
   * Queries the current view port size & position and updates all related internal state.
   *
   * <p>It is important that the internal state matches the OpenGL viewport or clipping won't work
   * correctly.
   *
   * <p>This method should only be called when the viewport size has changed. It can have negative
   * impact on performance to call every frame.
   *
   * @see #getWidth()
   * @see #getHeight()
   */
  public void syncViewportSize() {
    ib16.clear();
    GL11.glGetInteger(GL11.GL_VIEWPORT, ib16);
    viewportX = ib16.get(0);
    width = ib16.get(2);
    height = ib16.get(3);
    viewportBottom = ib16.get(1) + height;
  }

  /**
   * Sets the viewport size & position.
   *
   * <p>This method is preferred over {@link #syncViewportSize() } as it avoids calling {@link
   * GL11#glGetInteger(int, java.nio.IntBuffer) }.
   *
   * @param x the X position (GL_VIEWPORT index 0)
   * @param y the Y position (GL_VIEWPORT index 1)
   * @param width the width (GL_VIEWPORT index 2)
   * @param height the height (GL_VIEWPORT index 3)
   */
  public void setViewport(int x, int y, int width, int height) {
    this.viewportX = x;
    this.viewportBottom = y + height;
    this.width = width;
    this.height = height;
  }

  public long getTimeMillis() {
    long res = Sys.getTimerResolution();
    long time = Sys.getTime();
    if (res != 1000) {
      time = (time * 1000) / res;
    }
    return time;
  }

  protected void setupGLState() {
    GL11.glPushAttrib(
        GL11.GL_ENABLE_BIT
            | GL11.GL_TRANSFORM_BIT
            | GL11.GL_HINT_BIT
            | GL11.GL_COLOR_BUFFER_BIT
            | GL11.GL_SCISSOR_BIT
            | GL11.GL_LINE_BIT
            | GL11.GL_TEXTURE_BIT);
    GL11.glMatrixMode(GL11.GL_PROJECTION);
    GL11.glPushMatrix();
    GL11.glLoadIdentity();
    GL11.glOrtho(0, width, height, 0, -1.0, 1.0);
    GL11.glMatrixMode(GL11.GL_MODELVIEW);
    GL11.glPushMatrix();
    GL11.glLoadIdentity();
    GL11.glEnable(GL11.GL_TEXTURE_2D);
    GL11.glEnable(GL11.GL_BLEND);
    GL11.glEnable(GL11.GL_LINE_SMOOTH);
    GL11.glDisable(GL11.GL_DEPTH_TEST);
    GL11.glDisable(GL11.GL_LIGHTING);
    GL11.glDisable(GL11.GL_SCISSOR_TEST);
    GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
    GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST);
  }

  protected void revertGLState() {
    GL11.glPopMatrix();
    GL11.glMatrixMode(GL11.GL_PROJECTION);
    GL11.glPopMatrix();
    GL11.glPopAttrib();
  }

  /** Setup GL to start rendering the GUI. It assumes default GL state. */
  public boolean startRendering() {
    if (width <= 0 || height <= 0) {
      return false;
    }

    prepareForRendering();
    setupGLState();
    RenderScale.doscale();
    return true;
  }

  public void endRendering() {
    renderSWCursor();
    RenderScale.descale();
    revertGLState();
  }

  /**
   * Call to revert the GL state to the state before calling {@link #startRendering()}.
   *
   * @see #resumeRendering()
   */
  public void pauseRendering() {
    RenderScale.descale();
    revertGLState();
  }

  /** Resume rendering after a call to {@link #pauseRendering()}. */
  public void resumeRendering() {
    hasScissor = false;
    setupGLState();
    RenderScale.doscale();
    setClipRect();
  }

  public int getHeight() {
    return height;
  }

  public int getWidth() {
    return width;
  }

  /**
   * Retrieves the X position of the OpenGL viewport (index 0 of GL_VIEWPORT)
   *
   * @return the X position of the OpenGL viewport
   */
  public int getViewportX() {
    return viewportX;
  }

  /**
   * Retrieves the Y position of the OpenGL viewport (index 1 of GL_VIEWPORT)
   *
   * @return the Y position of the OpenGL viewport
   */
  public int getViewportY() {
    return viewportBottom - height;
  }

  public Font loadFont(URL url, StateSelect select, FontParameter... parameterList)
      throws IOException {
    if (url == null) {
      throw new NullPointerException("url");
    }
    if (select == null) {
      throw new NullPointerException("select");
    }
    if (parameterList == null) {
      throw new NullPointerException("parameterList");
    }
    if (select.getNumExpressions() + 1 != parameterList.length) {
      throw new IllegalArgumentException("select.getNumExpressions() + 1 != parameterList.length");
    }
    BitmapFont bmFont = activeCacheContext().loadBitmapFont(url);
    return new LWJGLFont(this, bmFont, select, parameterList);
  }

  public Texture loadTexture(URL url, String formatStr, String filterStr) throws IOException {
    LWJGLTexture.Format format = LWJGLTexture.Format.COLOR;
    LWJGLTexture.Filter filter = LWJGLTexture.Filter.NEAREST;
    if (formatStr != null) {
      try {
        format = LWJGLTexture.Format.valueOf(formatStr.toUpperCase(Locale.ENGLISH));
      } catch (IllegalArgumentException ex) {
        getLogger().log(Level.WARNING, "Unknown texture format: {0}", formatStr);
      }
    }
    if (filterStr != null) {
      try {
        filter = LWJGLTexture.Filter.valueOf(filterStr.toUpperCase(Locale.ENGLISH));
      } catch (IllegalArgumentException ex) {
        getLogger().log(Level.WARNING, "Unknown texture filter: {0}", filterStr);
      }
    }
    return load(url, format, filter);
  }

  public LineRenderer getLineRenderer() {
    return this;
  }

  public OffscreenRenderer getOffscreenRenderer() {
    return null;
  }

  public FontMapper getFontMapper() {
    return fontMapper;
  }

  /**
   * Installs a font mapper. It is the responsibility of the font mapper to manage the OpenGL state
   * correctly so that normal rendering by LWJGLRenderer is not disturbed.
   *
   * @param fontMapper the font mapper object - can be null.
   */
  public void setFontMapper(FontMapper fontMapper) {
    this.fontMapper = fontMapper;
  }

  public DynamicImage createDynamicImage(int width, int height) {
    if (width <= 0) {
      throw new IllegalArgumentException("width");
    }
    if (height <= 0) {
      throw new IllegalArgumentException("height");
    }
    if (width > maxTextureSize || height > maxTextureSize) {
      getLogger()
          .log(
              Level.WARNING,
              "requested size {0} x {1} exceeds maximum texture size {3}",
              new Object[] {width, height, maxTextureSize});
      return null;
    }

    int texWidth = width;
    int texHeight = height;

    ContextCapabilities caps = GLContext.getCapabilities();
    boolean useTextureRectangle = caps.GL_EXT_texture_rectangle || caps.GL_ARB_texture_rectangle;

    if (!useTextureRectangle && !caps.GL_ARB_texture_non_power_of_two) {
      texWidth = nextPowerOf2(width);
      texHeight = nextPowerOf2(height);
    }

    // ARB and EXT versions use the same enum !
    int proxyTarget =
        useTextureRectangle
            ? EXTTextureRectangle.GL_PROXY_TEXTURE_RECTANGLE_EXT
            : GL11.GL_PROXY_TEXTURE_2D;

    GL11.glTexImage2D(
        proxyTarget,
        0,
        GL11.GL_RGBA,
        texWidth,
        texHeight,
        0,
        GL11.GL_RGBA,
        GL11.GL_UNSIGNED_BYTE,
        (ByteBuffer) null);
    ib16.clear();
    GL11.glGetTexLevelParameter(proxyTarget, 0, GL11.GL_TEXTURE_WIDTH, ib16);
    if (ib16.get(0) != texWidth) {
      getLogger()
          .log(
              Level.WARNING,
              "requested size {0} x {1} failed proxy texture test",
              new Object[] {texWidth, texHeight});
      return null;
    }

    // ARB and EXT versions use the same enum !
    int target =
        useTextureRectangle ? EXTTextureRectangle.GL_TEXTURE_RECTANGLE_EXT : GL11.GL_TEXTURE_2D;
    int id = GL11.glGenTextures();

    GL11.glBindTexture(target, id);
    GL11.glTexImage2D(
        target,
        0,
        GL11.GL_RGBA,
        texWidth,
        texHeight,
        0,
        GL11.GL_RGBA,
        GL11.GL_UNSIGNED_BYTE,
        (ByteBuffer) null);
    GL11.glTexParameteri(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
    GL11.glTexParameteri(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);

    LWJGLDynamicImage image =
        new LWJGLDynamicImage(this, target, id, width, height, texWidth, texHeight, Color.WHITE);
    dynamicImages.add(image);
    return image;
  }

  public Image createGradient(Gradient gradient) {
    return new GradientImage(this, gradient);
  }

  public void clipEnter(int x, int y, int w, int h) {
    clipStack.push(x, y, w, h);
    setClipRect();
  }

  public void clipEnter(Rect rect) {
    clipStack.push(rect);
    setClipRect();
  }

  public void clipLeave() {
    clipStack.pop();
    setClipRect();
  }

  public boolean clipIsEmpty() {
    return clipStack.isClipEmpty();
  }

  public void setCursor(MouseCursor cursor) {
    try {
      swCursor = null;
      if (isMouseInsideWindow()) {
        if (cursor instanceof LWJGLCursor) {
          setNativeCursor(((LWJGLCursor) cursor).cursor);
        } else if (cursor instanceof SWCursor) {
          setNativeCursor(emptyCursor);
          swCursor = (SWCursor) cursor;
        } else {
          setNativeCursor(null);
        }
      }
    } catch (LWJGLException ex) {
      getLogger().log(Level.WARNING, "Could not set native cursor", ex);
    }
  }

  public void setMousePosition(int mouseX, int mouseY) {
    this.mouseX = mouseX;
    this.mouseY = mouseY;
  }

  public void setMouseButton(int button, boolean state) {
    swCursorAnimState.setAnimationState(button, state);
  }

  public LWJGLTexture load(URL textureUrl, LWJGLTexture.Format fmt, LWJGLTexture.Filter filter)
      throws IOException {
    return load(textureUrl, fmt, filter, null);
  }

  public LWJGLTexture load(
      URL textureUrl,
      LWJGLTexture.Format fmt,
      LWJGLTexture.Filter filter,
      TexturePostProcessing tpp)
      throws IOException {
    if (textureUrl == null) {
      throw new NullPointerException("textureUrl");
    }
    LWJGLCacheContext cc = activeCacheContext();
    if (tpp != null) {
      return cc.createTexture(textureUrl, fmt, filter, tpp);
    } else {
      return cc.loadTexture(textureUrl, fmt, filter);
    }
  }

  public void pushGlobalTintColor(float r, float g, float b, float a) {
    tintStack = tintStack.push(r, g, b, a);
  }

  public void popGlobalTintColor() {
    tintStack = tintStack.pop();
  }

  /**
   * Pushes a white entry on the tint stack which ignores the previous tint color. It must be
   * removed by calling {@link #popGlobalTintColor()}.
   *
   * <p>This is useful when rendering to texture
   */
  public void pushGlobalTintColorReset() {
    tintStack = tintStack.pushReset();
  }

  /**
   * Calls GL11.glColor4f() with the specified color multiplied by the current global tint color.
   *
   * @param color the color to set
   */
  public void setColor(Color color) {
    tintStack.setColor(color);
  }

  public void drawLine(float[] pts, int numPts, float width, Color color, boolean drawAsLoop) {
    if (numPts * 2 > pts.length) {
      throw new ArrayIndexOutOfBoundsException(numPts * 2);
    }
    if (numPts >= 2) {
      tintStack.setColor(color);
      GL11.glDisable(GL11.GL_TEXTURE_2D);
      if (useQuadsForLines) {
        drawLinesAsQuads(numPts, pts, width, drawAsLoop);
      } else {
        drawLinesAsLines(numPts, pts, width, drawAsLoop);
      }
      GL11.glEnable(GL11.GL_TEXTURE_2D);
    }
  }

  private void drawLinesAsLines(int numPts, float[] pts, float width, boolean drawAsLoop) {
    GL11.glLineWidth(width);
    GL11.glBegin(drawAsLoop ? GL11.GL_LINE_LOOP : GL11.GL_LINE_STRIP);
    for (int i = 0; i < numPts; i++) {
      GL11.glVertex2f(pts[i * 2 + 0], pts[i * 2 + 1]);
    }
    GL11.glEnd();
  }

  private void drawLinesAsQuads(int numPts, float[] pts, float width, boolean drawAsLoop) {
    width *= 0.5f;
    GL11.glBegin(GL11.GL_QUADS);
    for (int i = 1; i < numPts; i++) {
      drawLineAsQuad(pts[i * 2 - 2], pts[i * 2 - 1], pts[i * 2 + 0], pts[i * 2 + 1], width);
    }
    if (drawAsLoop) {
      int idx = numPts * 2;
      drawLineAsQuad(pts[idx], pts[idx + 1], pts[0], pts[1], width);
    }
    GL11.glEnd();
  }

  private static void drawLineAsQuad(float x0, float y0, float x1, float y1, float w) {
    float dx = x1 - x0;
    float dy = y1 - y0;
    float l = (float) Math.sqrt(dx * dx + dy * dy) / w;
    dx /= l;
    dy /= l;
    GL11.glVertex2f(x0 - dx + dy, y0 - dy - dx);
    GL11.glVertex2f(x0 - dx - dy, y0 - dy + dx);
    GL11.glVertex2f(x1 + dx - dy, y1 + dy + dx);
    GL11.glVertex2f(x1 + dx + dy, y1 + dy - dx);
  }

  protected void prepareForRendering() {
    hasScissor = false;
    tintStack = tintStateRoot;
    clipStack.clearStack();
  }

  protected void renderSWCursor() {
    if (swCursor != null) {
      tintStack = tintStateRoot;
      swCursor.render(mouseX, mouseY);
    }
  }

  protected void setNativeCursor(Cursor cursor) throws LWJGLException {
    Mouse.setNativeCursor(cursor);
  }

  protected boolean isMouseInsideWindow() {
    return Mouse.isInsideWindow();
  }

  protected void getTintedColor(Color color, float[] result) {
    result[0] = tintStack.r * color.getRed();
    result[1] = tintStack.g * color.getGreen();
    result[2] = tintStack.b * color.getBlue();
    result[3] = tintStack.a * color.getAlpha();
  }

  /**
   * Computes the tinted color from the given color.
   *
   * @param color the input color in RGBA order, value range is 0.0 (black) to 255.0 (white).
   * @param result the tinted color in RGBA order, can be the same array as color.
   */
  protected void getTintedColor(float[] color, float[] result) {
    result[0] = tintStack.r * color[0];
    result[1] = tintStack.g * color[1];
    result[2] = tintStack.b * color[2];
    result[3] = tintStack.a * color[3];
  }

  public void setClipRect() {
    final Rect rect = clipRectTemp;
    if (clipStack.getClipRect(rect)) {
      GL11.glScissor(
          viewportX + rect.getX() * RenderScale.scale,
          viewportBottom - rect.getBottom() * RenderScale.scale,
          rect.getWidth() * RenderScale.scale,
          rect.getHeight() * RenderScale.scale);
      if (!hasScissor) {
        GL11.glEnable(GL11.GL_SCISSOR_TEST);
        hasScissor = true;
      }
    } else if (hasScissor) {
      GL11.glDisable(GL11.GL_SCISSOR_TEST);
      hasScissor = false;
    }
  }

  /**
   * Retrieves the active clip region from the top of the stack
   *
   * @param rect the rect coordinates - may not be updated when clipping is disabled
   * @return true if clipping is active, false if clipping is disabled
   */
  public boolean getClipRect(Rect rect) {
    return clipStack.getClipRect(rect);
  }

  Logger getLogger() {
    return Logger.getLogger(LWJGLRenderer.class.getName());
  }

  /**
   * If the passed value is not a power of 2 then return the next highest power of 2 otherwise the
   * value is returned unchanged.
   *
   * <p>Warren Jr., Henry S. (2002). Hacker's Delight. Addison Wesley. pp. 48. ISBN 978-0201914658
   *
   * @param i a non negative number &lt;= 2^31
   * @return the smallest power of 2 which is &gt;= i
   */
  private static int nextPowerOf2(int i) {
    i--;
    i |= (i >> 1);
    i |= (i >> 2);
    i |= (i >> 4);
    i |= (i >> 8);
    i |= (i >> 16);
    return i + 1;
  }

  private static class SWCursorAnimState implements AnimationState {
    private final long[] lastTime;
    private final boolean[] active;

    SWCursorAnimState() {
      lastTime = new long[3];
      active = new boolean[3];
    }

    void setAnimationState(int idx, boolean isActive) {
      if (idx >= 0 && idx < 3 && active[idx] != isActive) {
        lastTime[idx] = Sys.getTime();
        active[idx] = isActive;
      }
    }

    public int getAnimationTime(StateKey state) {
      long curTime = Sys.getTime();
      int idx = getMouseButton(state);
      if (idx >= 0) {
        curTime -= lastTime[idx];
      }
      return (int) curTime & Integer.MAX_VALUE;
    }

    public boolean getAnimationState(StateKey state) {
      int idx = getMouseButton(state);
      if (idx >= 0) {
        return active[idx];
      }
      return false;
    }

    public boolean getShouldAnimateState(StateKey state) {
      return true;
    }

    private int getMouseButton(StateKey key) {
      if (key == STATE_LEFT_MOUSE_BUTTON) {
        return Event.MOUSE_LBUTTON;
      }
      if (key == STATE_MIDDLE_MOUSE_BUTTON) {
        return Event.MOUSE_MBUTTON;
      }
      if (key == STATE_RIGHT_MOUSE_BUTTON) {
        return Event.MOUSE_RBUTTON;
      }
      return -1;
    }
  }
}