scroll list and minor fixes

This commit is contained in:
Ronja 2020-10-11 17:22:57 +02:00
parent cbb2a4c405
commit 3ed3736631
10 changed files with 445 additions and 97 deletions

View file

@ -0,0 +1,3 @@
image = {
source = "assets/wip/Knob3.png"
}

BIN
Luxe/assets/wip/Knob3.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -16,8 +16,9 @@ import "blocks/ui/simple_text" for UISimpleText
import "blocks/ui/ui" for Ui
import "blocks/ui/slider" for UiSlider
import "blocks/ui/box" for UiBox
import "blocks/ui/scroll_box" for UiScrollBox
import "blocks/ui/compass" for UiCompass
import "blocks/ui/info" for UiInfo //this is a cyclic dependency waiting to happen
import "blocks/ui/info" for UiInfo //this is a cyclic dependency waiting to happen...
import "math/math" for M
import "math/util" for Util
import "blocks/human/human" for Human
@ -197,10 +198,8 @@ class UiAdventure{
}
}
var list = UiBox.create(_ent)
var list = UiScrollBox.create(_ent)
Control.child_add(page, list)
Control.set_clip(list, true)
UILayout.set_contain(_ent, list, UILayoutContain.column | UILayoutContain.start) //|
UILayout.set_behave(_ent, list, UILayoutBehave.fill)
UILayout.set_margin(_ent, list, 40, 0, 0, 0)
@ -218,14 +217,16 @@ class UiAdventure{
if(!adventure) return
//rebuild
var i = -1
adventure.adventurers.each{ |adventurer|
i = i + 1
var item = UiBox.create(_ent)
Control.child_add(list, item)
UILayout.set_behave(_ent, item, UILayoutBehave.left | UILayoutBehave.right)//|
UILayout.set_margin(_ent, item, 0, 0, 0, 0)
UILayout.set_contain(_ent, item, UILayoutContain.row | UILayoutContain.start)//|
Control.set_size(item, -1, 12)
Control.set_allow_input(item, true)
Control.set_id(item, "Person %(i)")
Control.set_events(item) {|event|
if(UI.event_cancelled(_ent, event.id)) return
if(event.type == UIEvent.press){
@ -234,6 +235,7 @@ class UiAdventure{
_game.focus.value = adventurer
}
}
UiScrollBox.add(list, item)
var head = UIImage.create(_ent)
Control.child_add(item, head)
@ -284,6 +286,82 @@ class UiAdventure{
_page.on_change(true){|val|
Control.set_visible(page, val == UiAdventure.resources)
}
var list = UiScrollBox.create(_ent)
Control.child_add(page, list)
UILayout.set_behave(_ent, list, UILayoutBehave.fill)
UILayout.set_margin(_ent, list, 40, 0, 0, 0)
var tiny_head = Assets.image("assets/wip/8Head")
var x_image = Assets.image("assets/wip/8Cross")
_itemListItems = []
_game.adventures.planning.on_change(true) {|adventure|
//cleanup
_itemListItems.each{|item|
Control.destroy(item)
}
_itemListItems.clear()
if(!adventure) return
//rebuild
adventure.adventurers.each{ |adventurer|
var item = UiBox.create(_ent)
UILayout.set_behave(_ent, item, UILayoutBehave.left | UILayoutBehave.right)//|
UILayout.set_margin(_ent, item, 0, 0, 0, 0)
UILayout.set_contain(_ent, item, UILayoutContain.row | UILayoutContain.start)//|
Control.set_size(item, -1, 12)
Control.set_allow_input(item, true)
Control.set_events(item) {|event|
if(UI.event_cancelled(_ent, event.id)) return
if(event.type == UIEvent.press){
_ui.ui_mode = Ui.Info
_ui.info.page.value = UiInfo.human
_game.focus.value = adventurer
}
}
UiScrollBox.add(list, item)
var head = UIImage.create(_ent)
Control.child_add(item, head)
Control.set_size(head, 8, 8)
UIImage.set_image(head, tiny_head)
UIImage.set_color(head, Human.get_color(adventurer))
UILayout.set_margin(_ent, head, 2, 0, 0, 0)
var name = UILabel.create(_ent)
Control.child_add(item, name)
UILabel.set_align_vertical(name, TextAlign.bottom)
UILabel.set_font(name, _ui.font)
UILabel.set_text_size(name, 8)
UILabel.set_text(name, Human.get_name(adventurer))
UILayout.set_behave(_ent, name, UILayoutBehave.hfill | UILayoutBehave.left)//|
UILayout.set_margin(_ent, name, 2, 1, 0, 1)
var remove = ImageButton.create(_ent)
Control.child_add(item, remove)
Control.set_size(remove, 8, 8)
UIImage.set_image(remove, x_image)
UIImage.set_color(remove, Color.hex(0xec172a))
UILayout.set_margin(_ent, remove, 0, 0, 2, 0)
ImageButton.set_tooltip(remove, "remove")
ImageButton.set_state_change(remove) { |data, button|
if(data["press"]){
Frame.end{
Globals["Tooltip"].clear(button)
Util.remove(adventure.adventurers, adventurer)
_game.adventures.planning.emit()
}
}
}
_itemListItems.add(item)
}
UILayout.commit(_ent)
UI.commit(_ent)
}
}
depart(){

View file

@ -37,7 +37,7 @@ class UiCompass{
if(event.control != control) return
if(event.type == UIEvent.press){
var relative_pos = AABB.grow(AABB.new(x, y, w, h),[2, 2]).relative_pos([event.x, event.y])
var relative_pos = AABB.new(x, y, w, h).relative_pos([event.x, event.y])
var inside = M.length(relative_pos.map{|comp| comp - 0.5}) < 0.5
if(inside) {
state["pressed"] = true
@ -46,10 +46,11 @@ class UiCompass{
} else if(event.type == UIEvent.release){
state["pressed"] = false
UI.uncapture(control)
} else if(event.type == UIEvent.move && state["pressed"]){
}
if((event.type == UIEvent.move || event.type == event.type == UIEvent.press) && state["pressed"]){
var center = [x + w/2, y + h/2]
var diff = [event.x - center.x, event.y - center.y]
var angle = Math.atan2(-diff.y, diff.x) - Num.tau / 4
var angle = Math.atan2(-diff.y, diff.x) - Num.tau / 4 //flip y because ui coordinates are different; shift by 1/4 circle constant to make up 0°
UI.events_emit(control, UIEvent.change, Math.degrees(angle))
}
}

View file

@ -0,0 +1,151 @@
import "luxe: ui/control" for Control
import "luxe: world" for UI, World, UIEvent, UILayout, UILayoutContain, UILayoutBehave
import "luxe: draw" for PathStyle
import "luxe: assets" for Assets
import "luxe: render" for Image
import "luxe: math" for Math
import "luxe: game" for Frame
import "globals" for Globals
import "math/rect" for AABB
import "math/math" for M
import "blocks/debug" for DrawDebug
import "blocks/ui/slider" for UiSlider
class UiScrollBox{
static create(ent){
var box = Control.create(ent)
var style = PathStyle.new()
style.color = [1,1,1,1]
style.thickness = 1
Control.set_allow_input(box, true)
var state = {"style": style, "pressed": false, "position": 0, "scrollSpeed": 4, "ent": ent, "spring": 32, "contentHeight": 0}
Control.set_state_data(box, state)
var slider = UiSlider.create(ent)
Control.child_add(box, slider)
UiSlider.set_line(slider, false)
UiSlider.set_direction(slider, UiSlider.vertical)
UiSlider.set_handle(slider, Assets.image("assets/wip/Knob3"))
UiSlider.set_value(slider, 0)
Control.set_size(slider, 7, -1)
UILayout.set_behave(ent, slider, UILayoutBehave.right | UILayoutBehave.vfill) //|
UILayout.set_margin(ent, slider, 1, 0, 1, 0)
Control.set_events(slider){|event|
if(event.type == UIEvent.change){
var position = 0
var max_height = (Control.get_height(state["childContainer"]) + 1) - state["contentHeight"]
if(max_height < 0){
position = M.lerp(0, max_height, event.change)
}
set_position(box, position)
}
}
state["slider"] = slider
var childContainer = Control.create(ent)
Control.child_add(box, childContainer)
UILayout.set_behave(ent, childContainer, UILayoutBehave.fill)
UILayout.set_margin(ent, childContainer, 1, 1, 7, 2) //don't include border, fix bottom bug, freedom to side slider
Control.set_clip(childContainer, true)
UILayout.set_contain(ent, childContainer, UILayoutContain.column | UILayoutContain.start) //|
state["childContainer"] = childContainer
var alignmentChild = Control.create(ent)
Control.child_add(childContainer, alignmentChild)
Control.set_size(alignmentChild, -1, 0)
UILayout.set_behave(ent, alignmentChild, UILayoutBehave.hfill | UILayoutBehave.top) //| //make it wide
UILayout.set_margin(ent, alignmentChild, 0, state["position"], 0, 0) //pos will always be 0 here, but writing it explicitly makes intent clearer
state["alignmentChild"] = alignmentChild
Control.set_render(box) {|control, state, x, y, w, h|
var scrollPos = state["position"]
if(scrollPos >= 0.5){
scrollPos = M.lerp(scrollPos, 0, M.pow2(-state["spring"] * Globals["Delta"]))
set_position(control, scrollPos, true)
}
var container_height = Control.get_height(childContainer) + 1 //+1 is here to fix clip bug
var max_pos = Math.min(container_height - state["contentHeight"], 0)
if(scrollPos <= max_pos - 0.5){
scrollPos = M.lerp(scrollPos, max_pos, M.pow2(-state["spring"] * Globals["Delta"]))
set_position(control, scrollPos, true)
}
var depth = UI.draw_depth_of(control, 0)
UI.draw_rect(control, x+0.5, y+0.5, depth, w-1, h-1, 0, state["style"])
}
Control.set_process(box){|control, state, event, x, y, w, h|
if(event.control != control) return
if(event.type == UIEvent.scroll){
var scrollPos = state["position"]
scrollPos = scrollPos + event.y * state["scrollSpeed"] * -1
set_position(control, scrollPos)
}
}
return box
}
static add(box, child){
var state = Control.get_state_data(box)
Control.child_add(state["childContainer"], child)
var ent = state["ent"]
UILayout.commit(ent)
UI.commit(ent)
update_content(box)
}
static remove(box, child){
var state = Control.get_state_data(box)
Control.child_remove(state["childContainer"], child)
var ent = state["ent"]
UILayout.commit(ent)
UI.commit(ent)
update_content(box)
}
static update_content(box){
var state = Control.get_state_data(box)
var container = state["childContainer"]
var child_count = Control.child_count(container)
//if only the alignmentChild exists
if(child_count <= 1){
state["contentHeight"] = 0
return
}
var first_child = Control.child_get(container, 1)
var upper_bound = Control.get_pos_y_abs(first_child)
var last_child = Control.child_get(container, child_count - 1)
var lower_bound = Control.get_pos_y_abs(last_child) + Control.get_height(last_child)
state["contentHeight"] = lower_bound - upper_bound //its lower - upper because of the y-negative coordinates
}
static set_position(box, position) {
set_position(box, position, false)
}
static set_position(box, position, delay_commit){
var state = Control.get_state_data(box)
state["position"] = position
var ent = state["ent"]
UILayout.set_margin(ent, state["alignmentChild"], 0, position, 0, 0)
if(!delay_commit){
UILayout.commit(ent)
} else {
Frame.end{
UILayout.commit(ent)
}
}
var rel_pos = 0
var max_height = (Control.get_height(state["childContainer"]) + 1) - state["contentHeight"]
if(max_height < 0){
rel_pos = M.clamp(M.inv_lerp(0, max_height, position), 0, 1)
}
UiSlider.set_value(state["slider"], rel_pos)
}
}

View file

@ -11,25 +11,44 @@ import "math/math" for M
import "blocks/debug" for DrawDebug
class UiSlider{
static horizontal{"horiz"}
static vertical{"vert"}
static create(ent){
var compass = Control.create(ent)
var slider = Control.create(ent)
var style = PathStyle.new()
style.color = [1,1,1,1]
style.thickness = 1
Control.set_allow_input(compass, true)
Control.set_state_data(compass, {"value": 0.5, "style": style, "pressed": false}) //angle is in radians between 0 and tau
Control.set_render(compass) {|control, state, x, y, w, h|
Control.set_allow_input(slider, true)
var state = {"value": 0.2,
"style": style,
"pressed": false,
"centerLine": true,
"handle": Assets.image("assets/wip/SliderHandle"),
"direction": UiSlider.horizontal
}
Control.set_state_data(slider, state)
Control.set_render(slider) {|control, state, x, y, w, h|
var depth = UI.draw_depth_of(control, 0)
UI.draw_rect(control, x+0.5, y+0.5, depth, w-1, h-1, 0, state["style"])
UI.draw_line(control, x + h/2, y + h/2, x + w - h/2, y + h/2, depth, state["style"])
var image = Assets.image("assets/wip/SliderHandle")
if(state["centerLine"]) {
UI.draw_line(control, x + h/2, y + h/2, x + w - h/2, y + h/2, depth, state["style"])
}
var image = state["handle"]
var img_size = [Image.get_width(image), Image.get_height(image)]
UI.draw_image(control, (M.lerp(x+h/2, x+w-h/2, state["value"]) - img_size.x/2).round,
y+h/2-img_size.y/2, depth, img_size.x, img_size.y,
var slider_x
var slider_y
if(state["direction"] == UiSlider.horizontal){
slider_x = (M.lerp(x+h/2, x+w-h/2, state["value"]) - img_size.x/2).round
slider_y = y+h/2-img_size.y/2
} else if(state["direction"] == UiSlider.vertical){
slider_x = x+w/2-img_size.x/2
slider_y = (M.lerp(y+w/2, y+h-w/2, state["value"]) - img_size.y/2).round
}
UI.draw_image(control, slider_x, slider_y, depth, img_size.x, img_size.y,
0, [1,1,1,1], [0, 0, 1, 1], image, true)
}
Control.set_process(compass){|control, state, event, x,y,w,h|
Control.set_process(slider){|control, state, event, x,y,w,h|
if(event.control != control) return
if(event.type == UIEvent.press){
state["pressed"] = true
@ -37,14 +56,36 @@ class UiSlider{
} else if(event.type == UIEvent.release){
state["pressed"] = false
UI.uncapture(control)
} else if(event.type == UIEvent.move && state["pressed"]){
var abs_x = Control.get_pos_x_abs(control)
var rel_pos = M.inv_lerp(abs_x+h/2, abs_x+w-h/2, event.x)
}
if((event.type == UIEvent.move || event.type == UIEvent.press) && state["pressed"]){
var min
var max
var value
if(state["direction"] == UiSlider.horizontal){
var abs_x = Control.get_pos_x_abs(control)
min = abs_x+h/2
max = abs_x+w-h/2
value = event.x
} else if(state["direction"] == UiSlider.vertical){
var abs_y = Control.get_pos_y_abs(control)
min = abs_y+w/2
max = abs_y+h-w/2
value = event.y
}
var rel_pos = M.inv_lerp(min, max, value)
rel_pos = M.clamp(rel_pos, 0, 1)
UI.events_emit(control, UIEvent.change, rel_pos)
}
}
return compass
//default events impl, overridable!
Control.set_events(slider){|event|
if(event.type == UIEvent.change){
set_value(slider, event.change)
}
}
return slider
}
static set_value(slider, value){
@ -52,4 +93,18 @@ class UiSlider{
data["value"] = value
}
static set_line(slider, line){
var data = Control.get_state_data(slider)
data["centerLine"] = line
}
static set_direction(slider, direction){
var data = Control.get_state_data(slider)
data["direction"] = direction
}
static set_handle(slider, handle){
var data = Control.get_state_data(slider)
data["handle"] = handle
}
}

View file

@ -42,6 +42,8 @@ class Game is Ready {
} //ready
tick(delta) {
Globals["Delta"] = delta
var mouse_pos = Vector.new(Input.mouse_x(), Input.mouse_y())
var game_mouse = Globals["Renderer"].game_mouse(mouse_pos)
game_mouse = Camera.screen_point_to_world(app.camera, game_mouse.x, game_mouse.y)

View file

@ -26,6 +26,10 @@ class M{
return result
}
static pow2(value){
return value * value
}
static lerp(from, to, value){
//if the range are numbers, we assume the interpolation value is too
if(from is Num && to is Num) return from + (to - from) * value

View file

@ -1,51 +1,44 @@
input View {
mat4 mvp,
mat4 proj,
mat4 proj_inverse,
mat4 view,
mat4 world, //geometry.world atm
float2 fov, //fov.x, fov.y
float2 resolution,
float4 target_region,
float4 target_region_size
mat4 mvp,
mat4 proj,
mat4 proj_inverse,
mat4 view,
mat4 world, //geometry.world atm
float2 fov, //fov.x, fov.y
float2 resolution,
float4 target_region,
float4 target_region_size
}
input Font {
image2D pages[8]
image2D pages[8]
}
input UI {
float2 canvas_size,
float textured,
float pixelated,
#0 image2D tex_mask,
#1 image2D tex,
#1 image2D tex_pixelated
}
input FUI {
float2 canvas_size,
#0 image2D tex_mask
float2 canvas_size,
#0 image2D tex_mask
}
stage vertex vert(
input { View view },
stage vertex vert(
input { View view, UI ui },
vertex in {
#0 float4 pos,
#1 float4 color,
#2 float2 uv,
#3 float4 data
#3 float4 data,
#4 float2 bounds
},
fragment out {
float4 color,
float2 uv,
float2 mask_uv,
float clip
})
{
out.uv = in.uv;
out.color = in.color;
out.clip = in.data.x;
out.mask_uv = float2(in.data.y, 1.0 - in.data.z);
stage.pos = input.view.mvp * float4(in.pos.xyz, 1.0);
}
stage fragment frag(
input { FUI ui, Font font },
fragment in {
float4 color,
float2 uv,
float2 mask_uv,
@ -53,52 +46,99 @@ stage fragment frag(
float page
})
{
//outside mask uvs?
bool outside_mask = in.mask_uv.x < 0.0 ||
in.mask_uv.x > 1.0 ||
in.mask_uv.y < 0.0 ||
in.mask_uv.y > 1.0;
if(outside_mask) {
discard;
// stage.color[0] = float4(1,0.2,0.3,1);
return;
}
out.uv = in.uv;
out.color = in.color;
out.clip = in.data.x;
out.page = in.pos.w;
float4 mask = texture(input.ui.tex_mask, in.mask_uv);
//for fonts the text pos is text local and needs to be converted to canvas space.
//we pass the bounds of the text item in via the vertices (I know) so we can calculate bounds-local space.
//Then we convert that to canvas space, by * (bounds_w/canvas_w)
//Then finally we add to the canvas local u/v from the data.z/w in
//16 bit mask
// float mask_clip = round(mask.r * 65535.0);
//8 bit mask
int lsb = int(round(mask.r * 255.0));
int msb = int(round(mask.g * 255.0)) * 256; //<< 8;
int mask_clip = lsb | msb;
// int mask_clip = lsb + msb; //webgl1
float bounds_u = 0.0;
float bounds_v = 0.0;
if(in.bounds.x != 0.0) { bounds_u = (in.pos.x / in.bounds.x); }
if(in.bounds.y != 0.0) { bounds_v = 1.0 - (in.pos.y / in.bounds.y); }
//outside our clipping bounds?
int my_clip = int(in.clip);
if(my_clip != 0 && mask_clip < my_clip) {
discard;
// stage.color[0] = float4(0,1,1,0.2);
return;
}
//bounds_u/v is the 0...1 value for where this vert is in the text item itself
//Note we flip the bounds_v because the text is in world space, bottom left origin, y+ up,
//canvas space is top left, y+ down so it has to be reversed to be bounds local in canvas space
int page = int(in.page);
float4 msdf_sample = texture(input.font.pages[0], in.uv.xy);
if(page == 1) { msdf_sample = texture(input.font.pages[1], in.uv.xy); }
if(page == 2) { msdf_sample = texture(input.font.pages[2], in.uv.xy); }
if(page == 3) { msdf_sample = texture(input.font.pages[3], in.uv.xy); }
if(page == 4) { msdf_sample = texture(input.font.pages[4], in.uv.xy); }
if(page == 5) { msdf_sample = texture(input.font.pages[5], in.uv.xy); }
if(page == 6) { msdf_sample = texture(input.font.pages[6], in.uv.xy); }
if(page == 7) { msdf_sample = texture(input.font.pages[7], in.uv.xy); }
float to_canvas_u = in.bounds.x / input.ui.canvas_size.x;
float to_canvas_v = in.bounds.y / input.ui.canvas_size.y;
float r = msdf_sample.r;
float g = msdf_sample.g;
float b = msdf_sample.b;
float median = max(min(r, g), min(max(r, g), b));
float opacity = float(median > 0.5);
//to_canvas_u is the text item width and height in 0...1 relative to the canvas
//e.g if the text item width is 100 and the canvas is 200, the value is 0.5
stage.color[0] = float4(in.color.rgb, in.color.a * opacity * mask.a);
//then what we do is add in.data.y/z which is the text x/y position,
//but relative to the canvas space. we add this to shift the x/y into place.
}
bounds_u = (bounds_u * to_canvas_u) + in.data.y;
bounds_v = (bounds_v * to_canvas_v) + in.data.z;
out.mask_uv = float2(bounds_u, bounds_v);
stage.pos = input.view.mvp * float4(in.pos.xyz, 1.0);
}
stage fragment frag(
input { FUI ui, Font font },
fragment in {
float4 color,
float2 uv,
float2 mask_uv,
float clip,
float page
})
{
//outside mask uvs?
bool outside_mask = in.mask_uv.x < 0.0 ||
in.mask_uv.x > 1.0 ||
in.mask_uv.y < 0.0 ||
in.mask_uv.y > 1.0;
if(outside_mask) {
discard;
// stage.color[0] = float4(1,0.2,0.3,1);
return;
}
float4 mask = texture(input.ui.tex_mask, in.mask_uv);
//16 bit mask
// float mask_clip = round(mask.r * 65535.0);
//8 bit mask
int lsb = int(round(mask.r * 255.0));
int msb = int(round(mask.g * 255.0)) * 256; //<< 8;
int mask_clip = lsb | msb;
// int mask_clip = lsb + msb; //webgl1
//outside our clipping bounds?
int my_clip = int(in.clip);
if(my_clip != 0 && mask_clip < my_clip) {
discard;
// stage.color[0] = float4(0,1,1,0.2);
return;
}
int page = int(in.page);
float4 msdf_sample = texture(input.font.pages[0], in.uv.xy);
if(page == 1) { msdf_sample = texture(input.font.pages[1], in.uv.xy); }
if(page == 2) { msdf_sample = texture(input.font.pages[2], in.uv.xy); }
if(page == 3) { msdf_sample = texture(input.font.pages[3], in.uv.xy); }
if(page == 4) { msdf_sample = texture(input.font.pages[4], in.uv.xy); }
if(page == 5) { msdf_sample = texture(input.font.pages[5], in.uv.xy); }
if(page == 6) { msdf_sample = texture(input.font.pages[6], in.uv.xy); }
if(page == 7) { msdf_sample = texture(input.font.pages[7], in.uv.xy); }
float r = msdf_sample.r;
float g = msdf_sample.g;
float b = msdf_sample.b;
float median = max(min(r, g), min(max(r, g), b));
float opacity = float(median > 0.5);
stage.color[0] = float4(in.color.rgb, in.color.a * opacity * mask.a);
}

View file

@ -1,11 +1,11 @@
material_basis = {
vertex_format = "luxe.textured"
vertex_format = "luxe.ui_font"
shaders = {
vertex = { library="shaders/pixel_text_ui" function="vert" }
fragment = { library="shaders/pixel_text_ui" function="frag" }
}
depth_test = true
depth_write = false
depth_write = true
depth_compare = "less_equal"
stencil_test = false
write_mask = { red=true green=true blue=true alpha=true }
@ -21,6 +21,13 @@ material_basis = {
winding = "counter_clockwise"
layers = ["default"]
inputs = {
ui.tex_mask = {
type = "image"
value = {
type = "image2D"
sampler_state = "nearest_clamp"
}
}
font.pages = {
type = "image"
count = 8
@ -59,5 +66,9 @@ material_basis = {
}
]
}
ui.canvas_size = {
type = "float2"
value = [1 1]
}
}
}