awesomeWM / awesome

awesome window manager
https://awesomewm.org/
GNU General Public License v2.0
6.41k stars 598 forks source link

Question: Scaling a wibox. #3159

Open slickroot opened 4 years ago

slickroot commented 4 years ago

awesome v4.3-862-g7a759432d-dirty (Too long)

While having fun making a battery wibox, I was playing with cairo drawing the shape of the battery and the face, then it stroke me that maybe I will need to scale the whole thing, make it smaller or bigger.

image

I looked around and found awful.placement.scale but the results are not as expected, it looks like only the container wibox is scaled, almost all the sizes stay the same (margins, borders, stroke_size,...). The other limitation is awful.place.scale scales on one direction, I know you can combine multiple awful.placement functions and give one args table, but I guess it works only with different functions, so I can't (correct me if I'm wrong) apply two times awful.place.scale in two directions.

image

Is there another way to scale a wibox as a whole, like scaling an image?

Many thanks!

psychon commented 4 years ago

I am not sure how exactly your wibox is composed, but it does look nice. Well done.

awful.placement.scale seems to only scale the size of the wibox (while also handling placement a bit). It does not do anything with the contents of the wibox.

Scaling a widget

Scaling the contents of a wibox can be done with a new container widget that you wrap around another widget. To my surprise, we do not have anything like that yet. Something like the following (completely untested! I just read the code to wibox.container.mirror and adapted it slightly) could help. This should be a widget that scales inner_widget by factor, but I wouldn't be surprised if I messed up * factor and / factor somewhere (or perhaps the matrix needs 1 / factor instead of factor?):

local inner_widget = whatever
local factor = math.pi
local scale_container = wibox.widget.base.make_widget()
local scale_matrix = gears.matrix.create_scale(factor, factor)
function scale_container:fit(context, width, height)
    local w, h = wibox.widget.base.fit_widget(self, context, inner_widget, width / factor, height / factor)
   return w * factor, h * factor
end
function scale_container:layout(_, width, height)
    return { wibox.widget.base.place_widget_via_matrix(inner_widget, scale_matrix, width / factor, height / factor) }
end

Scaling a shape

well: local function scale_shape(cr, width, height) cr:scale(factor, factor) inner_shape(cr, width / factor, height / factor) end

Is there another way to scale a wibox as a whole, like scaling an image?

Nope, sorry. You need to scale the bounding/clipping shape on your own, change the size of the wibox (which can be done e.g. with awful.placement.scale, I guess), and also scale the contents of the wibox (either by scaling all the shapes, or by scaling the widget as a whole with the code above).

I hope this helps.

slickroot commented 4 years ago

@psychon Thank you for your answer.

What I took from this is that having access to cairo when making a shape in Awesome is great. But it shouldn't be the way to go to make drawings. My goal was a battery widget that is listening to power events, it gets a percentage and updates the face from hungry to normal and to happy, and at the same time updates the body of the battery (the orange background).

I don't know if it's the right thing to do but I'll just post my code here, because even if the result looks good, not being able to scale it as a whole makes me think that my implementation was not the right approach in Awesome context.

local wibox = require('wibox')
local gears = require('gears')
local awful = require('awful')
local beautiful = require("beautiful")
local xrdb = beautiful.xresources.get_current_theme()
local cairo = require("lgi").cairo
-- CAIRO_LINE_CAP_ROUND

local battery = wibox({ visible = true, type="dock", ontop = true, bg = "#00000000", height = dpi(120), width = dpi(70) })

local background = xrdb.background

local percentage = 14

local MAX_HEIGHT = dpi(110)

local battery_height = percentage * MAX_HEIGHT / 100

local eye = wibox.widget {
    forced_height = dpi(5),
    forced_width = dpi(10),
    shape = function(cr, width, height)
        cr:move_to(0,0)
        gears.shape.rounded_rect(cr, width, height, 7)
    end,
    bg = background,
    widget = wibox.container.background()
}

-- draw hangry mouth
local d_hungry = function(cr, width, height)
    cr:set_source(gears.color(background))
    cr:set_line_width(dpi(3))
    cr:set_line_cap(cairo.LineCap.ROUND)
    cr:move_to(dpi(4), height - dpi(4))
    cr:curve_to(
        dpi(4), dpi(8),
        width - dpi(4), dpi(8),
        width - dpi(4), height - dpi(4)
        )
    cr:stroke()
    cr:arc(width - dpi(4), height - dpi(4), dpi(3), 0, 2*math.pi)
    cr:fill()
end

-- draw happy mouth
local d_happy = function(cr, width, height)
    cr:set_source_rgb(0, 0, 0) -- Red
    cr:set_line_width(dpi(4))
    cr:set_line_cap(cairo.LineCap.ROUND)
    cr:move_to(dpi(4), dpi(4))
    cr:curve_to(
        dpi(4), height - dpi(4),
        width - dpi(4), height - dpi(4),
        width - dpi(4), dpi(4)
        )
    cr:scale(0.8, 0.8)
    cr:stroke()
    -- cr:arc(width - dpi(4), height - dpi(4), dpi(4), 0, 2*math.pi)
    --cr:fill()
end

-- draw normal mouth
local d_normal = function(cr, width, height)
    cr:set_source(gears.color(background))
    cr:set_line_width(dpi(4))
    cr:set_line_cap(cairo.LineCap.ROUND)
    cr:move_to(dpi(4), dpi(4))
    cr:curve_to(
        dpi(4), dpi(12),
        width - dpi(4), dpi(12),
        width - dpi(4), dpi(4)
        )
    cr:stroke()
end

