## Smooth scroll functionality for ScrollContainer ## ## Applies velocity based momentum and "overdrag" ## functionality to a ScrollContainer @tool extends ScrollContainer class_name SmoothScrollContainer @export_group("Mouse Wheel") ## Drag impact for one scroll input @export_range(0, 10, 0.01, "or_greater", "hide_slider") var speed := 1000.0 ## ScrollDamper for wheel scrolling @export var wheel_scroll_damper: ScrollDamper = ExpoScrollDamper.new() @export_group("Dragging") ## ScrollDamper for dragging @export var dragging_scroll_damper: ScrollDamper = ExpoScrollDamper.new() ### Allow dragging with mouse or not @export var drag_with_mouse := true ## Allow dragging with touch or not @export var drag_with_touch := true @export_group("Container") ## Below this value, snap content to boundary @export var just_snap_under := 0.4 ## Margin of the currently focused element @export_range(0, 50) var follow_focus_margin := 20 ## Makes the container scrollable vertically @export var allow_vertical_scroll := true ## Makes the container scrollable horizontally @export var allow_horizontal_scroll := true ## Makes the container only scrollable where the content has overflow @export var auto_allow_scroll := true ## Whether the content of this container should be allowed to overshoot at the ends ## before interpolating back to its bounds @export var allow_overdragging := true @export_group("Scroll Bar") ## Hides scrollbar as long as not hovered or interacted with @export var hide_scrollbar_over_time := false: set(val): hide_scrollbar_over_time = _set_hide_scrollbar_over_time(val) ## Time after scrollbar starts to fade out when 'hide_scrollbar_over_time' is true @export var scrollbar_hide_time := 5.0 ## Fadein time for scrollbar when 'hide_scrollbar_over_time' is true @export var scrollbar_fade_in_time := 0.2 ## Fadeout time for scrollbar when 'hide_scrollbar_over_time' is true @export var scrollbar_fade_out_time := 0.5 @export_group("Input") ## If true sets the input event as handled with set_input_as_handled() @export var handle_input := true @export_group("Debug") ## Adds debug information @export var debug_mode := false ## Current velocity of the `content_node` var velocity := Vector2(0,0) ## Control node to move when scrolling var content_node: Control ## Current position of `content_node` var pos := Vector2(0, 0) ## Current ScrollDamper to use, recording to last input type var scroll_damper: ScrollDamper ## When true, `content_node`'s position is only set by dragging the h scroll bar var h_scrollbar_dragging := false ## When true, `content_node`'s position is only set by dragging the v scroll bar var v_scrollbar_dragging := false ## When ture, `content_node` follows drag position var content_dragging := false ## When ture, `content_node` has moved by dragging var content_dragging_moved := false ## Timer for hiding scroll bar var scrollbar_hide_timer := Timer.new() ## Tween for showing scroll bar var scrollbar_show_tween: Tween ## Tween for hiding scroll bar var scrollbar_hide_tween: Tween ## Tween for scroll x to var scroll_x_to_tween: Tween ## Tween for scroll y to var scroll_y_to_tween: Tween ## [0,1] Mouse or touch's relative movement accumulation when overdrag[br] ## [2,3] Position where dragging starts[br] ## [4,5,6,7] Left_distance, right_distance, top_distance, bottom_distance var drag_temp_data := [] ## Whether touch point is in deadzone. var is_in_deadzone := false ## Whether mouse is on h or v scroll bar var mouse_on_scrollbar := false ## If content is being scrolled var is_scrolling := false: set(val): if is_scrolling != val: if val: emit_signal("scroll_started") else: emit_signal("scroll_ended") is_scrolling = val ## Last type of input used to scroll enum SCROLL_TYPE {WHEEL, BAR, DRAG} var last_scroll_type: SCROLL_TYPE #region Virtual Functions func _ready() -> void: if debug_mode: setup_debug_drawing() # Initialize variables scroll_damper = wheel_scroll_damper get_v_scroll_bar().gui_input.connect(_scrollbar_input.bind(true)) get_h_scroll_bar().gui_input.connect(_scrollbar_input.bind(false)) get_v_scroll_bar().mouse_entered.connect(_mouse_on_scroll_bar.bind(true)) get_v_scroll_bar().mouse_exited.connect(_mouse_on_scroll_bar.bind(false)) get_h_scroll_bar().mouse_entered.connect(_mouse_on_scroll_bar.bind(true)) get_h_scroll_bar().mouse_exited.connect(_mouse_on_scroll_bar.bind(false)) get_viewport().gui_focus_changed.connect(_on_focus_changed) for c in get_children(): if not c is ScrollBar: content_node = c add_child(scrollbar_hide_timer) scrollbar_hide_timer.one_shot = true scrollbar_hide_timer.timeout.connect(_scrollbar_hide_timer_timeout) if hide_scrollbar_over_time: scrollbar_hide_timer.start(scrollbar_hide_time) get_tree().node_added.connect(_on_node_added) func _process(delta: float) -> void: if Engine.is_editor_hint(): return scroll(true, velocity.y, pos.y, delta) scroll(false, velocity.x, pos.x, delta) update_scrollbars() update_is_scrolling() if debug_mode: queue_redraw() # Detecting mouse entering and exiting scroll bar func _mouse_on_scroll_bar(entered: bool) -> void: mouse_on_scrollbar = entered # Forwarding scroll inputs from scrollbar func _scrollbar_input(event: InputEvent, vertical: bool) -> void: if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_WHEEL_DOWN\ or event.button_index == MOUSE_BUTTON_WHEEL_UP\ or event.button_index == MOUSE_BUTTON_WHEEL_LEFT\ or event.button_index == MOUSE_BUTTON_WHEEL_RIGHT: _gui_input(event) if event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: if vertical: v_scrollbar_dragging = true last_scroll_type = SCROLL_TYPE.BAR kill_scroll_to_tweens() else: h_scrollbar_dragging = true last_scroll_type = SCROLL_TYPE.BAR kill_scroll_to_tweens() else: if vertical: v_scrollbar_dragging = false else: h_scrollbar_dragging = false if event is InputEventScreenTouch: if event.pressed: if vertical: v_scrollbar_dragging = true last_scroll_type = SCROLL_TYPE.BAR kill_scroll_to_tweens() else: h_scrollbar_dragging = true last_scroll_type = SCROLL_TYPE.BAR kill_scroll_to_tweens() else: if vertical: v_scrollbar_dragging = false else: h_scrollbar_dragging = false func _gui_input(event: InputEvent) -> void: # Show scroll bars when mouse moves if hide_scrollbar_over_time and event is InputEventMouseMotion: show_scrollbars() if event is InputEventMouseButton: match event.button_index: MOUSE_BUTTON_WHEEL_DOWN: if event.pressed: last_scroll_type = SCROLL_TYPE.WHEEL if event.shift_pressed or not should_scroll_vertical(): if should_scroll_horizontal(): velocity.x -= speed * event.factor else: if should_scroll_vertical(): velocity.y -= speed * event.factor scroll_damper = wheel_scroll_damper kill_scroll_to_tweens() MOUSE_BUTTON_WHEEL_UP: if event.pressed: last_scroll_type = SCROLL_TYPE.WHEEL if event.shift_pressed or not should_scroll_vertical(): if should_scroll_horizontal(): velocity.x += speed * event.factor else: if should_scroll_vertical(): velocity.y += speed * event.factor scroll_damper = wheel_scroll_damper kill_scroll_to_tweens() MOUSE_BUTTON_WHEEL_LEFT: if event.pressed: last_scroll_type = SCROLL_TYPE.WHEEL if event.shift_pressed: if should_scroll_vertical(): velocity.y -= speed * event.factor else: if should_scroll_horizontal(): velocity.x += speed * event.factor scroll_damper = wheel_scroll_damper kill_scroll_to_tweens() MOUSE_BUTTON_WHEEL_RIGHT: if event.pressed: last_scroll_type = SCROLL_TYPE.WHEEL if event.shift_pressed: if should_scroll_vertical(): velocity.y += speed * event.factor else: if should_scroll_horizontal(): velocity.x -= speed * event.factor scroll_damper = wheel_scroll_damper kill_scroll_to_tweens() MOUSE_BUTTON_LEFT: if event.pressed: if !drag_with_mouse: return content_dragging = true is_in_deadzone = true scroll_damper = dragging_scroll_damper last_scroll_type = SCROLL_TYPE.DRAG init_drag_temp_data() kill_scroll_to_tweens() else: content_dragging = false is_in_deadzone = false if (event is InputEventScreenDrag and drag_with_touch) \ or (event is InputEventMouseMotion and drag_with_mouse): if content_dragging: if should_scroll_horizontal(): drag_temp_data[0] += event.relative.x if should_scroll_vertical(): drag_temp_data[1] += event.relative.y remove_all_children_focus(self) handle_content_dragging() if event is InputEventPanGesture: if should_scroll_horizontal(): velocity.x = -event.delta.x * speed kill_scroll_to_tweens() if should_scroll_vertical(): velocity.y = -event.delta.y * speed kill_scroll_to_tweens() if event is InputEventScreenTouch: if event.pressed: if !drag_with_touch: return content_dragging = true is_in_deadzone = true scroll_damper = dragging_scroll_damper last_scroll_type = SCROLL_TYPE.DRAG init_drag_temp_data() kill_scroll_to_tweens() else: content_dragging = false is_in_deadzone = false # Handle input if handle_input is true if handle_input: get_tree().get_root().set_input_as_handled() # Scroll to new focused element func _on_focus_changed(control: Control) -> void: if follow_focus: self.ensure_control_visible(control) func _draw() -> void: if debug_mode: draw_debug() # Sets default mouse filter for SmoothScroll children to MOUSE_FILTER_PASS func _on_node_added(node: Node) -> void: if node is Control and Engine.is_editor_hint(): if is_ancestor_of(node): node.mouse_filter = Control.MOUSE_FILTER_PASS func _scrollbar_hide_timer_timeout() -> void: if !any_scroll_bar_dragged(): hide_scrollbars() func _set_hide_scrollbar_over_time(value: bool) -> bool: if value == false: if scrollbar_hide_timer != null: scrollbar_hide_timer.stop() if scrollbar_show_tween != null: scrollbar_show_tween.kill() if scrollbar_hide_tween != null: scrollbar_hide_tween.kill() get_h_scroll_bar().modulate = Color.WHITE get_v_scroll_bar().modulate = Color.WHITE else: if scrollbar_hide_timer != null and scrollbar_hide_timer.is_inside_tree(): scrollbar_hide_timer.start(scrollbar_hide_time) return value func _get(property: StringName) -> Variant: match property: "scroll_horizontal": if !content_node: return 0 return -int(content_node.position.x) "scroll_vertical": if !content_node: return 0 return -int(content_node.position.y) _: return null func _set(property: StringName, value: Variant) -> bool: match property: "scroll_horizontal": if !content_node: scroll_horizontal = 0 return true scroll_horizontal = value kill_scroll_x_to_tween() velocity.x = 0.0 pos.x = clampf( -value as float, -get_child_size_x_diff(content_node, true), 0.0 ) return true "scroll_vertical": if !content_node: scroll_vertical = 0 return true scroll_vertical = value kill_scroll_y_to_tween() velocity.y = 0.0 pos.y = clampf( -value as float, -get_child_size_y_diff(content_node, true), 0.0 ) return true _: return false #endregion #region Scrolling Logic func scroll(vertical: bool, axis_velocity: float, axis_pos: float, delta: float): # If no scroll needed, don't apply forces if vertical: if not should_scroll_vertical(): return else: if not should_scroll_horizontal(): return if !scroll_damper: return # Applies counterforces when overdragging if not content_dragging: axis_velocity = handle_overdrag(vertical, axis_velocity, axis_pos, delta) # Move content node by applying velocity var slide_result = scroll_damper.slide(axis_velocity, delta) axis_velocity = slide_result[0] axis_pos += slide_result[1] # Snap to boundary if close enough var snap_result = snap(vertical, axis_velocity, axis_pos) axis_velocity = snap_result[0] axis_pos = snap_result[1] else: # Preserve dragging velocity for 1 frame # in case no movement event while releasing dragging with touch if content_dragging_moved: content_dragging_moved = false else: axis_velocity = 0.0 # If using scroll bar dragging, set the content_node's # position by using the scrollbar position if handle_scrollbar_drag(): return if vertical: if not allow_overdragging: # Clamp if calculated position is beyond boundary if is_outside_top_boundary(axis_pos): axis_pos = 0.0 axis_velocity = 0.0 elif is_outside_bottom_boundary(axis_pos): axis_pos = -get_child_size_y_diff(content_node, true) axis_velocity = 0.0 content_node.position.y = axis_pos pos.y = axis_pos velocity.y = axis_velocity else: if not allow_overdragging: # Clamp if calculated position is beyond boundary if is_outside_left_boundary(axis_pos): axis_pos = 0.0 axis_velocity = 0.0 elif is_outside_right_boundary(axis_pos): axis_pos = -get_child_size_x_diff(content_node, true) axis_velocity = 0.0 content_node.position.x = axis_pos pos.x = axis_pos velocity.x = axis_velocity func handle_overdrag(vertical: bool, axis_velocity: float, axis_pos: float, delta: float) -> float: if !scroll_damper: return 0.0 # Calculate the size difference between this container and content_node var size_diff = get_child_size_y_diff(content_node, true) \ if vertical else get_child_size_x_diff(content_node, true) # Calculate distance to left and right or top and bottom var dist1 = get_child_top_dist(axis_pos, size_diff) \ if vertical else get_child_left_dist(axis_pos, size_diff) var dist2 = get_child_bottom_dist(axis_pos, size_diff) \ if vertical else get_child_right_dist(axis_pos, size_diff) # Calculate velocity to left and right or top and bottom var target_vel1 = scroll_damper._calculate_velocity_to_dest(dist1, 0.0) var target_vel2 = scroll_damper._calculate_velocity_to_dest(dist2, 0.0) # Bounce when out of boundary. When velocity is not fast enough to go back, # apply a opposite force and get a new velocity. If the new velocity is too fast, # apply a velocity that makes it scroll back exactly. if axis_pos > 0.0: if axis_velocity > target_vel1: axis_velocity = scroll_damper.attract( dist1, 0.0, axis_velocity, delta ) if axis_pos < -size_diff: if axis_velocity < target_vel2: axis_velocity = scroll_damper.attract( dist2, 0.0, axis_velocity, delta ) return axis_velocity # Snap to boundary if close enough in next frame func snap(vertical: bool, axis_velocity: float, axis_pos: float) -> Array: # Calculate the size difference between this container and content_node var size_diff = get_child_size_y_diff(content_node, true) \ if vertical else get_child_size_x_diff(content_node, true) # Calculate distance to left and right or top and bottom var dist1 = get_child_top_dist(axis_pos, size_diff) \ if vertical else get_child_left_dist(axis_pos, size_diff) var dist2 = get_child_bottom_dist(axis_pos, size_diff) \ if vertical else get_child_right_dist(axis_pos, size_diff) if ( dist1 > 0.0 \ and abs(dist1) < just_snap_under \ and abs(axis_velocity) < just_snap_under \ ): axis_pos -= dist1 axis_velocity = 0.0 elif ( dist2 < 0.0 \ and abs(dist2) < just_snap_under \ and abs(axis_velocity) < just_snap_under \ ): axis_pos -= dist2 axis_velocity = 0.0 return [axis_velocity, axis_pos] ## Returns true when scrollbar was dragged func handle_scrollbar_drag() -> bool: if h_scrollbar_dragging: velocity.x = 0.0 pos.x = -get_h_scroll_bar().value return true if v_scrollbar_dragging: velocity.y = 0.0 pos.y = -get_v_scroll_bar().value return true return false func handle_content_dragging() -> void: if !dragging_scroll_damper: return if( Vector2(drag_temp_data[0], drag_temp_data[1]).length() < scroll_deadzone \ and is_in_deadzone ): return elif is_in_deadzone == true: is_in_deadzone = false drag_temp_data[0] = 0.0 drag_temp_data[1] = 0.0 content_dragging_moved = true var calculate_dest = func(delta: float, damping: float) -> float: if delta >= 0.0: return delta / (1 + delta * damping * 0.00001) else: return delta var calculate_position = func( temp_dist1: float, # Temp distance temp_dist2: float, temp_relative: float # Event's relative movement accumulation ) -> float: if temp_relative + temp_dist1 > 0.0: var delta = min(temp_relative, temp_relative + temp_dist1) var dest = calculate_dest.call(delta, dragging_scroll_damper._attract_factor) return dest - min(0.0, temp_dist1) elif temp_relative + temp_dist2 < 0.0: var delta = max(temp_relative, temp_relative + temp_dist2) var dest = -calculate_dest.call(-delta, dragging_scroll_damper._attract_factor) return dest - max(0.0, temp_dist2) else: return temp_relative if should_scroll_vertical(): var y_pos = calculate_position.call( drag_temp_data[6], # Temp top_distance drag_temp_data[7], # Temp bottom_distance drag_temp_data[1] # Temp y relative accumulation ) + drag_temp_data[3] velocity.y = (y_pos - pos.y) / get_process_delta_time() pos.y = y_pos if should_scroll_horizontal(): var x_pos = calculate_position.call( drag_temp_data[4], # Temp left_distance drag_temp_data[5], # Temp right_distance drag_temp_data[0] # Temp x relative accumulation ) + drag_temp_data[2] velocity.x = (x_pos - pos.x) / get_process_delta_time() pos.x = x_pos func remove_all_children_focus(node: Node) -> void: if node is Control: var control = node as Control control.release_focus() for child in node.get_children(): remove_all_children_focus(child) func update_is_scrolling() -> void: if( (content_dragging and not is_in_deadzone) or any_scroll_bar_dragged() or velocity != Vector2.ZERO ): is_scrolling = true else: is_scrolling = false func update_scrollbars() -> void: # Update vertical scroll bar if get_v_scroll_bar().value != -pos.y: get_v_scroll_bar().set_value_no_signal(-pos.y) get_v_scroll_bar().queue_redraw() # Update horizontal scroll bar if get_h_scroll_bar().value != -pos.x: get_h_scroll_bar().set_value_no_signal(-pos.x) get_h_scroll_bar().queue_redraw() # Always show sroll bars when scrolling or mouse is on any scroll bar if hide_scrollbar_over_time and (is_scrolling or mouse_on_scrollbar): show_scrollbars() func init_drag_temp_data() -> void: # Calculate the size difference between this container and content_node var content_node_size_diff = get_child_size_diff(content_node, true, true) # Calculate distance to left, right, top and bottom var content_node_boundary_dist = get_child_boundary_dist( content_node.position, content_node_size_diff ) drag_temp_data = [ 0.0, 0.0, content_node.position.x, content_node.position.y, content_node_boundary_dist.x, content_node_boundary_dist.y, content_node_boundary_dist.z, content_node_boundary_dist.w, ] # Get container size x without v scroll bar 's width func get_spare_size_x() -> float: var size_x = size.x if get_v_scroll_bar().visible: size_x -= get_v_scroll_bar().size.x return max(size_x, 0.0) # Get container size y without h scroll bar 's height func get_spare_size_y() -> float: var size_y = size.y if get_h_scroll_bar().visible: size_y -= get_h_scroll_bar().size.y return max(size_y, 0.0) # Get container size without scroll bars' size func get_spare_size() -> Vector2: return Vector2(get_spare_size_x(), get_spare_size_y()) # Calculate the size x difference between this container and child node func get_child_size_x_diff(child: Control, clamp: bool) -> float: var child_size_x = child.size.x * child.scale.x # Falsify the size of the child node to avoid errors # when its size is smaller than this container 's if clamp: child_size_x = max(child_size_x, get_spare_size_x()) return child_size_x - get_spare_size_x() # Calculate the size y difference between this container and child node func get_child_size_y_diff(child: Control, clamp: bool) -> float: var child_size_y = child.size.y * child.scale.y # Falsify the size of the child node to avoid errors # when its size is smaller than this container 's if clamp: child_size_y = max(child_size_y, get_spare_size_y()) return child_size_y - get_spare_size_y() # Calculate the size difference between this container and child node func get_child_size_diff(child: Control, clamp_x: bool, clamp_y: bool) -> Vector2: return Vector2( get_child_size_x_diff(child, clamp_x), get_child_size_y_diff(child, clamp_y) ) # Calculate distance to left func get_child_left_dist(child_pos_x: float, child_size_diff_x: float) -> float: return child_pos_x # Calculate distance to right func get_child_right_dist(child_pos_x: float, child_size_diff_x: float) -> float: return child_pos_x + child_size_diff_x # Calculate distance to top func get_child_top_dist(child_pos_y: float, child_size_diff_y: float) -> float: return child_pos_y # Calculate distance to bottom func get_child_bottom_dist(child_pos_y: float, child_size_diff_y: float) -> float: return child_pos_y + child_size_diff_y # Calculate distance to left, right, top and bottom func get_child_boundary_dist(child_pos: Vector2, child_size_diff: Vector2) -> Vector4: return Vector4( get_child_left_dist(child_pos.x, child_size_diff.x), get_child_right_dist(child_pos.x, child_size_diff.x), get_child_top_dist(child_pos.y, child_size_diff.y), get_child_bottom_dist(child_pos.y, child_size_diff.y), ) func kill_scroll_x_to_tween() -> void: if scroll_x_to_tween: scroll_x_to_tween.kill() func kill_scroll_y_to_tween() -> void: if scroll_y_to_tween: scroll_y_to_tween.kill() func kill_scroll_to_tweens() -> void: kill_scroll_x_to_tween() kill_scroll_y_to_tween() #endregion #region Debug Drawing var debug_gradient := Gradient.new() func setup_debug_drawing() -> void: debug_gradient.set_color(0.0, Color.GREEN) debug_gradient.set_color(1.0, Color.RED) func draw_debug() -> void: # Calculate the size difference between this container and content_node var size_diff = get_child_size_diff(content_node, false, false) # Calculate distance to left, right, top and bottom var boundary_dist = get_child_boundary_dist( content_node.position, size_diff ) var bottom_distance = boundary_dist.w var top_distance = boundary_dist.z var right_distance = boundary_dist.y var left_distance = boundary_dist.x # Overdrag lines # Top + Bottom draw_line(Vector2(0.0, 0.0), Vector2(0.0, top_distance), debug_gradient.sample(clamp(top_distance / size.y, 0.0, 1.0)), 5.0) draw_line(Vector2(0.0, size.y), Vector2(0.0, size.y+bottom_distance), debug_gradient.sample(clamp(-bottom_distance / size.y, 0.0, 1.0)), 5.0) # Left + Right draw_line(Vector2(0.0, size.y), Vector2(left_distance, size.y), debug_gradient.sample(clamp(left_distance / size.y, 0.0, 1.0)), 5.0) draw_line(Vector2(size.x, size.y), Vector2(size.x+right_distance, size.y), debug_gradient.sample(clamp(-right_distance / size.y, 0.0, 1.0)), 5.0) # Velocity lines var origin := Vector2(5.0, size.y/2) draw_line(origin, origin + Vector2(0.0, velocity.y*0.01), debug_gradient.sample(clamp(velocity.y*2 / size.y, 0.0, 1.0)), 5.0) draw_line(origin, origin + Vector2(0.0, velocity.x*0.01), debug_gradient.sample(clamp(velocity.x*2 / size.x, 0.0, 1.0)), 5.0) #endregion #region API Functions ## Scrolls to specific x position func scroll_x_to(x_pos: float, duration := 0.5) -> void: if not should_scroll_horizontal(): return if content_dragging: return velocity.x = 0.0 var size_x_diff = get_child_size_x_diff(content_node, true) x_pos = clampf(x_pos, -size_x_diff, 0.0) kill_scroll_x_to_tween() scroll_x_to_tween = create_tween() var tweener = scroll_x_to_tween.tween_property(self, "pos:x", x_pos, duration) tweener.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUINT) ## Scrolls to specific y position func scroll_y_to(y_pos: float, duration := 0.5) -> void: if not should_scroll_vertical(): return if content_dragging: return velocity.y = 0.0 var size_y_diff = get_child_size_y_diff(content_node, true) y_pos = clampf(y_pos, -size_y_diff, 0.0) kill_scroll_y_to_tween() scroll_y_to_tween = create_tween() var tweener = scroll_y_to_tween.tween_property(self, "pos:y", y_pos, duration) tweener.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUINT) ## Scrolls up a page func scroll_page_up(duration := 0.5) -> void: var destination = content_node.position.y + get_spare_size_y() scroll_y_to(destination, duration) ## Scrolls down a page func scroll_page_down(duration := 0.5) -> void: var destination = content_node.position.y - get_spare_size_y() scroll_y_to(destination, duration) ## Scrolls left a page func scroll_page_left(duration := 0.5) -> void: var destination = content_node.position.x + get_spare_size_x() scroll_x_to(destination, duration) ## Scrolls right a page func scroll_page_right(duration := 0.5) -> void: var destination = content_node.position.x - get_spare_size_x() scroll_x_to(destination, duration) ## Adds velocity to the vertical scroll func scroll_vertically(amount: float) -> void: velocity.y -= amount ## Adds velocity to the horizontal scroll func scroll_horizontally(amount: float) -> void: velocity.x -= amount ## Scrolls to top func scroll_to_top(duration := 0.5) -> void: scroll_y_to(0.0, duration) ## Scrolls to bottom func scroll_to_bottom(duration := 0.5) -> void: scroll_y_to(get_spare_size_y() - content_node.size.y, duration) ## Scrolls to left func scroll_to_left(duration := 0.5) -> void: scroll_x_to(0.0, duration) ## Scrolls to right func scroll_to_right(duration := 0.5) -> void: scroll_x_to(get_spare_size_x() - content_node.size.x, duration) func is_outside_top_boundary(y_pos: float = pos.y) -> bool: var size_y_diff = get_child_size_y_diff(content_node,true) var top_dist = get_child_top_dist(y_pos, size_y_diff) return top_dist > 0.0 func is_outside_bottom_boundary(y_pos: float = pos.y) -> bool: var size_y_diff = get_child_size_y_diff(content_node,true) var bottom_dist = get_child_bottom_dist(y_pos, size_y_diff) return bottom_dist < 0.0 func is_outside_left_boundary(x_pos: float = pos.x) -> bool: var size_x_diff = get_child_size_x_diff(content_node,true) var left_dist = get_child_left_dist(x_pos, size_x_diff) return left_dist > 0.0 func is_outside_right_boundary(x_pos: float = pos.x) -> bool: var size_x_diff = get_child_size_x_diff(content_node,true) var right_dist = get_child_right_dist(x_pos, size_x_diff) return right_dist < 0.0 ## Returns true if any scroll bar is being dragged func any_scroll_bar_dragged() -> bool: return h_scrollbar_dragging or v_scrollbar_dragging ## Returns true if there is enough content height to scroll func should_scroll_vertical() -> bool: var disable_scroll = (not allow_vertical_scroll) \ or (auto_allow_scroll and get_child_size_y_diff(content_node, false) <= 0) \ or !scroll_damper if disable_scroll: velocity.y = 0.0 return false else: return true ## Returns true if there is enough content width to scroll func should_scroll_horizontal() -> bool: var disable_scroll = (not allow_horizontal_scroll) \ or (auto_allow_scroll and get_child_size_x_diff(content_node, false) <= 0) \ or !scroll_damper if disable_scroll: velocity.x = 0.0 return false else: return true ## Fades out scrollbars within given [param time].[br] ## Default for [param time] is current [member scrollbar_fade_out_time] func hide_scrollbars(time: float = scrollbar_fade_out_time) -> void: # Kill scrollbar_show_tween to avoid animation conflict if scrollbar_show_tween != null and scrollbar_show_tween.is_valid(): scrollbar_show_tween.kill() # Create new tweens if needed if ( get_v_scroll_bar().modulate != Color.TRANSPARENT \ or get_h_scroll_bar().modulate != Color.TRANSPARENT ): if scrollbar_hide_tween and !scrollbar_hide_tween.is_running(): scrollbar_hide_tween.kill() if scrollbar_hide_tween == null or !scrollbar_hide_tween.is_valid(): scrollbar_hide_tween = create_tween() scrollbar_hide_tween.set_parallel(true) scrollbar_hide_tween.tween_property(get_v_scroll_bar(), 'modulate', Color.TRANSPARENT, time) scrollbar_hide_tween.tween_property(get_h_scroll_bar(), 'modulate', Color.TRANSPARENT, time) ## Fades in scrollbars within given [param time].[br] ## Default for [param time] is current [member scrollbar_fade_in_time] func show_scrollbars(time: float = scrollbar_fade_in_time) -> void: # Restart timer scrollbar_hide_timer.start(scrollbar_hide_time) # Kill scrollbar_hide_tween to avoid animation conflict if scrollbar_hide_tween != null and scrollbar_hide_tween.is_valid(): scrollbar_hide_tween.kill() # Create new tweens if needed if ( get_v_scroll_bar().modulate != Color.WHITE \ or get_h_scroll_bar().modulate != Color.WHITE \ ): if scrollbar_show_tween and !scrollbar_show_tween.is_running(): scrollbar_show_tween.kill() if scrollbar_show_tween == null or !scrollbar_show_tween.is_valid(): scrollbar_show_tween = create_tween() scrollbar_show_tween.set_parallel(true) scrollbar_show_tween.tween_property(get_v_scroll_bar(), 'modulate', Color.WHITE, time) scrollbar_show_tween.tween_property(get_h_scroll_bar(), 'modulate', Color.WHITE, time) ## Scroll to position to ensure the given control node is visible func ensure_control_visible(control: Control) -> void: if !content_node: return if !content_node.is_ancestor_of(control): return if !scroll_damper: return var size_diff = ( control.get_global_rect().size - get_global_rect().size ) / (get_global_rect().size / size) var boundary_dist = get_child_boundary_dist( (control.global_position - global_position) \ / (get_global_rect().size / size), size_diff ) var content_node_position = content_node.position if boundary_dist.x < 0 + follow_focus_margin: scroll_x_to(content_node_position.x - boundary_dist.x + follow_focus_margin) elif boundary_dist.y > 0 - follow_focus_margin: scroll_x_to(content_node_position.x - boundary_dist.y - follow_focus_margin) if boundary_dist.z < 0 + follow_focus_margin: scroll_y_to(content_node_position.y - boundary_dist.z + follow_focus_margin) elif boundary_dist.w > 0 - follow_focus_margin: scroll_y_to(content_node_position.y - boundary_dist.w - follow_focus_margin) #endregion