aboutsummaryrefslogtreecommitdiff
path: root/src/main.cpp
blob: edd010dd7254dfb66bf829aab703054e8fb25016 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
#include <mglpp/mglpp.hpp>
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
#include <mglpp/system/Clock.hpp>
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/system/MemoryMappedFile.hpp>
#include <mglpp/graphics/Font.hpp>
#include <mglpp/graphics/Text.hpp>
#include <mglpp/graphics/Texture.hpp>
#include <mglpp/graphics/Sprite.hpp>

#include <stdio.h>
#include <string.h>
#include <array>

#include <X11/Xlib.h>

extern "C" {
#include <mgl/mgl.h>
}

static const mgl::Color bg_color(118, 185, 0);

enum class State {
    SLIDE_IN_WINDOW,
    SLIDE_IN_CONTENT,
    FADE_IN_CONTENT,
    FADE_OUT_CONTENT,
    SLIDE_OUT_CONTENT,
    SLIDE_OUT_WINDOW,
    PAUSE
};

struct StateWithPayload {
    State state;
    double time_in_state_sec;
};

// Linear interpolation
static double interpolate(double a, double b, double interpolation) {
    return a + (b - a) * interpolation;
}

static double min_double(double a, double b) {
    return a < b ? a : b;
}

static float max_float(float a, float b) {
    return a > b ? a : b;
}

#define _NET_WM_STATE_REMOVE  0
#define _NET_WM_STATE_ADD     1
#define _NET_WM_STATE_TOGGLE  2

static Bool set_window_wm_state(Display *display, Window window, Atom atom) {
    Atom net_wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False);
    if(!net_wm_state_atom) {
        fprintf(stderr, "Error: failed to find atom _NET_WM_STATE\n");
        return False;
    }

    XClientMessageEvent xclient;
    memset(&xclient, 0, sizeof(xclient));

    xclient.type = ClientMessage;
    xclient.window = window;
    xclient.message_type = net_wm_state_atom;
    xclient.format = 32;
    xclient.data.l[0] = _NET_WM_STATE_ADD;
    xclient.data.l[1] = atom;
    xclient.data.l[2] = 0;
    xclient.data.l[3] = 0;
    xclient.data.l[4] = 0;

    XSendEvent(display, DefaultRootWindow(display), False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&xclient);
    XFlush(display);
    return True;
}

static Bool make_window_always_on_top(Display* display, Window window) {
    Atom net_wm_state_above_atom = XInternAtom(display, "_NET_WM_STATE_ABOVE", False);
    if(!net_wm_state_above_atom) {
        fprintf(stderr, "Error: failed to find atom _NET_WM_STATE_ABOVE\n");
        return False;
    }
    
    return set_window_wm_state(display, window, net_wm_state_above_atom);
}

static Bool make_window_sticky(Display* display, Window window) {
    Atom net_wm_state_sticky_atom = XInternAtom(display, "_NET_WM_STATE_STICKY", False);
    if(!net_wm_state_sticky_atom) {
        fprintf(stderr, "Error: failed to find atom _NET_WM_STATE_STICKY\n");
        return False;
    }
    
    return set_window_wm_state(display, window, net_wm_state_sticky_atom);
}

