Use a 9-slice texture to draw a button outline.

fixes #379

Also using separate texture for the selection/pressed state of buttons and checkboxes to allow more artistic freedom.
This commit is contained in:
IntegratedQuantum 2024-05-24 14:50:50 +02:00
parent a9c21ed93f
commit 448f2b0f1f
17 changed files with 161 additions and 39 deletions

View File

@ -9,6 +9,8 @@ flat out vec4 fColor;
uniform vec2 start;
uniform vec2 size;
uniform vec2 screen;
uniform vec2 uvOffset;
uniform vec2 uvDim;
uniform int color;
@ -22,5 +24,5 @@ void main() {
gl_Position = vec4(position, 0, 1);
fColor = vec4((color & 0xff0000)>>16, (color & 0xff00)>>8, color & 0xff, (color>>24) & 255)/255.0;
uv = vertex_pos;
uv = uvOffset + vertex_pos*uvDim;
}

View File

@ -0,0 +1,9 @@
#version 330
layout (location=0) out vec4 frag_color;
flat in vec4 color;
void main(){
frag_color = color;
}

View File

@ -0,0 +1,27 @@
#version 330 core
layout (location=0) in vec4 vertex_pos;
flat out vec4 color;
//in pixel
uniform vec2 start;
uniform vec2 size;
uniform vec2 screen;
uniform float lineWidth;
uniform int rectColor;
void main() {
// Convert to opengl coordinates:
vec2 position_percentage = (start + vertex_pos.xy*size + vertex_pos.zw*lineWidth)/screen;
vec2 position = vec2(position_percentage.x, -position_percentage.y)*2+vec2(-1, 1);
gl_Position = vec4(position, 0, 1);
color = vec4((rectColor & 0xff0000)>>16, (rectColor & 0xff00)>>8, rectColor & 0xff, (rectColor>>24) & 255)/255.0;;
}

View File

