Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,39 @@ drawn in the same text still work. `ClickCounter` and `text_word_range` cover
double-click word selection, and `text_column_at_x` maps an x offset to a
column given a prefix-width function.

## Scrolling

A `Scroll` element scrolls over its children, vertically unless told otherwise.
`scroll_mode`, named after vlang/gui's setting, chooses the axes:
`vertical_only` (the default), `horizontal_only`, or `both`. The content is as
wide as its rightmost child reaches and as tall as its lowest, and whatever
overflows along an axis the mode leaves out is clipped.

```v
ui2.scroll_with_mode('columns', frame, 0xffffff, .horizontal_only, columns)
```

```vml
Scroll {
id: columns
scroll_mode: horizontal_only
}
```

The horizontal scroller runs along the bottom edge. A trackpad or a tilt wheel
scrolls sideways directly, and Shift turns a plain wheel sideways. Scroll
elements nest: a gesture along an axis the element under the pointer does not
scroll moves the nearest enclosing one that does, so a strip of columns that
each scroll vertically still scrolls sideways with the pointer over a column.

`scroll_offset(id)` and `scroll_horizontal_offset(id)` read the position,
`scroll_to_offset`, `scroll_to_horizontal_offset` and `scroll_to_rect` set it,
and `on_scroll` reports a change along either axis. An offset asked for before
the element exists is taken up when it is first laid out. To scroll to content
the event being handled has just added, call `refresh()` first so the element
has its new range. The iOS backend scrolls the same modes but does not expose
these functions.

## Fonts and text sizes

`TextStyle.size` is in points. Win32 and the Linux desktops resolve a point at
Expand Down Expand Up @@ -920,6 +953,8 @@ Typed-VML ports from `v-ui` include:
scrollable multiline text areas.
- `v run examples/scrollview/main.v` — two independently scrollable read-only
text panes with generated content.
- `v run examples/horizontal_scroll/main.v` — a column browser whose strip of
columns scrolls sideways while each column scrolls its own rows.
- `v run examples/box_layout_with_textbox/main.v` — fixed and proportional box
layout with an editable multiline text area.
- `v run examples/files_dropped/main.v` — collect dropped files and plain text
Expand Down
316 changes: 302 additions & 14 deletions appkit/ui_macos_darwin.v

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions appkit/ui_macos_geometry_darwin.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,14 @@ static inline ui2_macos_rect ui2_macos_msg_rect_rect(void* obj, void* sel, ui2_m
return ((ui2_macos_rect (*)(void*, void*, ui2_macos_rect))objc_msgSend)(obj, sel, rect);
#endif
}

// A class built at runtime has no `super` to send to, so reaching the method it
// overrides means naming the class the lookup should start from. objc_super is a
// receiver followed by that class; plain pointers keep this free of ownership casts.
static inline void ui2_macos_msg_super_void_id(void* obj, void* superclass, void* sel, void* arg) {
struct {
void* receiver;
void* super_class;
} target = { obj, superclass };
((void (*)(void*, void*, void*))objc_msgSendSuper)(&target, sel, arg);
}
126 changes: 125 additions & 1 deletion appkit/ui_macos_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ fn test_macos_native_style_button_keeps_appkit_bezel_and_press_state() {
}

