1 /* 2 * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.javafx.scene.control.skin; 27 28 import javafx.geometry.Orientation; 29 import javafx.geometry.Point2D; 30 import javafx.scene.AccessibleAction; 31 import javafx.scene.AccessibleAttribute; 32 import javafx.scene.AccessibleRole; 33 import javafx.scene.control.ScrollBar; 34 import javafx.scene.input.MouseButton; 35 import javafx.scene.input.ScrollEvent; 36 import javafx.scene.layout.Region; 37 import javafx.scene.layout.StackPane; 38 import javafx.scene.Node; 39 import com.sun.javafx.util.Utils; 40 import com.sun.javafx.scene.control.behavior.ScrollBarBehavior; 41 42 public class ScrollBarSkin extends BehaviorSkinBase<ScrollBar, ScrollBarBehavior> { 43 44 /*************************************************************************** 45 * * 46 * UI Subcomponents * 47 * * 48 **************************************************************************/ 49 50 public final static int DEFAULT_LENGTH = 100; 51 public final static int DEFAULT_WIDTH = 20; 52 53 private StackPane thumb; 54 private StackPane trackBackground; 55 private StackPane track; 56 private EndButton incButton; 57 private EndButton decButton; 58 59 private double trackLength; 60 private double thumbLength; 61 62 private double preDragThumbPos; 63 private Point2D dragStart; // in the track's coord system 64 65 private double trackPos; 66 67 /*************************************************************************** 68 * * 69 * Constructors * 70 * * 71 **************************************************************************/ 72 73 public ScrollBarSkin(ScrollBar scrollbar) { 74 super(scrollbar, new ScrollBarBehavior(scrollbar)); 75 initialize(); 76 getSkinnable().requestLayout(); 77 // Register listeners 78 registerChangeListener(scrollbar.minProperty(), "MIN"); 79 registerChangeListener(scrollbar.maxProperty(), "MAX"); 80 registerChangeListener(scrollbar.valueProperty(), "VALUE"); 81 registerChangeListener(scrollbar.orientationProperty(), "ORIENTATION"); 82 registerChangeListener(scrollbar.visibleAmountProperty(), "VISIBLE_AMOUNT"); 83 } 84 85 /** 86 * Initializes the ScrollBarSkin. Creates the scene and sets up all the 87 * bindings for the group. 88 */ 89 private void initialize() { 90 91 track = new StackPane(); 92 track.getStyleClass().setAll("track"); 93 94 trackBackground = new StackPane(); 95 trackBackground.getStyleClass().setAll("track-background"); 96 97 thumb = new StackPane() { 98 @Override 99 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 100 switch (attribute) { 101 case VALUE: return getSkinnable().getValue(); 102 default: return super.queryAccessibleAttribute(attribute, parameters); 103 } 104 } 105 }; 106 thumb.getStyleClass().setAll("thumb"); 107 thumb.setAccessibleRole(AccessibleRole.THUMB); 108 109 110 if (!IS_TOUCH_SUPPORTED) { 111 112 incButton = new EndButton("increment-button", "increment-arrow") { 113 @Override 114 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 115 switch (action) { 116 case FIRE: 117 getSkinnable().increment(); 118 break; 119 default: super.executeAccessibleAction(action, parameters); 120 } 121 } 122 }; 123 incButton.setAccessibleRole(AccessibleRole.INCREMENT_BUTTON); 124 incButton.setOnMousePressed(me -> { 125 /* 126 ** if the tracklenght isn't greater than do nothing.... 127 */ 128 if (!thumb.isVisible() || trackLength > thumbLength) { 129 getBehavior().incButtonPressed(); 130 } 131 me.consume(); 132 }); 133 incButton.setOnMouseReleased(me -> { 134 /* 135 ** if the tracklenght isn't greater than do nothing.... 136 */ 137 if (!thumb.isVisible() || trackLength > thumbLength) { 138 getBehavior().incButtonReleased(); 139 } 140 me.consume(); 141 }); 142 143 decButton = new EndButton("decrement-button", "decrement-arrow") { 144 @Override 145 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 146 switch (action) { 147 case FIRE: 148 getSkinnable().decrement(); 149 break; 150 default: super.executeAccessibleAction(action, parameters); 151 } 152 } 153 }; 154 decButton.setAccessibleRole(AccessibleRole.DECREMENT_BUTTON); 155 decButton.setOnMousePressed(me -> { 156 /* 157 ** if the tracklenght isn't greater than do nothing.... 158 */ 159 if (!thumb.isVisible() || trackLength > thumbLength) { 160 getBehavior().decButtonPressed(); 161 } 162 me.consume(); 163 }); 164 decButton.setOnMouseReleased(me -> { 165 /* 166 ** if the tracklenght isn't greater than do nothing.... 167 */ 168 if (!thumb.isVisible() || trackLength > thumbLength) { 169 getBehavior().decButtonReleased(); 170 } 171 me.consume(); 172 }); 173 } 174 175 176 track.setOnMousePressed(me -> { 177 if (!thumb.isPressed() && me.getButton() == MouseButton.PRIMARY) { 178 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 179 if (trackLength != 0) { 180 getBehavior().trackPress(me.getY() / trackLength); 181 me.consume(); 182 } 183 } else { 184 if (trackLength != 0) { 185 getBehavior().trackPress(me.getX() / trackLength); 186 me.consume(); 187 } 188 } 189 } 190 }); 191 192 track.setOnMouseReleased(me -> { 193 getBehavior().trackRelease(); 194 me.consume(); 195 }); 196 197 thumb.setOnMousePressed(me -> { 198 if (me.isSynthesized()) { 199 // touch-screen events handled by Scroll handler 200 me.consume(); 201 return; 202 } 203 /* 204 ** if max isn't greater than min then there is nothing to do here 205 */ 206 if (getSkinnable().getMax() > getSkinnable().getMin()) { 207 dragStart = thumb.localToParent(me.getX(), me.getY()); 208 double clampedValue = Utils.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax()); 209 preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin()); 210 me.consume(); 211 } 212 }); 213 214 215 thumb.setOnMouseDragged(me -> { 216 if (me.isSynthesized()) { 217 // touch-screen events handled by Scroll handler 218 me.consume(); 219 return; 220 } 221 /* 222 ** if max isn't greater than min then there is nothing to do here 223 */ 224 if (getSkinnable().getMax() > getSkinnable().getMin()) { 225 /* 226 ** if the tracklength isn't greater then do nothing.... 227 */ 228 if (trackLength > thumbLength) { 229 Point2D cur = thumb.localToParent(me.getX(), me.getY()); 230 if (dragStart == null) { 231 // we're getting dragged without getting a mouse press 232 dragStart = thumb.localToParent(me.getX(), me.getY()); 233 } 234 double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX(); 235 getBehavior().thumbDragged(preDragThumbPos + dragPos / (trackLength - thumbLength)); 236 } 237 238 me.consume(); 239 } 240 }); 241 242 thumb.setOnScrollStarted(se -> { 243 if (se.isDirect()) { 244 /* 245 ** if max isn't greater than min then there is nothing to do here 246 */ 247 if (getSkinnable().getMax() > getSkinnable().getMin()) { 248 dragStart = thumb.localToParent(se.getX(), se.getY()); 249 double clampedValue = Utils.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax()); 250 preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin()); 251 se.consume(); 252 } 253 } 254 }); 255 256 thumb.setOnScroll(event -> { 257 if (event.isDirect()) { 258 /* 259 ** if max isn't greater than min then there is nothing to do here 260 */ 261 if (getSkinnable().getMax() > getSkinnable().getMin()) { 262 /* 263 ** if the tracklength isn't greater then do nothing.... 264 */ 265 if (trackLength > thumbLength) { 266 Point2D cur = thumb.localToParent(event.getX(), event.getY()); 267 if (dragStart == null) { 268 // we're getting dragged without getting a mouse press 269 dragStart = thumb.localToParent(event.getX(), event.getY()); 270 } 271 double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX(); 272 getBehavior().thumbDragged(/*todo*/ preDragThumbPos + dragPos / (trackLength - thumbLength)); 273 } 274 275 event.consume(); 276 return; 277 } 278 } 279 }); 280 281 282 getSkinnable().addEventHandler(ScrollEvent.SCROLL, event -> { 283 /* 284 ** if the tracklength isn't greater then do nothing.... 285 */ 286 if (trackLength > thumbLength) { 287 288 double dx = event.getDeltaX(); 289 double dy = event.getDeltaY(); 290 291 /* 292 ** in 2.0 a horizontal scrollbar would scroll on a vertical 293 ** drag on a tracker-pad. We need to keep this behavior. 294 */ 295 dx = (Math.abs(dx) < Math.abs(dy) ? dy : dx); 296 297 /* 298 ** we only consume an event that we've used. 299 */ 300 ScrollBar sb = (ScrollBar) getSkinnable(); 301 302 double delta = (getSkinnable().getOrientation() == Orientation.VERTICAL ? dy : dx); 303 304 /* 305 ** RT-22941 - If this is either a touch or inertia scroll 306 ** then we move to the position of the touch point. 307 * 308 * TODO: this fix causes RT-23406 ([ScrollBar, touch] Dragging scrollbar from the 309 * track on touchscreen causes flickering) 310 */ 311 if (event.isDirect()) { 312 if (trackLength > thumbLength) { 313 getBehavior().thumbDragged((getSkinnable().getOrientation() == Orientation.VERTICAL ? event.getY(): event.getX()) / trackLength); 314 event.consume(); 315 } 316 } 317 else { 318 if (delta > 0.0 && sb.getValue() > sb.getMin()) { 319 sb.decrement(); 320 event.consume(); 321 } else if (delta < 0.0 && sb.getValue() < sb.getMax()) { 322 sb.increment(); 323 event.consume(); 324 } 325 } 326 } 327 }); 328 329 getChildren().clear(); 330 if (!IS_TOUCH_SUPPORTED) { 331 getChildren().addAll(trackBackground, incButton, decButton, track, thumb); 332 } 333 else { 334 getChildren().addAll(track, thumb); 335 } 336 } 337 338 339 340 @Override protected void handleControlPropertyChanged(String p) { 341 super.handleControlPropertyChanged(p); 342 if ("ORIENTATION".equals(p)) { 343 getSkinnable().requestLayout(); 344 } else if ("MIN".equals(p) || "MAX".equals(p) || "VISIBLE_AMOUNT".equals(p)) { 345 positionThumb(); 346 getSkinnable().requestLayout(); 347 } else if ("VALUE".equals(p)) { 348 positionThumb(); 349 } 350 } 351 352 /*************************************************************************** 353 * * 354 * Layout * 355 * * 356 **************************************************************************/ 357 358 private static final double DEFAULT_EMBEDDED_SB_BREADTH = 8.0; 359 360 /* 361 * Gets the breadth of the scrollbar. The "breadth" is the distance 362 * across the scrollbar, i.e. if vertical the width, otherwise the height. 363 * On desktop this is determined by the greater of the breadths of the end-buttons. 364 * Embedded doesn't have end-buttons, so currently we use a default breadth. 365 * We should change this when we get width/height css properties. 366 */ 367 double getBreadth() { 368 if (!IS_TOUCH_SUPPORTED) { 369 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 370 return Math.max(decButton.prefWidth(-1), incButton.prefWidth(-1)) +snappedLeftInset()+snappedRightInset(); 371 } else { 372 return Math.max(decButton.prefHeight(-1), incButton.prefHeight(-1)) +snappedTopInset()+snappedBottomInset(); 373 } 374 } 375 else { 376 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 377 return Math.max(DEFAULT_EMBEDDED_SB_BREADTH, DEFAULT_EMBEDDED_SB_BREADTH)+snappedLeftInset()+snappedRightInset(); 378 } else { 379 return Math.max(DEFAULT_EMBEDDED_SB_BREADTH, DEFAULT_EMBEDDED_SB_BREADTH)+snappedTopInset()+snappedBottomInset(); 380 } 381 } 382 } 383 384 double minThumbLength() { 385 return 1.5f * getBreadth(); 386 } 387 388 double minTrackLength() { 389 return 2.0f * getBreadth(); 390 } 391 392 /* 393 * Minimum length is the length of the end buttons plus twice the 394 * minimum thumb length, which should be enough for a reasonably-sized 395 * track. Minimum breadth is determined by the breadths of the 396 * end buttons. 397 */ 398 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 399 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 400 return getBreadth(); 401 } else { 402 if (!IS_TOUCH_SUPPORTED) { 403 return decButton.minWidth(-1) + incButton.minWidth(-1) + minTrackLength()+leftInset+rightInset; 404 } else { 405 return minTrackLength()+leftInset+rightInset; 406 } 407 } 408 } 409 410 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 411 if (getSkinnable().getOrientation() == Orientation.VERTICAL) { 412 if (!IS_TOUCH_SUPPORTED) { 413 return decButton.minHeight(-1) + incButton.minHeight(-1) + minTrackLength()+topInset+bottomInset; 414 } else { 415 return minTrackLength()+topInset+bottomInset; 416 } 417 } else { 418 return getBreadth(); 419 } 420 } 421 422 /* 423 * Preferred size. The breadth is determined by the breadth of 424 * the end buttons. The length is a constant default length. 425 * Usually applications or other components will either set a 426 * specific length using LayoutInfo or will stretch the length 427 * of the scrollbar to fit a container. 428 */ 429 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 430 final ScrollBar s = getSkinnable(); 431 return s.getOrientation() == Orientation.VERTICAL ? getBreadth() : DEFAULT_LENGTH+leftInset+rightInset; 432 } 433 434 @Override protected double computePrefHeight(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 435 final ScrollBar s = getSkinnable(); 436 return s.getOrientation() == Orientation.VERTICAL ? DEFAULT_LENGTH+topInset+bottomInset : getBreadth(); 437 } 438 439 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 440 final ScrollBar s = getSkinnable(); 441 return s.getOrientation() == Orientation.VERTICAL ? s.prefWidth(-1) : Double.MAX_VALUE; 442 } 443 444 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 445 final ScrollBar s = getSkinnable(); 446 return s.getOrientation() == Orientation.VERTICAL ? Double.MAX_VALUE : s.prefHeight(-1); 447 } 448 449 /** 450 * Called when ever either min, max or value changes, so thumb's layoutX, Y is recomputed. 451 */ 452 void positionThumb() { 453 ScrollBar s = getSkinnable(); 454 double clampedValue = Utils.clamp(s.getMin(), s.getValue(), s.getMax()); 455 trackPos = (s.getMax() - s.getMin() > 0) ? ((trackLength - thumbLength) * (clampedValue - s.getMin()) / (s.getMax() - s.getMin())) : (0.0F); 456 457 if (!IS_TOUCH_SUPPORTED) { 458 if (s.getOrientation() == Orientation.VERTICAL) { 459 trackPos += decButton.prefHeight(-1); 460 } else { 461 trackPos += decButton.prefWidth(-1); 462 } 463 } 464 465 thumb.setTranslateX( snapPosition(s.getOrientation() == Orientation.VERTICAL ? snappedLeftInset() : trackPos + snappedLeftInset())); 466 thumb.setTranslateY( snapPosition(s.getOrientation() == Orientation.VERTICAL ? trackPos + snappedTopInset() : snappedTopInset())); 467 } 468 469 @Override protected void layoutChildren(final double x, final double y, 470 final double w, final double h) { 471 472 final ScrollBar s = getSkinnable(); 473 474 /** 475 * Compute the percentage length of thumb as (visibleAmount/range) 476 * if max isn't greater than min then there is nothing to do here 477 */ 478 double visiblePortion; 479 if (s.getMax() > s.getMin()) { 480 visiblePortion = s.getVisibleAmount()/(s.getMax() - s.getMin()); 481 } 482 else { 483 visiblePortion = 1.0; 484 } 485 486 if (s.getOrientation() == Orientation.VERTICAL) { 487 if (!IS_TOUCH_SUPPORTED) { 488 double decHeight = snapSize(decButton.prefHeight(-1)); 489 double incHeight = snapSize(incButton.prefHeight(-1)); 490 491 decButton.resize(w, decHeight); 492 incButton.resize(w, incHeight); 493 494 trackLength = snapSize(h - (decHeight + incHeight)); 495 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength)); 496 497 trackBackground.resizeRelocate(snapPosition(x), snapPosition(y), w, trackLength+decHeight+incHeight); 498 decButton.relocate(snapPosition(x), snapPosition(y)); 499 incButton.relocate(snapPosition(x), snapPosition(y + h - incHeight)); 500 track.resizeRelocate(snapPosition(x), snapPosition(y + decHeight), w, trackLength); 501 thumb.resize(snapSize(x >= 0 ? w : w + x), thumbLength); // Account for negative padding (see also RT-10719) 502 positionThumb(); 503 } 504 else { 505 trackLength = snapSize(h); 506 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength)); 507 508 track.resizeRelocate(snapPosition(x), snapPosition(y), w, trackLength); 509 thumb.resize(snapSize(x >= 0 ? w : w + x), thumbLength); // Account for negative padding (see also RT-10719) 510 positionThumb(); 511 } 512 } else { 513 if (!IS_TOUCH_SUPPORTED) { 514 double decWidth = snapSize(decButton.prefWidth(-1)); 515 double incWidth = snapSize(incButton.prefWidth(-1)); 516 517 decButton.resize(decWidth, h); 518 incButton.resize(incWidth, h); 519 520 trackLength = snapSize(w - (decWidth + incWidth)); 521 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength)); 522 523 trackBackground.resizeRelocate(snapPosition(x), snapPosition(y), trackLength+decWidth+incWidth, h); 524 decButton.relocate(snapPosition(x), snapPosition(y)); 525 incButton.relocate(snapPosition(x + w - incWidth), snapPosition(y)); 526 track.resizeRelocate(snapPosition(x + decWidth), snapPosition(y), trackLength, h); 527 thumb.resize(thumbLength, snapSize(y >= 0 ? h : h + y)); // Account for negative padding (see also RT-10719) 528 positionThumb(); 529 } 530 else { 531 trackLength = snapSize(w); 532 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength)); 533 534 track.resizeRelocate(snapPosition(x), snapPosition(y), trackLength, h); 535 thumb.resize(thumbLength, snapSize(y >= 0 ? h : h + y)); // Account for negative padding (see also RT-10719) 536 positionThumb(); 537 } 538 539 s.resize(snapSize(s.getWidth()), snapSize(s.getHeight())); 540 } 541 542 // things should be invisible only when well below minimum length 543 if (s.getOrientation() == Orientation.VERTICAL && h >= (computeMinHeight(-1, (int)y , snappedRightInset(), snappedBottomInset(), (int)x) - (y+snappedBottomInset())) || 544 s.getOrientation() == Orientation.HORIZONTAL && w >= (computeMinWidth(-1, (int)y , snappedRightInset(), snappedBottomInset(), (int)x) - (x+snappedRightInset()))) { 545 trackBackground.setVisible(true); 546 track.setVisible(true); 547 thumb.setVisible(true); 548 if (!IS_TOUCH_SUPPORTED) { 549 incButton.setVisible(true); 550 decButton.setVisible(true); 551 } 552 } 553 else { 554 trackBackground.setVisible(false); 555 track.setVisible(false); 556 thumb.setVisible(false); 557 558 if (!IS_TOUCH_SUPPORTED) { 559 /* 560 ** once the space is big enough for one button we 561 ** can look at drawing 562 */ 563 if (h >= decButton.computeMinWidth(-1)) { 564 decButton.setVisible(true); 565 } 566 else { 567 decButton.setVisible(false); 568 } 569 if (h >= incButton.computeMinWidth(-1)) { 570 incButton.setVisible(true); 571 } 572 else { 573 incButton.setVisible(false); 574 } 575 } 576 } 577 } 578 579 580 public Node getThumb() { 581 return thumb; 582 } 583 584 public Node getTrack() { 585 return track; 586 } 587 588 public Node getIncButton() { 589 return incButton; 590 } 591 592 public Node getDecButton() { 593 return decButton; 594 } 595 596 private static class EndButton extends Region { 597 private Region arrow; 598 599 private EndButton(String styleClass, String arrowStyleClass) { 600 getStyleClass().setAll(styleClass); 601 arrow = new Region(); 602 arrow.getStyleClass().setAll(arrowStyleClass); 603 getChildren().setAll(arrow); 604 requestLayout(); 605 } 606 607 @Override protected void layoutChildren() { 608 final double top = snappedTopInset(); 609 final double left = snappedLeftInset(); 610 final double bottom = snappedBottomInset(); 611 final double right = snappedRightInset(); 612 final double aw = snapSize(arrow.prefWidth(-1)); 613 final double ah = snapSize(arrow.prefHeight(-1)); 614 final double yPos = snapPosition((getHeight() - (top + bottom + ah)) / 2.0); 615 final double xPos = snapPosition((getWidth() - (left + right + aw)) / 2.0); 616 arrow.resizeRelocate(xPos + left, yPos + top, aw, ah); 617 } 618 619 @Override protected double computeMinHeight(double width) { 620 return prefHeight(-1); 621 } 622 623 @Override protected double computeMinWidth(double height) { 624 return prefWidth(-1); 625 } 626 627 @Override protected double computePrefWidth(double height) { 628 final double left = snappedLeftInset(); 629 final double right = snappedRightInset(); 630 final double aw = snapSize(arrow.prefWidth(-1)); 631 return left + aw + right; 632 } 633 634 @Override protected double computePrefHeight(double width) { 635 final double top = snappedTopInset(); 636 final double bottom = snappedBottomInset(); 637 final double ah = snapSize(arrow.prefHeight(-1)); 638 return top + ah + bottom; 639 } 640 } 641 }