@ -6,14 +6,10 @@ uniform sampler2D image;
flat in vec4 fColor;
in vec2 startCoord;
uniform bool pressed;
uniform float scale;
void main() {
frag_color = texture(image, (gl_FragCoord.xy - startCoord)/(2*scale)/textureSize(image, 0));
frag_color.a *= fColor.a;
frag_color.rgb += fColor.rgb;
if(pressed) {
frag_color.rgb *= 0.8;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -396,6 +396,8 @@ pub const draw = struct {
size: c_int,
image: c_int,
color: c_int,
uvOffset: c_int,
uvDim: c_int,
} = undefined;
var imageShader: Shader = undefined;
@ -422,6 +424,30 @@ pub const draw = struct {
c.glUniform2f(imageUniforms.start, pos[0], pos[1]);
c.glUniform2f(imageUniforms.size, dim[0], dim[1]);
c.glUniform1i(imageUniforms.color, @bitCast(color));
c.glUniform2f(imageUniforms.uvOffset, 0, 0);
c.glUniform2f(imageUniforms.uvDim, 1, 1);
c.glBindVertexArray(rectVAO);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
}
pub fn boundSubImage(_pos: Vec2f, _dim: Vec2f, uvOffset: Vec2f, uvDim: Vec2f) void {
var pos = _pos;
var dim = _dim;
pos *= @splat(scale);
pos += translation;
dim *= @splat(scale);
pos = @floor(pos);
dim = @ceil(dim);
imageShader.bind();
c.glUniform2f(imageUniforms.screen, @floatFromInt(Window.width), @floatFromInt(Window.height));
c.glUniform2f(imageUniforms.start, pos[0], pos[1]);
c.glUniform2f(imageUniforms.size, dim[0], dim[1]);
c.glUniform1i(imageUniforms.color, @bitCast(color));
c.glUniform2f(imageUniforms.uvOffset, uvOffset[0], 1 - uvOffset[1] - uvDim[1]);
c.glUniform2f(imageUniforms.uvDim, uvDim[0], uvDim[1]);
c.glBindVertexArray(rectVAO);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
@ -1697,6 +1723,14 @@ pub const Texture = struct {
self.bindTo(0);
draw.boundImage(pos, dim);
}
pub fn size(self: Texture) Vec2i {
self.bind();
var result: Vec2i = undefined;
c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_WIDTH, &result[0]);
c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_HEIGHT, &result[1]);
return result;
}
};
pub const CubeMapTexture = struct {

View File

@ -19,7 +19,31 @@ const Button = @This();
const border: f32 = 3;
const fontSize: f32 = 16;
var texture: Texture = undefined;
const Textures = struct {
texture: Texture,
outlineTexture: Texture,
outlineTextureSize: Vec2f,
pub fn init(basePath: []const u8) Textures {
var self: Textures = undefined;
const buttonPath = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}.png", .{basePath}) catch unreachable;
defer main.stackAllocator.free(buttonPath);
self.texture = Texture.initFromFile(buttonPath);
const outlinePath = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}_outline.png", .{basePath}) catch unreachable;
defer main.stackAllocator.free(outlinePath);
self.outlineTexture = Texture.initFromFile(outlinePath);
self.outlineTextureSize = @floatFromInt(self.outlineTexture.size());
return self;
}
pub fn deinit(self: Textures) void {
self.texture.deinit();
self.outlineTexture.deinit();
}
};
var normalTextures: Textures = undefined;
var hoveredTextures: Textures = undefined;
var pressedTextures: Textures = undefined;
pub var shader: Shader = undefined;
pub var buttonUniforms: struct {
screen: c_int,
@ -29,7 +53,6 @@ pub var buttonUniforms: struct {
scale: c_int,
image: c_int,
pressed: c_int,
} = undefined;
pos: Vec2f,
@ -43,12 +66,16 @@ pub fn __init() void {
shader = Shader.initAndGetUniforms("assets/cubyz/shaders/ui/button.vs", "assets/cubyz/shaders/ui/button.fs", &buttonUniforms);
shader.bind();
graphics.c.glUniform1i(buttonUniforms.image, 0);
texture = Texture.initFromFile("assets/cubyz/ui/button.png");
normalTextures = Textures.init("assets/cubyz/ui/button");
hoveredTextures = Textures.init("assets/cubyz/ui/button_hovered");
pressedTextures = Textures.init("assets/cubyz/ui/button_pressed");
}
pub fn __deinit() void {
shader.deinit();
texture.deinit();
normalTextures.deinit();
hoveredTextures.deinit();
pressedTextures.deinit();
}
fn defaultOnAction(_: usize) void {}
@ -106,20 +133,33 @@ pub fn mainButtonReleased(self: *Button, mousePosition: Vec2f) void {
}
pub fn render(self: *Button, mousePosition: Vec2f) void {
texture.bindTo(0);
const textures = if(self.pressed)
pressedTextures
else if(GuiComponent.contains(self.pos, self.size, mousePosition) and self.hovered)
hoveredTextures
else normalTextures;
textures.texture.bindTo(0);
shader.bind();
graphics.c.glUniform1i(buttonUniforms.pressed, 0);
if(self.pressed) {
draw.setColor(0xff000000);
graphics.c.glUniform1i(buttonUniforms.pressed, 1);
} else if(GuiComponent.contains(self.pos, self.size, mousePosition) and self.hovered) {
draw.setColor(0xff000040);
} else {
draw.setColor(0xff000000);
}
self.hovered = false;
draw.customShadedRect(buttonUniforms, self.pos, self.size);
graphics.c.glUniform1i(buttonUniforms.pressed, 0);
draw.customShadedRect(buttonUniforms, self.pos + Vec2f{2, 2}, self.size - Vec2f{4, 4});
{ // Draw the outline using the 9-slice texture.
const cornerSize = (textures.outlineTextureSize - Vec2f{1, 1});
const cornerSizeUV = (textures.outlineTextureSize - Vec2f{1, 1})/Vec2f{2, 2}/textures.outlineTextureSize;
const lowerTexture = (textures.outlineTextureSize - Vec2f{1, 1})/Vec2f{2, 2}/textures.outlineTextureSize;
const upperTexture = (textures.outlineTextureSize + Vec2f{1, 1})/Vec2f{2, 2}/textures.outlineTextureSize;
textures.outlineTexture.bindTo(0);
draw.setColor(0xffffffff);
// Corners:
graphics.draw.boundSubImage(self.pos + Vec2f{0, 0}, cornerSize, .{0, 0}, cornerSizeUV);
graphics.draw.boundSubImage(self.pos + Vec2f{self.size[0], 0} - Vec2f{cornerSize[0], 0}, cornerSize, .{upperTexture[0], 0}, cornerSizeUV);
graphics.draw.boundSubImage(self.pos + Vec2f{0, self.size[1]} - Vec2f{0, cornerSize[1]}, cornerSize, .{0, upperTexture[1]}, cornerSizeUV);
graphics.draw.boundSubImage(self.pos + self.size - cornerSize, cornerSize, upperTexture, cornerSizeUV);
// Edges:
graphics.draw.boundSubImage(self.pos + Vec2f{cornerSize[0], 0}, Vec2f{self.size[0] - 2*cornerSize[0], cornerSize[1]}, .{lowerTexture[0], 0}, .{upperTexture[0] - lowerTexture[0], cornerSizeUV[1]});
graphics.draw.boundSubImage(self.pos + Vec2f{cornerSize[0], self.size[1] - cornerSize[1]}, Vec2f{self.size[0] - 2*cornerSize[0], cornerSize[1]}, .{lowerTexture[0], upperTexture[1]}, .{upperTexture[0] - lowerTexture[0], cornerSizeUV[1]});
graphics.draw.boundSubImage(self.pos + Vec2f{0, cornerSize[1]}, Vec2f{cornerSize[0], self.size[1] - 2*cornerSize[1]}, .{0, lowerTexture[1]}, .{cornerSizeUV[0], upperTexture[1] - lowerTexture[1]});
graphics.draw.boundSubImage(self.pos + Vec2f{self.size[0] - cornerSize[0], cornerSize[1]}, Vec2f{cornerSize[0], self.size[1] - 2*cornerSize[1]}, .{upperTexture[0], lowerTexture[1]}, .{cornerSizeUV[0], upperTexture[1] - lowerTexture[1]});
}
const textPos = self.pos + self.size/@as(Vec2f, @splat(2.0)) - self.child.size()/@as(Vec2f, @splat(2.0));
self.child.mutPos().* = textPos;
self.child.render(mousePosition - self.pos);

View File

@ -21,8 +21,12 @@ const border: f32 = 3;
const fontSize: f32 = 16;
const boxSize: f32 = 16;
var textureChecked: Texture = undefined;
var textureEmpty: Texture = undefined;
var textureCheckedNormal: Texture = undefined;
var textureCheckedHovered: Texture = undefined;
var textureCheckedPressed: Texture = undefined;
var textureEmptyNormal: Texture = undefined;
var textureEmptyHovered: Texture = undefined;
var textureEmptyPressed: Texture = undefined;
pos: Vec2f,
size: Vec2f,
@ -33,13 +37,21 @@ onAction: *const fn(bool) void,
label: *Label,
pub fn __init() void {
textureChecked = Texture.initFromFile("assets/cubyz/ui/checked_box.png");
textureEmpty = Texture.initFromFile("assets/cubyz/ui/box.png");
textureCheckedNormal = Texture.initFromFile("assets/cubyz/ui/checked_box.png");
textureCheckedHovered = Texture.initFromFile("assets/cubyz/ui/checked_box_hovered.png");
textureCheckedPressed = Texture.initFromFile("assets/cubyz/ui/checked_box_pressed.png");
textureEmptyNormal = Texture.initFromFile("assets/cubyz/ui/box.png");
textureEmptyHovered = Texture.initFromFile("assets/cubyz/ui/box_hovered.png");
textureEmptyPressed = Texture.initFromFile("assets/cubyz/ui/box_pressed.png");
}
pub fn __deinit() void {
textureChecked.deinit();
textureEmpty.deinit();
textureCheckedNormal.deinit();
textureCheckedHovered.deinit();
textureCheckedPressed.deinit();
textureEmptyNormal.deinit();
textureEmptyHovered.deinit();
textureEmptyPressed.deinit();
}
pub fn init(pos: Vec2f, width: f32, text: []const u8, initialValue: bool, onAction: *const fn(bool) void) *CheckBox {
@ -86,23 +98,25 @@ pub fn mainButtonReleased(self: *CheckBox, mousePosition: Vec2f) void {
pub fn render(self: *CheckBox, mousePosition: Vec2f) void {
if(self.state) {
textureChecked.bindTo(0);
if(self.pressed) {
textureCheckedPressed.bindTo(0);
} else if(GuiComponent.contains(self.pos, self.size, mousePosition) and self.hovered) {
textureCheckedHovered.bindTo(0);
} else {
textureCheckedNormal.bindTo(0);
}
} else {
textureEmpty.bindTo(0);
if(self.pressed) {
textureEmptyPressed.bindTo(0);
} else if(GuiComponent.contains(self.pos, self.size, mousePosition) and self.hovered) {
textureEmptyHovered.bindTo(0);
} else {
textureEmptyNormal.bindTo(0);
}
}
Button.shader.bind();
graphics.c.glUniform1i(Button.buttonUniforms.pressed, 0);
if(self.pressed) {
draw.setColor(0xff000000);
graphics.c.glUniform1i(Button.buttonUniforms.pressed, 1);
} else if(GuiComponent.contains(self.pos, self.size, mousePosition) and self.hovered) {
draw.setColor(0xff000040);
} else {
draw.setColor(0xff000000);
}
self.hovered = false;
draw.customShadedRect(Button.buttonUniforms, self.pos + Vec2f{0, self.size[1]/2 - boxSize/2}, @as(Vec2f, @splat(boxSize)));
graphics.c.glUniform1i(Button.buttonUniforms.pressed, 0);
const textPos = self.pos + Vec2f{boxSize/2, 0} + self.size/@as(Vec2f, @splat(2.0)) - self.label.size/@as(Vec2f, @splat(2.0));
self.label.pos = textPos;
self.label.render(mousePosition - textPos);