fn test_macos_transparent_box_controls_disable_native_backgrounds() {
ensure_runtime_classes()
pool := macos.autorelease_pool_new()
defer {
macos.release(pool)
Expand All @@ -186,7 +187,8 @@ fn test_macos_transparent_box_controls_disable_native_backgrounds() {
}
button_view := native_new_button(native_rect(0, 0, 96, 40), 'Clear', BoxStyle{},
0xffffff, 15, false, false, false, 0, '', true)
scroll_view := native_new_scroll(native_rect(0, 0, 120, 80), BoxStyle{}, false)
scroll_view := native_new_scroll(native_rect(0, 0, 120, 80), BoxStyle{}, false,
.vertical_only)
toggle_view := native_new_toggle_button(toggle_button(
title: 'Clear toggle'
box: transparent_box
Expand Down Expand Up @@ -224,6 +226,128 @@ fn test_macos_transparent_box_controls_disable_native_backgrounds() {
assert !macos.msg_bool(text_view, 'drawsBackground')
}

fn test_macos_scroll_mode_selects_scrollers_and_document_size() {
ensure_runtime_classes()
pool := macos.autorelease_pool_new()
defer {
macos.release(pool)
}
columns := scroll_with_mode('test-columns', rect(0, 0, 120, 80), 0xffffff, .horizontal_only, [
view('', rect(0, 0, 200, 80), BoxStyle{}, []),
view('', rect(200, 0, 200, 300), BoxStyle{}, []),
])
scroll_view := native_new_scroll(element_rect(columns.frame), columns.box, false,
columns.scroll_mode)
defer {
macos.release(scroll_view)
}
assert macos.msg_bool(scroll_view, 'hasHorizontalScroller')
assert !macos.msg_bool(scroll_view, 'hasVerticalScroller')

// A sideways strip is as wide as its columns and no taller than what shows.
assert scroll_document_size(columns, 120, 80) == ScrollDocument{
width: 400
height: 80
follows_height: true
}
canvas := Element{
...columns
scroll_mode: .both
}
assert scroll_document_size(canvas, 120, 80) == ScrollDocument{
width: 400
height: 316
}
// A vertical list is held to the width that shows, however far its children reach,
// so a scroller taking room of its own leaves no sideways range behind it.
assert scroll_document_size(Element{
...columns
scroll_mode: .vertical_only
}, 105, 80) == ScrollDocument{
width: 105
height: 316
follows_width: true
}
// Content that fits across is held to the width that shows, which a scroller
// taking room of its own makes less than the frame, so no sideways range is left.
narrow := Element{
...canvas
children: [view('', rect(0, 0, 100, 300), BoxStyle{}, [])]
}
fitted := scroll_document_size(narrow, 105, 65)
assert fitted == ScrollDocument{
width: 105
height: 316
follows_width: true
}
assert fitted.autoresizing_mask() == ns_view_width_sizable
assert scroll_document_size(columns, 120, 65).autoresizing_mask() == ns_view_height_sizable
// Before the scroll view has been laid out there is only the frame to go by.
assert scroll_document_size(narrow, 0, 0).width == 120

native_set_scroll_axes(scroll_view, .both)
assert macos.msg_bool(scroll_view, 'hasVerticalScroller')
assert macos.msg_bool(scroll_view, 'hasHorizontalScroller')
}

fn test_macos_scroll_gesture_passes_to_an_enclosing_view_only_when_it_cannot_be_followed() {
ensure_runtime_classes()
pool := macos.autorelease_pool_new()
defer {
macos.release(pool)
}
outer := native_new_scroll(native_rect(0, 0, 400, 300), BoxStyle{}, false, .horizontal_only)
defer {
macos.release(outer)
}
outer_document := native_new_flipped_view(native_rect(0, 0, 1200, 300), BoxStyle{})
native_set_document_view(outer, outer_document)
macos.release(outer_document)
inner := native_new_scroll(native_rect(0, 0, 200, 150), BoxStyle{}, false, .horizontal_only)
inner_document := native_new_flipped_view(native_rect(0, 0, 200, 150), BoxStyle{})
native_set_document_view(inner, inner_document)
macos.release(inner_document)
native_add_subview(outer_document, inner)
macos.release(inner)

assert scroll_view_scrolls_axis(outer, true)
assert !scroll_view_scrolls_axis(outer, false)
// Content that fits leaves the inner strip nowhere to go, so a sideways gesture
// over it belongs to the strip around it.
assert !scroll_view_scrolls_axis(inner, true)
assert scroll_view_passes_gesture(inner, true, -1)
// Both strips are at their start, so no one can take a gesture heading back that
// way, and nothing here scrolls vertically at all.
assert !scroll_view_passes_gesture(inner, true, 1)
assert !scroll_view_passes_gesture(inner, false, -1)
// Once its content overflows it keeps the gesture itself...
native_set_frame(inner_document, native_rect(0, 0, 800, 150))
assert scroll_view_can_move(inner, true, -1)
assert !scroll_view_passes_gesture(inner, true, -1)
// ...until it has run out, where further the same way is the outer strip's again,
// while the way back is still its own.
macos.msg_void_point(macos.msg_id(inner, 'contentView'), 'setBoundsOrigin:', macos.point(600,
0))
assert !scroll_view_can_move(inner, true, -1)
assert scroll_view_passes_gesture(inner, true, -1)
assert scroll_view_can_move(inner, true, 1)
assert !scroll_view_passes_gesture(inner, true, 1)
// A view with nothing around it keeps even what it cannot use.
assert !scroll_view_passes_gesture(outer, true, 1)

// Ten short of its end, the inner strip has room for ten of a 48 point step and
// the strip around it for the other 38; the way back is all its own.
macos.msg_void_point(macos.msg_id(inner, 'contentView'), 'setBoundsOrigin:', macos.point(590,
0))
assert scroll_view_room(inner, true, 48) == 10
assert scroll_view_room(outer, true, 48 - 10) == 38
assert scroll_view_room(inner, true, -48) == -48
assert scroll_view_room(inner, true, -1000) == -590
// Neither has any along the axis it does not scroll, nor the outer back past its start.
assert scroll_view_room(inner, false, 48) == 0
assert scroll_view_room(outer, true, -48) == 0
}

fn test_macos_text_field_uses_native_bezel_without_layer_mask() {
pool := macos.autorelease_pool_new()
defer {
Expand Down
39 changes: 39 additions & 0 deletions examples/horizontal_scroll/horizontal_scroll_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module main

import ui2

fn test_column_browser_is_a_sideways_strip_of_vertically_scrolling_columns() {
browser := &ColumnBrowser{
selected: [3, 2]
}
root := browser_screen(browser, ui2.rect(0, 0, 720, 440))
ui2.validate_element_tree(root) or { panic(err) }
strip := root.children.last()
assert strip.kind == .scroll
assert strip.id == 'columns'
assert strip.scroll_mode == .horizontal_only
// A column and its divider for each chosen folder, and for the one left open.
assert strip.children.len == 6
for index in 0 .. 3 {
column := strip.children[index * 2]
assert column.kind == .scroll
assert column.scroll_mode == .vertical_only
assert column.frame.x == index * columns_column_width
assert column.frame.height == strip.frame.height
}
assert browser.content_width() == 3 * columns_column_width
assert browser.path() == '/Documents/Pictures'
}

fn test_opening_a_row_replaces_the_columns_to_its_right() {
mut browser := ColumnBrowser{
selected: [3, 2, 6, 1]
}
browser.open(1, 4)
assert browser.selected == [3, 4]
assert browser.column_count() == 3
// A row the column does not have, or a column that is not open, changes nothing.
browser.open(0, 500)
browser.open(7, 0)
assert browser.selected == [3, 4]
}
Loading
Loading