Skip to content

Commit

Permalink
MainWindow: add light/dark headerbar colors
Browse files Browse the repository at this point in the history
  • Loading branch information
cassidyjames committed Aug 28, 2024
1 parent cd66fe6 commit d3ba2e7
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 52 deletions.
1 change: 1 addition & 0 deletions data/gresource.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
<gresource prefix="/com/cassidyjames/butler/">
<file preprocess="xml-stripblanks" compressed="true">metainfo.xml.in</file>
<file alias="Devel/style.css">style.css</file>
<file alias="Devel/style-dark.css">style-dark.css</file>
</gresource>
</gresources>
5 changes: 5 additions & 0 deletions data/gschema.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
<summary>Current URL</summary>
<description>The last-viewed URL for restoring state</description>
</key>
<key name="headerbar-colors" type="(ss)">
<default>("#03a9f5", "#03a9f5")</default>
<summary>Headerbar Colors</summary>
<description>Colors for the header bar to better integrate into custom servers, stored as a (light, dark) pair to be used according to the system color scheme preference</description>
</key>
<key name="server" type="s">
<default>"https://demo.home-assistant.io/"</default>
<summary>Home Assistant server URL</summary>
Expand Down
2 changes: 2 additions & 0 deletions data/style-dark.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@define-color headerbar_bg_color @headerbar_bg_dark;
@define-color headerbar_fg_color @headerbar_fg_dark;
12 changes: 7 additions & 5 deletions data/style.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@define-color headerbar_bg_color #03a9f5;
@define-color headerbar_fg_color #fff;
@define-color headerbar_border_color #fff;
@define-color headerbar_backdrop_color mix(@headerbar_bg_color, @window_bg_color, 0.1);
@define-color ha_color #03a9f5;
@define-color accent_color @ha_color;

@define-color headerbar_bg_color @headerbar_bg_light;
@define-color headerbar_fg_color @headerbar_fg_light;