int main(int argc, char **argv) {
    if(argc != 2) {
        fprintf(stderr, "usage: gpu-screen-recorder-notification <title>\n");
        exit(1);
    }

    const char *notification_title = argv[1];
    const double notification_timeout_sec = 3.0;

    mgl::Init init;

    mgl::Window::CreateParams window_create_params;
    window_create_params.size = { 1280, 720 };
    window_create_params.min_size = window_create_params.size;
    window_create_params.max_size = window_create_params.max_size;
    window_create_params.hidden = true;
    window_create_params.override_redirect = true;
    window_create_params.background_color = bg_color;
    window_create_params.hide_decorations = true;
    window_create_params.window_type = MGL_WINDOW_TYPE_DIALOG;

    mgl::Window window;
    if(!window.create("GPU Screen Recorder Notification", window_create_params))
        return 1;

    mgl_window *win = window.internal_window();
    if(win->num_monitors == 0) {
        fprintf(stderr, "Error: no monitors found\n");
        exit(1);
    }

    mgl::MemoryMappedFile font_file;
    if(!font_file.load("/usr/share/fonts/noto/NotoSans-Bold.ttf", mgl::MemoryMappedFile::LoadOptions{true, false}))
        return 1;

    mgl::Font font;
    if(!font.load_from_file(font_file, win->monitors[0].size.y / 60))
        return 1;

    mgl::Texture record_texture;
    if(!record_texture.load_from_file("images/record.png"))
        return 1;

    const int window_height = win->monitors[0].size.y / 12;
    const float content_padding_left = 10.0f;//max_float(5.0f, (float)window_size.x / 30.0f);

    mgl::Text content_title(notification_title, font);
    content_title.set_color(mgl::Color(255, 255, 255, 0));

    mgl::Sprite content_record_sprite(&record_texture);
    content_record_sprite.set_color(mgl::Color(118, 185, 0, 0));
    content_record_sprite.set_height((int)(window_height * 0.4f));
    const float content_record_sprite_padding_x = (int)((window_height - content_record_sprite.get_size().y) * 0.4f);
    const float padding_between_icon_and_text_x = (int)(content_record_sprite_padding_x * 0.7f);

    // TODO: Use the monitor that the cursor is on or the focused window is on
    const int window_width = content_padding_left + content_record_sprite_padding_x + content_record_sprite.get_size().x + padding_between_icon_and_text_x + content_title.get_bounds().size.x + content_record_sprite_padding_x + padding_between_icon_and_text_x + content_padding_left;
    const mgl::vec2i window_size{window_width, window_height};
    const mgl::vec2i window_start_position{win->monitors[0].size.x, window_size.y};
    window.set_size_limits(window_size, window_size);
    window.set_size(window_size);
    window.set_position(window_start_position);
    window.set_visible(true);

    Display *display = (Display*)mgl_get_context()->connection;
    make_window_always_on_top(display, window.get_system_handle());
    make_window_sticky(display, window.get_system_handle());

    const int slide_window_start_x = win->monitors[0].size.x;
    const int slide_window_end_x = win->monitors[0].size.x - window_size.x;

    const std::array<StateWithPayload, 9> states_to_execute_in_order = {
        StateWithPayload{State::SLIDE_IN_WINDOW,   0.15},
        StateWithPayload{State::PAUSE,             0.05},
        StateWithPayload{State::SLIDE_IN_CONTENT,  0.10},
        StateWithPayload{State::FADE_IN_CONTENT,   0.50},
        StateWithPayload{State::PAUSE,             notification_timeout_sec},
        StateWithPayload{State::FADE_OUT_CONTENT,  0.001},
        StateWithPayload{State::SLIDE_OUT_CONTENT, 0.15},
        StateWithPayload{State::PAUSE,             0.10},
        StateWithPayload{State::SLIDE_OUT_WINDOW,  0.15},
    };
    int current_state_index = 0;

    mgl::Clock state_timer;

    mgl::Rectangle content_bg(window_size.to_vec2f() - mgl::vec2f(content_padding_left, 0.0f));
    content_bg.set_color(mgl::Color(0, 0, 0));

    const int slide_content_start_x = window_size.x + content_padding_left;
    const int slide_content_end_x = content_padding_left;

    const mgl::vec2f content_bg_start_position{(float)slide_content_start_x, 0.0f};
    content_bg.set_position(content_bg_start_position);

    const float content_start_alpha = 0.0f;
    const float content_end_alpha = 1.0f;

    const auto slide_content_handler = [&](double interpolate_start, double interpolate_end, double interpolation) {
        double new_slide_x = interpolate(interpolate_start, interpolate_end, interpolation);
        content_bg.set_position(mgl::vec2f(new_slide_x, content_bg_start_position.y).floor());
        content_record_sprite.set_position((content_bg.get_position() + mgl::vec2f(content_record_sprite_padding_x, content_bg.get_size().y * 0.5f - content_record_sprite.get_size().y * 0.5f)).floor());
        const float content_space_left_pos_x = content_record_sprite.get_position().x + content_record_sprite.get_size().x + padding_between_icon_and_text_x;
        //const float content_space_left_x = content_bg.get_size().x - content_space_left_pos_x;
        content_title.set_position((mgl::vec2f(content_space_left_pos_x, content_bg.get_position().y) + mgl::vec2f(0.0f, content_bg.get_size().y) * 0.5f - mgl::vec2f(0.0f, content_title.get_bounds().size.y) * 0.5f).floor());
    };

    mgl::Event event;
    while(window.is_open()) {
        while(window.poll_event(event)) {}

        const StateWithPayload current_state = states_to_execute_in_order[current_state_index];
        const double state_elapsed_time_sec = state_timer.get_elapsed_time_seconds();
        const double state_interpolation = min_double(1.0, state_elapsed_time_sec / current_state.time_in_state_sec);

        switch(current_state.state) {
            case State::SLIDE_IN_WINDOW: {
                double new_slide_x = interpolate(slide_window_start_x, slide_window_end_x, state_interpolation);
                window.set_position(mgl::vec2i(new_slide_x, window_start_position.y));
                break;
            }
            case State::SLIDE_IN_CONTENT: {
                slide_content_handler(slide_content_start_x, slide_content_end_x, state_interpolation);
                break;
            }
            case State::FADE_IN_CONTENT: {
                double new_alpha = interpolate(content_start_alpha, content_end_alpha, state_interpolation);
                content_title.set_color(mgl::Color(255, 255, 255, new_alpha * 255.0f));
                content_record_sprite.set_color(mgl::Color(118, 185, 0, new_alpha * 255.0f));
                break;
            }
            case State::FADE_OUT_CONTENT: {
                double new_alpha = interpolate(content_end_alpha, content_start_alpha, state_interpolation);
                content_title.set_color(mgl::Color(255, 255, 255, new_alpha * 255.0f));
                content_record_sprite.set_color(mgl::Color(118, 185, 0, new_alpha * 255.0f));
                break;
            }
            case State::SLIDE_OUT_CONTENT: {
                slide_content_handler(slide_content_end_x, slide_content_start_x, state_interpolation);
                break;
            }
            case State::SLIDE_OUT_WINDOW: {
                double new_slide_x = interpolate(slide_window_end_x, slide_window_start_x, state_interpolation);
                window.set_position(mgl::vec2i(new_slide_x, window_start_position.y));
                break;
            }
            case State::PAUSE: {
                break;
            }
        }

        window.clear(bg_color);
        window.draw(content_bg);
        window.draw(content_record_sprite);
        window.draw(content_title);
        window.display();

        if(state_interpolation >= 1.0) {
            state_timer.restart();
            ++current_state_index;
            if(current_state_index >= (int)states_to_execute_in_order.size())
                window.close();
        }
    }
}