local mouth_shape = d_happy
local battery_color = xrdb.color2.."BB"
if percentage < 15 then
    mouth_shape = d_hungry
    battery_color = xrdb.color1.."BB"
elseif percentage < 45 then
    mouth_shape = d_normal
    battery_color = xrdb.color4.."BB"
end

local mouth = {
    fit    = function(self, context, width, height)
        return dpi(20), dpi(20) -- A square taking the full height
    end,
    draw   = function(self, context, cr, width, height)
        mouth_shape(cr, width, height)
    end,
    layout = wibox.widget.base.make_widget,
}

local face = wibox.widget {
    {
        eye,
        valign = "top",
        widget = wibox.container.place
    },
    {
        mouth,
        valign = "bottom",
        widget = wibox.container.place
    },
    {
        eye,
        valign = "top",
        widget = wibox.container.place
    },
    spacing = dpi(0),
    forced_height = dpi(30),
    layout = wibox.layout.fixed.horizontal
}

battery:setup {
    {
        {
            {
                {
                    {
                        {
                            {
                                shape = gears.shape.rectangle,
                                bg = battery_color,
                                forced_height = battery_height,
                                widget = wibox.container.background,
                                forced_height = battery_height,
                                forced_width = dpi(70),
                            },
                            valign = "bottom",
                            widget = wibox.container.place
                        },
                        shape = function(cr, width, height)
                            gears.shape.rounded_rect(cr, width, height, dpi(9))
                        end,
                        widget = wibox.container.background
                    },
                    margins = dpi(3),
                    widget = wibox.container.margin
                },
                {
                    {
                        face,
                        valign = "top",
                        widget = wibox.container.place
                    },
                    top = dpi(20),
                    widget = wibox.container.margin
                }, 
                layout = wibox.layout.stack
            },
            bg = xrdb.foreground.."BB",
            shape = function(cr, width, height)
                gears.shape.rounded_rect(cr, width, height, dpi(9))
            end,
            widget = wibox.container.background
        }, 
        margins = {left = dpi(4), bottom = dpi(4), right = dpi(4)},
        top = dpi(13), 
        widget = wibox.container.margin
    },
    bg = background,
    shape = function(cr, width, height)
        local r = dpi(8)
        local sr = dpi(4)
        local head = dpi(12)
        cr:move_to(0, 2 * r) 
        cr:line_to(0, height - r)
        cr:arc_negative(r, height - r, r, math.pi, math.pi/2)
        cr:line_to(width - r, height)
        cr:arc_negative(width - r, height - r, r, math.pi/2, 0)
        cr:line_to(width, 2* r)
        cr:arc_negative(width - r, r * 2, r, 0, 3*math.pi/2)
        cr:line_to(r , r )
        cr:arc_negative(r, r*2, r, 3*math.pi/2, 0)
        cr:move_to(width / 2 - head, r)
        cr:line_to(width / 2 - head, sr)
        cr:arc(width / 2 - head + sr, sr, sr, math.pi, 3*math.pi/2)
        cr:line_to(width / 2 + head - sr, 0)
        cr:arc(width / 2 + head - sr, sr, sr, 3*math.pi/2, 0)
        cr:line_to(width / 2 + head, r)
    end,
    widget = wibox.container.background
}

awful.placement.bottom_right(battery) 

return battery

As you see I just started drawing with cairo whenever I get the chance, using numbers that were not relative to the width and the height of the widget, most of them are just constants.

I saw in the documentation that I can make a cairo surface and use it as a bgimage of another widget. Isn't there a way to make a widget then convert it to an image then have it as a background image of another widget that I can scale?

psychon commented 4 years ago

not being able to scale it as a whole makes me think that my implementation was not the right approach in Awesome context.

Well... your shape is already scalable (it uses the provided width and height). Your widget on the other hand ignores these arguments and contains hardcoded positions like dpi(4). However, since all of this is in a widget, my "Scaling a widget" answer from above applies: You could wrap your widget in a scaling_widget. If you use it at the "top-level" (i.e. as the widget for the whole wibox and not contained in another widget), you do not even need a fit method since it would not be used anyway.

Also:

local mouth = {
    fit    = function(self, context, width, height)
        return dpi(20), dpi(20) -- A square taking the full height
    end,
    draw   = function(self, context, cr, width, height)
        mouth_shape(cr, width, height)
    end,
    layout = wibox.widget.base.make_widget,
}

Wow, I did not know that you can create a widget like this. You should definitely use the same approach for the scaling widget instead of the way I wrote the code above. Since you wrote all that code, I bet you know how to do this. :-)

I believe that this should solve your problem, no?

Isn't there a way to make a widget then convert it to an image then have it as a background image of another widget that I can scale?

gears.surface.widget_to_surface can do that (although it is deprecated and one should use wibox.widget_draw_to_image_surface instead, according to the code...). However, once you render, your image is rasterised. Scaling this later can lead to scaling artifacts. Scaling via cairo (cr:scale(2, 3)) should lead to better results, I believe.

slickroot commented 4 years ago

The scaling widget didn't work, I think there's something missing and I couldn't figure it out.

Inspired by you I duplicated the code of wibox.container.mirror and made a scale container, the only change was inside mirror:layout signal I changed the scale to m:scale(factor, factor), then I used this new container as I would use any container. The results were perefect, so I think I will make this container more generic maybe replace a set_reflections with set_scale and make it a scale container, it's going to be useful for me.

I'll share the code later if you'd like that.

Thanks a lot @psychon :))

actionless commented 4 years ago

and make it a scale container, it's going to be useful for me.

sounds nice, feel free to post a PR with new method for shape API