@define-color accent_color @headerbar_bg_color;
@define-color headerbar_border_color @headerbar_fg_color;
@define-color headerbar_backdrop_color mix(@headerbar_bg_color, @window_bg_color, 0.1);
201 changes: 154 additions & 47 deletions src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,22 @@ public class Butler.MainWindow : Adw.ApplicationWindow {

private const GLib.ActionEntry[] ACTION_ENTRIES = {
{ "toggle_fullscreen", toggle_fullscreen },
{ "set_server", on_set_server_activate },
{ "settings", on_settings_activate },
{ "log_out", on_log_out_activate },
{ "about", on_about_activate },
};

private Butler.WebView web_view;

private const string CSS = """
@define-color headerbar_bg_light %s;
@define-color headerbar_fg_light %s;
@define-color headerbar_bg_dark %s;
@define-color headerbar_fg_dark %s;
""";
private Gtk.CssProvider css_provider;

public MainWindow (Adw.Application app) {
Object (
application: app,
Expand All @@ -35,7 +44,7 @@ public class Butler.MainWindow : Adw.ApplicationWindow {
construct {
maximized = App.settings.get_boolean ("window-maximized");
fullscreened = App.settings.get_boolean ("window-fullscreened");
this.add_css_class (PROFILE);
add_css_class (PROFILE);

about_dialog = new Adw.AboutDialog.from_appdata (
"/com/cassidyjames/butler/metainfo.xml.in", VERSION
Expand All @@ -51,8 +60,7 @@ public class Butler.MainWindow : Adw.ApplicationWindow {
};
about_dialog.application_icon = APP_ID;
about_dialog.application_name = APP_NAME;
about_dialog.copyright = "© 2020–%i %s".printf (
new DateTime.now_local ().get_year (),
about_dialog.copyright = "© 2020–2024 %s".printf (
about_dialog.developer_name
);
about_dialog.add_link (_("About Home Assistant"), "https://www.home-assistant.io/");
Expand All @@ -73,7 +81,7 @@ public class Butler.MainWindow : Adw.ApplicationWindow {
var app_menu = new Menu ();
// TODO: How do I add shortcuts to the menu?
app_menu.append (_("_Fullscreen"), "win.toggle_fullscreen");
app_menu.append (_("Change _Server"), "win.set_server");
app_menu.append (_("_Server Settings"), "win.settings");
app_menu.append (_("_About %s").printf (APP_NAME), "win.about");

var menu = new Menu ();
Expand All @@ -96,8 +104,8 @@ public class Butler.MainWindow : Adw.ApplicationWindow {
};

demo_banner = new Adw.Banner (_("Browsing Home Assistant Demo")) {
action_name = "win.set_server",
button_label = _("Set _Server…")
action_name = "win.settings",
button_label = _("Change _Server…")
};

fullscreen_toast = new Adw.Toast (_("Press <b>Ctrl F</b> or <b>F11</b> to toggle fullscreen")) {
Expand Down Expand Up @@ -130,18 +138,20 @@ public class Butler.MainWindow : Adw.ApplicationWindow {
stack.add_named (status_page, "loading");
stack.add_named (web_view, "web");

string headerbar_color_light, headerbar_color_dark;
App.settings.get ("headerbar-colors", "(ss)", out headerbar_color_light, out headerbar_color_dark);
update_headerbar_colors (headerbar_color_light, headerbar_color_dark);

toast_overlay = new Adw.ToastOverlay () {
child = stack
};

var grid = new Gtk.Grid () {
orientation = Gtk.Orientation.VERTICAL
};
grid.attach (header_revealer, 0, 0);
grid.attach (toast_overlay, 0, 1);
grid.attach (demo_banner, 0, 2);
var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
box.append (header_revealer);
box.append (toast_overlay);
box.append (demo_banner);

set_content (grid);
set_content (box);

int window_width, window_height;
App.settings.get ("window-size", "(ii)", out window_width, out window_height);
Expand Down Expand Up @@ -177,6 +187,30 @@ public class Butler.MainWindow : Adw.ApplicationWindow {
App.settings.bind ("zoom", web_view, "zoom-level", SettingsBindFlags.DEFAULT);
}

private void update_headerbar_colors (string light, string dark) {
var light_rgba = Gdk.RGBA ();
light_rgba.parse (light);

var dark_rgba = Gdk.RGBA ();
dark_rgba.parse (dark);

css_provider = new Gtk.CssProvider ();

var css = CSS.printf (
light_rgba.to_string (),
contrasting_foreground_color (light_rgba).to_string (),
dark_rgba.to_string (),
contrasting_foreground_color (dark_rgba).to_string ()
);

css_provider.load_from_string (css);
Gtk.StyleContext.add_provider_for_display (
Gdk.Display.get_default (),
css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 1
);
}

private void save_window_state () {
if (fullscreened) {
App.settings.set_boolean ("window-fullscreened", true);
Expand Down Expand Up @@ -270,54 +304,127 @@ public class Butler.MainWindow : Adw.ApplicationWindow {
}
}

private void on_set_server_activate () {
private void on_settings_activate () {
string current_server = App.settings.get_string ("server");
string default_server = App.settings.get_default_value ("server").get_string ();

var server_entry = new Gtk.Entry.with_buffer (new Gtk.EntryBuffer ((uint8[]) current_server)) {
string current_color_light, current_color_dark;
App.settings.get ("headerbar-colors", "(ss)", out current_color_light, out current_color_dark);

var current_rgba_light = Gdk.RGBA ();
current_rgba_light.parse (current_color_light);

var current_rgba_dark = Gdk.RGBA ();
current_rgba_dark.parse (current_color_dark);

var server_entry = new Adw.EntryRow () {
activates_default = true,
hexpand = true,
placeholder_text = default_server
text = current_server,
title = _("Server URL"),
};

var server_dialog = new Adw.AlertDialog (
_("Set Server URL"),
_("Enter the full URL including any custom port")
) {
body_use_markup = true,
default_response = "save",
extra_child = server_entry,
var server_group = new Adw.PreferencesGroup () {
title = _("Server Settings"),
description = _("Enter the full URL including any custom port"),
};
server_dialog.add_response ("close", _("_Cancel"));
server_group.add (server_entry);

server_dialog.add_response ("demo", _("_Reset to Demo"));
server_dialog.set_response_appearance ("demo", Adw.ResponseAppearance.DESTRUCTIVE);
var color_light_button = new Gtk.ColorDialogButton (new Gtk.ColorDialog ()) {
rgba = current_rgba_light,
valign = Gtk.Align.CENTER,
};

server_dialog.add_response ("save", _("_Set Server"));
server_dialog.set_response_appearance ("save", Adw.ResponseAppearance.SUGGESTED);
var color_light_row = new Adw.ActionRow () {
title = _("Light"),
subtitle = _("Used with default system style preference"),
activatable_widget = color_light_button,
};
color_light_row.add_suffix (color_light_button);

var color_dark_button = new Gtk.ColorDialogButton (new Gtk.ColorDialog ()) {
rgba = current_rgba_dark,
valign = Gtk.Align.CENTER,
};

server_dialog.present (this);
var color_dark_row = new Adw.ActionRow () {
title = _("Dark"),
subtitle = _("Used with dark system style preference"),
activatable_widget = color_dark_button,
};
color_dark_row.add_suffix (color_dark_button);

server_dialog.response.connect ((response_id) => {
if (response_id == "save") {
string new_server = server_entry.buffer.text;
var colors_group = new Adw.PreferencesGroup () {
title = _("Header Bar Color"),
description = _("Better match your dashboard"),
};
colors_group.add (color_light_row);
colors_group.add (color_dark_row);

if (new_server == "") {
new_server = default_server;
}
var settings_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 24);
settings_box.append (server_group);
settings_box.append (colors_group);

if (!new_server.contains ("://")) {
new_server = "http://" + new_server;
}
var settings_dialog = new Adw.AlertDialog (null, null) {
body_use_markup = true,
default_response = "save",
extra_child = settings_box,
};
settings_dialog.add_response ("close", _("_Cancel"));

settings_dialog.add_response ("reset", _("_Reset to Default"));
settings_dialog.set_response_appearance ("reset", Adw.ResponseAppearance.DESTRUCTIVE);

settings_dialog.add_response ("save", _("_Save"));
settings_dialog.set_response_appearance ("save", Adw.ResponseAppearance.SUGGESTED);

settings_dialog.present (this);

settings_dialog.response.connect ((response_id) => {
switch (response_id) {
case "save":
string new_server = server_entry.text;
string new_color_light = color_light_button.get_rgba ().to_string ();
string new_color_dark = color_dark_button.get_rgba ().to_string ();

if (new_server == "") {
new_server = default_server;
}

if (!new_server.contains ("://")) {
new_server = "http://" + new_server;
}

if (new_server != current_server) {
// FIXME: There's currently no validation of this
App.settings.set_string ("server", new_server);
log_out ();
}

if (
new_color_light != current_color_light ||
new_color_dark != current_color_dark
) {
App.settings.set (
"headerbar-colors", "(ss)", new_color_light, new_color_dark
);
update_headerbar_colors (new_color_light, new_color_dark);
}
break;

case "reset":
App.settings.reset ("headerbar-colors");
App.settings.reset ("server");

string color_light, color_dark;
App.settings.get ("headerbar-colors", "(ss)", out color_light, out color_dark);
update_headerbar_colors (color_light, color_dark);

if (new_server != current_server) {
// FIXME: There's currently no validation of this
App.settings.set_string ("server", new_server);
log_out ();
}
} else if (response_id == "demo") {
App.settings.reset ("server");
log_out ();
break;

case "close":
default:
break;
}
});
}
Expand Down
71 changes: 71 additions & 0 deletions src/Utils.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020–2024 Cassidy James Blaede <[email protected]>
*/

namespace Butler {

private static double contrast_ratio (Gdk.RGBA bg_color, Gdk.RGBA fg_color) {
// From WCAG 2.0 https://www.w3.org/TR/WCAG20/#contrast-ratiodef
var bg_luminance = get_luminance (bg_color);
var fg_luminance = get_luminance (fg_color);

if (bg_luminance > fg_luminance) {
return (bg_luminance + 0.05) / (fg_luminance + 0.05);
}

return (fg_luminance + 0.05) / (bg_luminance + 0.05);
}

private static double get_luminance (Gdk.RGBA color) {
// Values from WCAG 2.0 https://www.w3.org/TR/WCAG20/#relativeluminancedef
var red = sanitize_color (color.red) * 0.2126;
var green = sanitize_color (color.green) * 0.7152;
var blue = sanitize_color (color.blue) * 0.0722;

return red + green + blue;
}

private static double sanitize_color (double color) {
// From WCAG 2.0 https://www.w3.org/TR/WCAG20/#relativeluminancedef
if (color <= 0.03928) {
return color / 12.92;
}

return Math.pow ((color + 0.055) / 1.055, 2.4);
}

/**
* Takes a {@link Gdk.RGBA} background color and returns a suitably-contrasting foreground color, i.e. for determining text color on a colored background. There is a slight bias toward returning white, as white generally looks better on a wider range of colored backgrounds than black.
*
* Copied from my implementation in Granite https://github.com/elementary/granite/commit/74b7e4318dc7721a6c09dd6bf67713299d7be8eb
*
* @param bg_color any {@link Gdk.RGBA} background color
*
* @return a contrasting {@link Gdk.RGBA} foreground color, i.e. white ({ 1.0, 1.0, 1.0, 1.0}) or black ({ 0.0, 0.0, 0.0, 1.0}).
*/
public static Gdk.RGBA contrasting_foreground_color (Gdk.RGBA bg_color) {
Gdk.RGBA gdk_white = { 1.0f, 1.0f, 1.0f, 1.0f };
Gdk.RGBA gdk_black = { 0.0f, 0.0f, 0.0f, 1.0f };

var contrast_with_white = contrast_ratio (
bg_color,
gdk_white
);
var contrast_with_black = contrast_ratio (
bg_color,
gdk_black
);

// Default to white
var fg_color = gdk_white;

// NOTE: We cheat and add 6 to contrast when checking against black,
// because white generally looks better on a colored background
if ( contrast_with_black > (contrast_with_white + 6) ) {
fg_color = gdk_black;
}

return fg_color;
}
}
Loading

0 comments on commit d3ba2e7

Please sign in to comment.