Add units support (POINT, PIXEL, SPACE) (#2578)

* add units support (POINT, PIXEL, SPACE) for polybar

- add a size_with_unit struct
- add a geometry_format_values struct
- move dpi initialisation from renderer.cpp to bar.cpp
- add a string to size_with_unit converter
- add point support (with pt)
- add pixel support (with px)

* Fix unit test compilation

* clang-format

* Better names

The old names didn't really capture the purpose of the structs and
function.

space_type -> spacing_type
space_size -> spacing_val

size_type -> extent_type
geometry -> extent_val

geometry_format_values -> percentage_with_offset

* Remove parse_size_with_unit

No longer needed. The convert<spacing_val> function in config.cpp
already does all the work for us and always setting the type to pixel
was wrong.

In addition, line-size should not be of type spacing_val but extent_val.

* Cleanup

I tried to address most of my comments on the old PR

* Fix renderer width calculation

We can't just blindly add the x difference to the width because for
example the width should increase if x < width and the increase keeps
x < width.

Similarly, we can't just add the offset to the width.

* Rename geom_format_to_pixels to percentage_with_offset_to_pixel

* Cleanup

* Apply suggested changes from Patrick on GitHub

Co-authored-by: Patrick Ziegler <p.ziegler96@gmail.com>

* Update src/components/bar.cpp

Co-authored-by: Patrick Ziegler <p.ziegler96@gmail.com>

* Update src/components/config.cpp

Co-authored-by: Patrick Ziegler <p.ziegler96@gmail.com>

* Update src/components/builder.cpp

Co-authored-by: Patrick Ziegler <p.ziegler96@gmail.com>

* Update src/components/builder.cpp

Co-authored-by: Patrick Ziegler <p.ziegler96@gmail.com>

* config: Use stod for parsing percentage

* Use stof instead of strtof

* units: Fix test edge cases

* Remove unnecessary clang-format toggle

* Use percentage_with_offset for margin-{top,bottom}

* Support negative extent values

* Rename unit to units and create a cpp file

* Move percentage_with_offset_to_pixel unit test to units

* Add unit tests for units_utils

* Clarify when and how negative spacing/extent is allowed

Negative spacing is never allowed and produces a config error.

Extents allow negative values in theory, but only a few use-cases accept
it.
Only the extent value used for the `%{O}` tag and the offset value in
percentage_with_offset can be negative. Everything else is capped below
at 0.

The final pixel value of percentage_with_offset also caps below at 0.

* Fix parsing errors not being caught in config

* Print a proper error message for uncaught exceptions

* Cleanup module::get_output

All changes preserve the existing semantics

* Stop using remove_trailing_space in module::get_output

Instead, we first check if the current tag is built, and only if it is,
the spacing is prepended.

* Remove unused imports

* Restore old behavior

If there are two tags and the second one isn't built (module::build
returns false), the space in between them is removed.
For example in the mpd module:

format-online = <toggle> <label-song> foo

If mpd is not running, the mpd module will return false when trying to
build the `<label-song>` tag. If we don't remove the space between
`<toggle>` and `<label-song>`, we end up with two spaces between
`<toggle>` and `foo`.

This change is to match the old behavior where at least one trailing
space character was removed from the builder.

* Add changelog entry

* Remove unused setting

* Use percentage with offset for tray-offset

Co-authored-by: Jérôme BOULMIER <jerome.boulmier@outlook.fr>
Co-authored-by: Joe Groocock <github@frebib.net>
This commit is contained in:
Patrick Ziegler 2022-02-20 21:08:57 +01:00 committed by GitHub
parent ab915fb724
commit ce93188a4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 860 additions and 358 deletions

View File

@ -10,4 +10,5 @@ AllowShortIfStatementsOnASingleLine: false
BreakConstructorInitializersBeforeComma: true
DerivePointerAlignment: false
PointerAlignment: Left
SpacesBeforeTrailingComments: 1
---

View File

@ -81,6 +81,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `DEBUG_SHADED` cmake variable and its associated functionality.
### Added
- Support `px` and `pt` units everyhwere where before only a number of spaces
or pixels could be specified.
([`#2578`](https://github.com/polybar/polybar/pull/2578))
- Right and middle click events for alsa module.
([`#2566`](https://github.com/polybar/polybar/issues/2566))
- `internal/network`: New token `%mac%` shows MAC address of selected interface
@ -111,7 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `internal/memory`: `format-warn`, `label-warn`, `warn-percentage = 90`
- `radius` now affects the bar border as well
([`#1566`](https://github.com/polybar/polybar/issues/1566))
- Per-corner corner radius with `radius-{bottom,top}-{left,right}`
- Per-corner radius with `radius-{bottom,top}-{left,right}`
([`#2294`](https://github.com/polybar/polybar/issues/2294))
- `internal/network`: `speed-unit = B/s` can be used to customize how network
speeds are displayed.

View File

@ -1,8 +1,6 @@
#pragma once
#include <atomic>
#include <cstdlib>
#include <mutex>
#include "common.hpp"
#include "components/eventloop.hpp"
@ -31,28 +29,6 @@ namespace tags {
}
// }}}
/**
* Allows a new format for pixel sizes (like width in the bar section)
*
* The new format is X%:Z, where X is in [0, 100], and Z is any real value
* describing a pixel offset. The actual value is calculated by X% * max + Z
*/
inline double geom_format_to_pixels(std::string str, double max) {
size_t i;
if ((i = str.find(':')) != std::string::npos) {
std::string a = str.substr(0, i - 1);
std::string b = str.substr(i + 1);
return std::max<double>(
0, math_util::percentage_to_value<double>(strtod(a.c_str(), nullptr), max) + strtod(b.c_str(), nullptr));
} else {
if (str.find('%') != std::string::npos) {
return math_util::percentage_to_value<double>(strtod(str.c_str(), nullptr), max);
} else {
return strtod(str.c_str(), nullptr);
}
}
}
class bar : public xpp::event::sink<evt::button_press, evt::expose, evt::property_notify, evt::enter_notify,
evt::leave_notify, evt::motion_notify, evt::destroy_notify, evt::client_message, evt::configure_notify>,
public signal_receiver<SIGN_PRIORITY_BAR, signals::ui::dim_window

View File

@ -21,17 +21,14 @@ class builder {
void reset();
string flush();
void append(string text);
void node(string str);
void node(string str, int font_index);
void append(const string& text);
void node(const string& str);
void node(const string& str, int font_index);
void node(const label_t& label);
void node_repeat(const string& str, size_t n);
void node_repeat(const label_t& label, size_t n);
void offset(int pixels);
void space(size_t width);
void space();
void remove_trailing_space(size_t len);
void remove_trailing_space();
void offset(extent_val pixels = ZERO_PX_EXTENT);
void spacing(spacing_val size);
void font(int index);
void font_close();
void background(rgba color);
@ -55,6 +52,8 @@ class builder {
void action(mousebtn btn, const modules::module_interface& module, string action, string data, const label_t& label);
void action_close();
static string get_spacing_format_string(const spacing_val& space);
protected:
void tag_open(tags::syntaxtag tag, const string& value);
void tag_open(tags::attribute attr);

View File

@ -107,7 +107,7 @@ class config {
return dereference<T>(move(section), move(key), move(string_value), move(result));
} catch (const key_error& err) {
return default_value;
} catch (const value_error& err) {
} catch (const std::exception& err) {
m_log.err("Invalid value for \"%s.%s\", using default value (reason: %s)", section, key, err.what());
return default_value;
}
@ -196,7 +196,7 @@ class config {
}
} catch (const key_error& err) {
break;
} catch (const value_error& err) {
} catch (const std::exception& err) {
m_log.err("Invalid value in list \"%s.%s\", using list as-is (reason: %s)", section, key, err.what());
return default_value;
}

View File

@ -1,6 +1,5 @@
#pragma once
#include <atomic>
#include <mutex>
#include <queue>
@ -29,7 +28,7 @@ class logger;
class signal_emitter;
namespace modules {
struct module_interface;
} // namespace modules
} // namespace modules
using module_t = shared_ptr<modules::module_interface>;
using modulemap_t = std::map<alignment, vector<module_t>>;
// }}}

View File

@ -28,8 +28,18 @@ using std::map;
struct alignment_block {
cairo_pattern_t* pattern;
/**
* The x-position where the next thing will be rendered.
*/
double x;
double y;
/**
* The total width of this block.
*
* This is always >= x, but may be larger because a negative offset may
* decrease x, but the width doesn't change.
*/
double width;
};
class renderer : public renderer_interface,
@ -48,7 +58,7 @@ class renderer : public renderer_interface,
void end();
void flush();
void render_offset(const tags::context& ctxt, int pixels) override;
void render_offset(const tags::context& ctxt, const extent_val offset) override;
void render_text(const tags::context& ctxt, const string&&) override;
void change_alignment(const tags::context& ctxt) override;
@ -62,12 +72,15 @@ class renderer : public renderer_interface,
void fill_overline(rgba color, double x, double w);
void fill_underline(rgba color, double x, double w);
void fill_borders();
void draw_offset(rgba color, double x, double w);
double block_x(alignment a) const;
double block_y(alignment a) const;
double block_w(alignment a) const;
double block_h(alignment a) const;
void increase_x(double dx);
void flush(alignment a);
void highlight_clickable_areas();

View File

@ -10,7 +10,7 @@ class renderer_interface {
public:
renderer_interface(const tags::action_context& action_ctxt) : m_action_ctxt(action_ctxt){};
virtual void render_offset(const tags::context& ctxt, int pixels) = 0;
virtual void render_offset(const tags::context& ctxt, const extent_val offset) = 0;
virtual void render_text(const tags::context& ctxt, const string&& str) = 0;
virtual void change_alignment(const tags::context& ctxt) = 0;

View File

@ -84,9 +84,52 @@ struct size {
unsigned int h{1U};
};
enum class spacing_type { SPACE, POINT, PIXEL };
enum class extent_type { POINT, PIXEL };
struct spacing_val {
spacing_type type{spacing_type::SPACE};
/**
* Numerical spacing value. Is truncated to an integer for pixels and spaces.
* Must be non-negative.
*/
float value{0};
/**
* Any non-positive number is interpreted as no spacing.
*/
operator bool() const {
return value > 0;
}
};
static constexpr spacing_val ZERO_SPACE = {spacing_type::SPACE, 0};
/*
* Defines the signed length of something as either a number of pixels or points.
*
* Used for widths, heights, and offsets
*/
struct extent_val {
extent_type type{extent_type::PIXEL};
float value{0};
operator bool() const {
return value != 0;
}
};
static constexpr extent_val ZERO_PX_EXTENT = {extent_type::PIXEL, 0};
struct side_values {
unsigned int left{0U};
unsigned int right{0U};
spacing_val left{ZERO_SPACE};
spacing_val right{ZERO_SPACE};
};
struct percentage_with_offset {
double percentage{0};
extent_val offset{ZERO_PX_EXTENT};
};
struct edge_values {
@ -134,11 +177,14 @@ struct bar_settings {
struct size size {
1U, 1U
};
double dpi_x{0.};
double dpi_y{0.};
position pos{0, 0};
position offset{0, 0};
side_values padding{0U, 0U};
side_values margin{0U, 0U};
side_values module_margin{0U, 0U};
side_values padding{ZERO_SPACE, ZERO_SPACE};
side_values module_margin{ZERO_SPACE, ZERO_SPACE};
edge_values strut{0U, 0U, 0U, 0U};
rgba background{0xFF000000};
@ -151,7 +197,10 @@ struct bar_settings {
std::unordered_map<edge, border_settings, enum_hash> borders{};
struct radius radius {};
int spacing{0};
/**
* TODO deprecated
*/
spacing_val spacing{ZERO_SPACE};
label_t separator{};
string wmname{};

View File

@ -25,8 +25,8 @@ namespace drawtypes {
rgba m_underline{};
rgba m_overline{};
int m_font{0};
side_values m_padding{0U, 0U};
side_values m_margin{0U, 0U};
side_values m_padding{ZERO_SPACE, ZERO_SPACE};
side_values m_margin{ZERO_SPACE, ZERO_SPACE};
size_t m_minlen{0};
/*
@ -40,15 +40,15 @@ namespace drawtypes {
alignment m_alignment{alignment::LEFT};
bool m_ellipsis{true};
explicit label(string text, int font) : m_font(font), m_text(text), m_tokenized(m_text) {}
explicit label(string text, int font) : m_font(font), m_text(move(text)), m_tokenized(m_text) {}
explicit label(string text, rgba foreground = rgba{}, rgba background = rgba{}, rgba underline = rgba{},
rgba overline = rgba{}, int font = 0, struct side_values padding = {0U, 0U},
struct side_values margin = {0U, 0U}, int minlen = 0, size_t maxlen = 0_z,
rgba overline = rgba{}, int font = 0, side_values padding = {ZERO_SPACE, ZERO_SPACE},
side_values margin = {ZERO_SPACE, ZERO_SPACE}, int minlen = 0, size_t maxlen = 0_z,
alignment label_alignment = alignment::LEFT, bool ellipsis = true, vector<token>&& tokens = {})
: m_foreground(foreground)
, m_background(background)
, m_underline(underline)
, m_overline(overline)
: m_foreground(move(foreground))
, m_background(move(background))
, m_underline(move(underline))
, m_overline(move(overline))
, m_font(font)
, m_padding(padding)
, m_margin(margin)
@ -56,14 +56,14 @@ namespace drawtypes {
, m_maxlen(maxlen)
, m_alignment(label_alignment)
, m_ellipsis(ellipsis)
, m_text(text)
, m_text(move(text))
, m_tokenized(m_text)
, m_tokens(forward<vector<token>>(tokens)) {
assert(!m_ellipsis || (m_maxlen == 0 || m_maxlen >= 3));
}
string get() const;
operator bool();
explicit operator bool();
label_t clone();
void clear();
void reset_tokens();
@ -81,6 +81,6 @@ namespace drawtypes {
label_t load_label(const config& conf, const string& section, string name, bool required = true, string def = ""s);
label_t load_optional_label(const config& conf, string section, string name, string def = ""s);
} // namespace drawtypes
} // namespace drawtypes
POLYBAR_NS_END

View File

@ -40,13 +40,12 @@ namespace modules {
static constexpr auto TAG_RAMP_LOAD_PER_CORE = "<ramp-coreload>";
static constexpr auto FORMAT_WARN = "format-warn";
label_t m_label;
label_t m_labelwarn;
progressbar_t m_barload;
ramp_t m_rampload;
ramp_t m_rampload_core;
int m_ramp_padding;
spacing_val m_ramp_padding{spacing_type::SPACE, 1U};
vector<cpu_time_t> m_cputimes;
vector<cpu_time_t> m_cputimes_prev;

View File

@ -68,7 +68,7 @@ namespace modules {
vector<fs_mount_t> m_mounts;
bool m_fixed{false};
bool m_remove_unmounted{false};
int m_spacing{2};
spacing_val m_spacing{spacing_type::SPACE, 2U};
int m_perc_used_warn{90};
// used while formatting output

View File

@ -36,7 +36,7 @@ namespace drawtypes {
using animation_t = shared_ptr<animation>;
class iconset;
using iconset_t = shared_ptr<iconset>;
} // namespace drawtypes
} // namespace drawtypes
class builder;
class config;
@ -67,10 +67,10 @@ namespace modules {
rgba ol{};
size_t ulsize{0};
size_t olsize{0};
size_t spacing{0};
size_t padding{0};
size_t margin{0};
int offset{0};
spacing_val spacing{ZERO_SPACE};
spacing_val padding{ZERO_SPACE};
spacing_val margin{ZERO_SPACE};
extent_val offset{ZERO_PX_EXTENT};
int font{0};
string decorate(builder* builder, string output);
@ -225,6 +225,6 @@ namespace modules {
};
// }}}
} // namespace modules
} // namespace modules
POLYBAR_NS_END

View File

@ -197,48 +197,89 @@ namespace modules {
std::lock_guard<std::mutex> guard(m_buildlock);
auto format_name = CONST_MOD(Impl).get_format();
auto format = m_formatter->get(format_name);
bool no_tag_built{true};
bool fake_no_tag_built{false};
bool tag_built{false};
auto mingap = std::max(1_z, format->spacing);
size_t start, end;
string value{format->value};
while ((start = value.find('<')) != string::npos && (end = value.find('>', start)) != string::npos) {
if (start > 0) {
if (no_tag_built) {
// If no module tag has been built we do not want to add
// whitespace defined between the format tags, but we do still
// want to output other non-tag content
auto trimmed = string_util::ltrim(value.substr(0, start), ' ');
if (!trimmed.empty()) {
fake_no_tag_built = false;
m_builder->node(move(trimmed));
}
} else {
m_builder->node(value.substr(0, start));
/*
* Builder for building individual tags isolated, so that we can
*/
builder tag_builder(m_bar);
// Whether any tags have been processed yet
bool has_tags = false;
// Cursor pointing into 'value'
size_t cursor = 0;
const string& value{format->value};
/*
* Search for all tags in the format definition. A tag is enclosed in '<' and '>'.
* Each tag is given to the module to produce some output for it. All other text is added as-is.
*/
while (cursor < value.size()) {
// Check if there are any tags left
// Start index of next tag
size_t start = value.find('<', cursor);
if (start == string::npos) {
break;
}
// End index (inclusive) of next tag
size_t end = value.find('>', start + 1);
if (end == string::npos) {
break;
}
// Potential regular text that appears before the tag.
string non_tag;
// There is some non-tag text
if (start > cursor) {
/*
* Produce anything between the previous and current tag as regular text.
*/
non_tag = value.substr(cursor, start - cursor);
if (!has_tags) {
/*
* If no module tag has been built we do not want to add
* whitespace defined between the format tags, but we do still
* want to output other non-tag content
*/
non_tag = string_util::ltrim(move(non_tag), ' ');
}
value.erase(0, start);
end -= start;
start = 0;
}
string tag{value.substr(start, end + 1)};
if (tag.empty()) {
continue;
} else if (tag[0] == '<' && tag[tag.size() - 1] == '>') {
if (!no_tag_built)
m_builder->space(format->spacing);
else if (fake_no_tag_built)
no_tag_built = false;
if (!(tag_built = CONST_MOD(Impl).build(m_builder.get(), tag)) && !no_tag_built)
m_builder->remove_trailing_space(mingap);
if (tag_built)
no_tag_built = false;
string tag = value.substr(start, end - start + 1);
bool tag_built = CONST_MOD(Impl).build(&tag_builder, tag);
string tag_content = tag_builder.flush();
/*
* Remove exactly one space between two tags if the second tag was not built.
*/
if (!tag_built && has_tags && !format->spacing) {
if (!non_tag.empty() && non_tag.back() == ' ') {
non_tag.erase(non_tag.size() - 1);
}
}
value.erase(0, tag.size());
m_builder->node(non_tag);
if (tag_built) {
if (has_tags) {
// format-spacing is added between all tags
m_builder->spacing(format->spacing);
}
m_builder->append(tag_content);
has_tags = true;
}
cursor = end + 1;
}
if (!value.empty()) {
m_builder->append(value);
if (cursor < value.size()) {
m_builder->append(value.substr(cursor));
}
return format->decorate(&*m_builder, m_builder->flush());
@ -267,6 +308,6 @@ namespace modules {
}
// }}}
} // namespace modules
} // namespace modules
POLYBAR_NS_END

View File

@ -126,7 +126,7 @@ namespace tags {
color_value parse_color();
int parse_fontindex();
int parse_offset();
extent_val parse_offset();
controltag parse_control();
std::pair<action_value, string> parse_action();
mousebtn parse_action_btn();

View File

@ -13,18 +13,18 @@ namespace tags {
enum class attr_activation { NONE, ON, OFF, TOGGLE };
enum class syntaxtag {
A, // mouse action
B, // background color
F, // foreground color
T, // font index
O, // pixel offset
R, // flip colors
o, // overline color
u, // underline color
P, // Polybar control tag
l, // Left alignment
r, // Right alignment
c, // Center alignment
A, // mouse action
B, // background color
F, // foreground color
T, // font index
O, // pixel offset
R, // flip colors
o, // overline color
u, // underline color
P, // Polybar control tag
l, // Left alignment
r, // Right alignment
c, // Center alignment
};
/**
@ -35,7 +35,7 @@ namespace tags {
*/
enum class controltag {
NONE = 0,
R, // Reset all open tags (B, F, T, o, u). Used at module edges
R, // Reset all open tags (B, F, T, o, u). Used at module edges
};
enum class color_type { RESET = 0, COLOR };
@ -89,7 +89,7 @@ namespace tags {
/**
* For for 'O' tags
*/
int offset;
extent_val offset;
/**
* For for 'P' tags
*/
@ -113,6 +113,6 @@ namespace tags {
using format_string = vector<element>;
} // namespace tags
} // namespace tags
POLYBAR_NS_END

View File

@ -19,7 +19,9 @@ class rgba {
operator string() const;
operator uint32_t() const;
operator bool() const;
bool operator==(const rgba& other) const;
bool operator!=(const rgba& other) const;
uint32_t value() const;
type get_type() const;

31
include/utils/units.hpp Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <cassert>
#include <cmath>
#include <stdexcept>
#include <string>
#include "components/types.hpp"
#include "utils/string.hpp"
POLYBAR_NS
namespace units_utils {
int point_to_pixel(double point, double dpi);
int extent_to_pixel(const extent_val size, double dpi);
unsigned extent_to_pixel_nonnegative(const extent_val size, double dpi);
extent_type parse_extent_unit(const string& str);
extent_val parse_extent(const string& str);
string extent_to_string(extent_val extent);
unsigned percentage_with_offset_to_pixel(percentage_with_offset g_format, double max, double dpi);
spacing_type parse_spacing_unit(const string& str);
spacing_val parse_spacing(const string& str);
} // namespace units_utils
POLYBAR_NS_END

View File

@ -121,6 +121,7 @@ set(POLY_SOURCES
${src_dir}/utils/process.cpp
${src_dir}/utils/socket.cpp
${src_dir}/utils/string.cpp
${src_dir}/utils/units.cpp
${src_dir}/x11/atoms.cpp
${src_dir}/x11/background_manager.cpp

View File

@ -14,6 +14,7 @@
#include "utils/color.hpp"
#include "utils/math.hpp"
#include "utils/string.hpp"
#include "utils/units.hpp"
#include "x11/atoms.hpp"
#include "x11/connection.hpp"
#include "x11/ewmh.hpp"
@ -157,7 +158,39 @@ bar::bar(connection& conn, signal_emitter& emitter, const config& config, const
m_opts.wmname = m_conf.get(bs, "wm-name", "polybar-" + bs.substr(4) + "_" + m_opts.monitor->name);
m_opts.wmname = string_util::replace(m_opts.wmname, " ", "-");
// Configure DPI
{
double dpi_x = 96, dpi_y = 96;
if (m_conf.has(m_conf.section(), "dpi")) {
dpi_x = dpi_y = m_conf.get<double>("dpi");
} else {
if (m_conf.has(m_conf.section(), "dpi-x")) {
dpi_x = m_conf.get<double>("dpi-x");
}
if (m_conf.has(m_conf.section(), "dpi-y")) {
dpi_y = m_conf.get<double>("dpi-y");
}
}
// dpi to be computed
if (dpi_x <= 0 || dpi_y <= 0) {
auto screen = m_connection.screen();
if (dpi_x <= 0) {
dpi_x = screen->width_in_pixels * 25.4 / screen->width_in_millimeters;
}
if (dpi_y <= 0) {
dpi_y = screen->height_in_pixels * 25.4 / screen->height_in_millimeters;
}
}
m_opts.dpi_x = dpi_x;
m_opts.dpi_y = dpi_y;
m_log.info("Configured DPI = %gx%g", dpi_x, dpi_y);
}
// Load configuration values
m_opts.origin = m_conf.get(bs, "bottom", false) ? edge::BOTTOM : edge::TOP;
m_opts.spacing = m_conf.get(bs, "spacing", m_opts.spacing);
m_opts.separator = drawtypes::load_optional_label(m_conf, bs, "separator", "");
@ -171,11 +204,11 @@ bar::bar(connection& conn, signal_emitter& emitter, const config& config, const
m_opts.radius.bottom_left = m_conf.get(bs, "radius-bottom-left", bottom);
m_opts.radius.bottom_right = m_conf.get(bs, "radius-bottom-right", bottom);
auto padding = m_conf.get<unsigned int>(bs, "padding", 0U);
auto padding = m_conf.get(bs, "padding", ZERO_SPACE);
m_opts.padding.left = m_conf.get(bs, "padding-left", padding);
m_opts.padding.right = m_conf.get(bs, "padding-right", padding);
auto margin = m_conf.get<unsigned int>(bs, "module-margin", 0U);
auto margin = m_conf.get(bs, "module-margin", ZERO_SPACE);
m_opts.module_margin.left = m_conf.get(bs, "module-margin-left", margin);
m_opts.module_margin.right = m_conf.get(bs, "module-margin-right", margin);
@ -186,8 +219,10 @@ bar::bar(connection& conn, signal_emitter& emitter, const config& config, const
}
// Load values used to adjust the struts atom
m_opts.strut.top = m_conf.get("global/wm", "margin-top", 0);
m_opts.strut.bottom = m_conf.get("global/wm", "margin-bottom", 0);
auto margin_top = m_conf.get("global/wm", "margin-top", percentage_with_offset{});
auto margin_bottom = m_conf.get("global/wm", "margin-bottom", percentage_with_offset{});
m_opts.strut.top = units_utils::percentage_with_offset_to_pixel(margin_top, m_opts.monitor->h, m_opts.dpi_y);
m_opts.strut.bottom = units_utils::percentage_with_offset_to_pixel(margin_bottom, m_opts.monitor->h, m_opts.dpi_y);
// Load commands used for fallback click handlers
vector<action> actions;
@ -240,44 +275,51 @@ bar::bar(connection& conn, signal_emitter& emitter, const config& config, const
// Load over-/underline
auto line_color = m_conf.get(bs, "line-color", rgba{0xFFFF0000});
auto line_size = m_conf.get(bs, "line-size", 0);
auto line_size = m_conf.get(bs, "line-size", ZERO_PX_EXTENT);
m_opts.overline.size = m_conf.get(bs, "overline-size", line_size);
auto overline_size = m_conf.get(bs, "overline-size", line_size);
auto underline_size = m_conf.get(bs, "underline-size", line_size);
m_opts.overline.size = units_utils::extent_to_pixel_nonnegative(overline_size, m_opts.dpi_y);
m_opts.overline.color = parse_or_throw_color("overline-color", line_color);
m_opts.underline.size = m_conf.get(bs, "underline-size", line_size);
m_opts.underline.size = units_utils::extent_to_pixel_nonnegative(underline_size, m_opts.dpi_y);
m_opts.underline.color = parse_or_throw_color("underline-color", line_color);
// Load border settings
auto border_color = m_conf.get(bs, "border-color", rgba{0x00000000});
auto border_size = m_conf.get(bs, "border-size", ""s);
auto border_size = m_conf.get(bs, "border-size", percentage_with_offset{});
auto border_top = m_conf.deprecated(bs, "border-top", "border-top-size", border_size);
auto border_bottom = m_conf.deprecated(bs, "border-bottom", "border-bottom-size", border_size);
auto border_left = m_conf.deprecated(bs, "border-left", "border-left-size", border_size);
auto border_right = m_conf.deprecated(bs, "border-right", "border-right-size", border_size);
m_opts.borders.emplace(edge::TOP, border_settings{});
m_opts.borders[edge::TOP].size = geom_format_to_pixels(border_top, m_opts.monitor->h);
m_opts.borders[edge::TOP].size =
units_utils::percentage_with_offset_to_pixel(border_top, m_opts.monitor->h, m_opts.dpi_y);
m_opts.borders[edge::TOP].color = parse_or_throw_color("border-top-color", border_color);
m_opts.borders.emplace(edge::BOTTOM, border_settings{});
m_opts.borders[edge::BOTTOM].size = geom_format_to_pixels(border_bottom, m_opts.monitor->h);
m_opts.borders[edge::BOTTOM].size =
units_utils::percentage_with_offset_to_pixel(border_bottom, m_opts.monitor->h, m_opts.dpi_y);
m_opts.borders[edge::BOTTOM].color = parse_or_throw_color("border-bottom-color", border_color);
m_opts.borders.emplace(edge::LEFT, border_settings{});
m_opts.borders[edge::LEFT].size = geom_format_to_pixels(border_left, m_opts.monitor->w);
m_opts.borders[edge::LEFT].size =
units_utils::percentage_with_offset_to_pixel(border_left, m_opts.monitor->w, m_opts.dpi_x);
m_opts.borders[edge::LEFT].color = parse_or_throw_color("border-left-color", border_color);
m_opts.borders.emplace(edge::RIGHT, border_settings{});
m_opts.borders[edge::RIGHT].size = geom_format_to_pixels(border_right, m_opts.monitor->w);
m_opts.borders[edge::RIGHT].size =
units_utils::percentage_with_offset_to_pixel(border_right, m_opts.monitor->w, m_opts.dpi_x);
m_opts.borders[edge::RIGHT].color = parse_or_throw_color("border-right-color", border_color);
// Load geometry values
auto w = m_conf.get(m_conf.section(), "width", "100%"s);
auto h = m_conf.get(m_conf.section(), "height", "24"s);
auto offsetx = m_conf.get(m_conf.section(), "offset-x", ""s);
auto offsety = m_conf.get(m_conf.section(), "offset-y", ""s);
auto w = m_conf.get(m_conf.section(), "width", percentage_with_offset{100.});
auto h = m_conf.get(m_conf.section(), "height", percentage_with_offset{0., {extent_type::PIXEL, 24}});
auto offsetx = m_conf.get(m_conf.section(), "offset-x", percentage_with_offset{});
auto offsety = m_conf.get(m_conf.section(), "offset-y", percentage_with_offset{});
m_opts.size.w = geom_format_to_pixels(w, m_opts.monitor->w);
m_opts.size.h = geom_format_to_pixels(h, m_opts.monitor->h);
m_opts.offset.x = geom_format_to_pixels(offsetx, m_opts.monitor->w);
m_opts.offset.y = geom_format_to_pixels(offsety, m_opts.monitor->h);
m_opts.size.w = units_utils::percentage_with_offset_to_pixel(w, m_opts.monitor->w, m_opts.dpi_x);
m_opts.size.h = units_utils::percentage_with_offset_to_pixel(h, m_opts.monitor->h, m_opts.dpi_y);
m_opts.offset.x = units_utils::percentage_with_offset_to_pixel(offsetx, m_opts.monitor->w, m_opts.dpi_x);
m_opts.offset.y = units_utils::percentage_with_offset_to_pixel(offsety, m_opts.monitor->h, m_opts.dpi_y);
// Apply offsets
m_opts.pos.x = m_opts.offset.x + m_opts.monitor->x;

View File

@ -7,6 +7,8 @@
#include "utils/color.hpp"
#include "utils/string.hpp"
#include "utils/time.hpp"
#include "utils/units.hpp"
POLYBAR_NS
using namespace tags;
@ -86,9 +88,9 @@ string builder::flush() {
/**
* Insert raw text string
*/
void builder::append(string text) {
void builder::append(const string& text) {
m_output.reserve(text.size());
m_output += move(text);
m_output += text;
}
/**
@ -96,12 +98,12 @@ void builder::append(string text) {
*
* This will also parse raw syntax tags
*/
void builder::node(string str) {
void builder::node(const string& str) {
if (str.empty()) {
return;
}
append(move(str));
append(str);
}
/**
@ -109,9 +111,9 @@ void builder::node(string str) {
*
* \see builder::node
*/
void builder::node(string str, int font_index) {
void builder::node(const string& str, int font_index) {
font(font_index);
node(move(str));
node(str);
font_close();
}
@ -125,8 +127,8 @@ void builder::node(const label_t& label) {
auto text = label->get();
if (label->m_margin.left > 0) {
space(label->m_margin.left);
if (label->m_margin.left) {
spacing(label->m_margin.left);
}
if (label->m_overline.has_color()) {
@ -143,14 +145,14 @@ void builder::node(const label_t& label) {
color(label->m_foreground);
}
if (label->m_padding.left > 0) {
space(label->m_padding.left);
if (label->m_padding.left) {
spacing(label->m_padding.left);
}
node(text, label->m_font);
if (label->m_padding.right > 0) {
space(label->m_padding.right);
if (label->m_padding.right) {
spacing(label->m_padding.right);
}
if (label->m_background.has_color()) {
@ -167,8 +169,8 @@ void builder::node(const label_t& label) {
overline_close();
}
if (label->m_margin.right > 0) {
space(label->m_margin.right);
if (label->m_margin.right) {
spacing(label->m_margin.right);
}
}
@ -200,42 +202,29 @@ void builder::node_repeat(const label_t& label, size_t n) {
}
/**
* Insert tag that will offset the contents by given pixels
* Insert tag that will offset the contents by the given extent
*/
void builder::offset(int pixels) {
if (pixels == 0) {
void builder::offset(extent_val extent) {
if (extent) {
return;
}
tag_open(syntaxtag::O, to_string(pixels));
tag_open(syntaxtag::O, units_utils::extent_to_string(extent));
}
/**
* Insert spaces
* Insert spacing
*/
void builder::space(size_t width) {
if (width) {
m_output.append(width, ' ');
} else {
space();
void builder::spacing(spacing_val size) {
if (!size && m_bar.spacing) {
// TODO remove once the deprecated spacing key in the bar section is removed
// The spacing in the bar section acts as a fallback for all spacing value
size = m_bar.spacing;
}
}
void builder::space() {
m_output.append(m_bar.spacing, ' ');
}
/**
* Remove trailing space
*/
void builder::remove_trailing_space(size_t len) {
if (len == 0_z || len > m_output.size()) {
return;
} else if (m_output.substr(m_output.size() - len) == string(len, ' ')) {
m_output.erase(m_output.size() - len);
if (size) {
m_output += get_spacing_format_string(size);
}
}
void builder::remove_trailing_space() {
remove_trailing_space(m_bar.spacing);
}
/**
* Insert tag to alter the current font index
@ -574,4 +563,33 @@ void builder::tag_close(attribute attr) {
}
}
string builder::get_spacing_format_string(const spacing_val& space) {
float value = space.value;
if (value == 0) {
return "";
}
string out;
if (space.type == spacing_type::SPACE) {
out += string(value, ' ');
} else {
out += "%{O";
switch (space.type) {
case spacing_type::POINT:
out += to_string(value) + "pt";
break;
case spacing_type::PIXEL:
out += to_string(static_cast<int>(value)) + "px";
break;
default:
break;
}
out += '}';
}
return out;
}
POLYBAR_NS_END

View File

@ -1,13 +1,16 @@
#include "components/config.hpp"
#include <climits>
#include <cmath>
#include <fstream>
#include "cairo/utils.hpp"
#include "components/types.hpp"
#include "utils/color.hpp"
#include "utils/env.hpp"
#include "utils/factory.hpp"
#include "utils/string.hpp"
#include "utils/units.hpp"
POLYBAR_NS
@ -217,6 +220,38 @@ unsigned long long config::convert(string&& value) const {
return v < ULLONG_MAX ? v : 0ULL;
}
template <>
spacing_val config::convert(string&& value) const {
return units_utils::parse_spacing(value);
}
template <>
extent_val config::convert(std::string&& value) const {
return units_utils::parse_extent(value);
}
/**
* Allows a new format for pixel sizes (like width in the bar section)
*
* The new format is X%:Z, where X is in [0, 100], and Z is any real value
* describing a pixel offset. The actual value is calculated by X% * max + Z
*/
template <>
percentage_with_offset config::convert(string&& value) const {
size_t i = value.find(':');
if (i == std::string::npos) {
if (value.find('%') != std::string::npos) {
return {std::stod(value), {}};
} else {
return {0., convert<extent_val>(move(value))};
}
} else {
std::string percentage = value.substr(0, i - 1);
return {std::stod(percentage), convert<extent_val>(value.substr(i + 1))};
}
}
template <>
chrono::seconds config::convert(string&& value) const {
return chrono::seconds{convert<chrono::seconds::rep>(forward<string>(value))};

View File

@ -465,10 +465,10 @@ void controller::process_inputdata(string&& cmd) {
bool controller::process_update(bool force) {
const bar_settings& bar{m_bar->settings()};
string contents;
string padding_left(bar.padding.left, ' ');
string padding_right(bar.padding.right, ' ');
string margin_left(bar.module_margin.left, ' ');
string margin_right(bar.module_margin.right, ' ');
string padding_left = builder::get_spacing_format_string(bar.padding.left);
string padding_right = builder::get_spacing_format_string(bar.padding.right);
string margin_left = builder::get_spacing_format_string(bar.module_margin.left);
string margin_right = builder::get_spacing_format_string(bar.module_margin.right);
builder build{bar};
build.node(bar.separator);

View File

@ -8,6 +8,7 @@
#include "events/signal_emitter.hpp"
#include "events/signal_receiver.hpp"
#include "utils/math.hpp"
#include "utils/units.hpp"
#include "x11/atoms.hpp"
#include "x11/background_manager.hpp"
#include "x11/connection.hpp"
@ -109,9 +110,9 @@ renderer::renderer(connection& conn, signal_emitter& sig, const config& conf, co
m_log.trace("renderer: Allocate alignment blocks");
{
m_blocks.emplace(alignment::LEFT, alignment_block{nullptr, 0.0, 0.0});
m_blocks.emplace(alignment::CENTER, alignment_block{nullptr, 0.0, 0.0});
m_blocks.emplace(alignment::RIGHT, alignment_block{nullptr, 0.0, 0.0});
m_blocks.emplace(alignment::LEFT, alignment_block{nullptr, 0.0, 0.0, 0.});
m_blocks.emplace(alignment::CENTER, alignment_block{nullptr, 0.0, 0.0, 0.});
m_blocks.emplace(alignment::RIGHT, alignment_block{nullptr, 0.0, 0.0, 0.});
}
m_log.trace("renderer: Allocate cairo components");
@ -122,31 +123,6 @@ renderer::renderer(connection& conn, signal_emitter& sig, const config& conf, co
m_log.trace("renderer: Load fonts");
{
double dpi_x = 96, dpi_y = 96;
if (m_conf.has(m_conf.section(), "dpi")) {
dpi_x = dpi_y = m_conf.get<double>("dpi");
} else {
if (m_conf.has(m_conf.section(), "dpi-x")) {
dpi_x = m_conf.get<double>("dpi-x");
}
if (m_conf.has(m_conf.section(), "dpi-y")) {
dpi_y = m_conf.get<double>("dpi-y");
}
}
// dpi to be computed
if (dpi_x <= 0 || dpi_y <= 0) {
auto screen = m_connection.screen();
if (dpi_x <= 0) {
dpi_x = screen->width_in_pixels * 25.4 / screen->width_in_millimeters;
}
if (dpi_y <= 0) {
dpi_y = screen->height_in_pixels * 25.4 / screen->height_in_millimeters;
}
}
m_log.info("Configured DPI = %gx%g", dpi_x, dpi_y);
auto fonts = m_conf.get_list<string>(m_conf.section(), "font", {});
if (fonts.empty()) {
m_log.warn("No fonts specified, using fallback font \"fixed\"");
@ -161,7 +137,7 @@ renderer::renderer(connection& conn, signal_emitter& sig, const config& conf, co
offset = std::strtol(pattern.substr(pos + 1).c_str(), nullptr, 10);
pattern.erase(pos);
}
auto font = cairo::make_font(*m_context, string{pattern}, offset, dpi_x, dpi_y);
auto font = cairo::make_font(*m_context, string{pattern}, offset, m_bar.dpi_x, m_bar.dpi_y);
m_log.notice("Loaded font \"%s\" (name=%s, offset=%i, file=%s)", pattern, font->name(), offset, font->file());
*m_context << move(font);
}
@ -287,7 +263,7 @@ void renderer::end() {
// the bar will be filled by the wallpaper creating illusion of transparency.
if (m_pseudo_transparency) {
cairo_pattern_t* barcontents{};
m_context->pop(&barcontents); // corresponding push is in renderer::begin
m_context->pop(&barcontents); // corresponding push is in renderer::begin
auto root_bg = m_background->get_surface();
if (root_bg != nullptr) {
@ -491,7 +467,7 @@ double renderer::block_y(alignment) const {
* Get block width for given alignment
*/
double renderer::block_w(alignment a) const {
return m_blocks.at(a).x;
return m_blocks.at(a).width;
}
/**
@ -501,6 +477,14 @@ double renderer::block_h(alignment) const {
return m_rect.height;
}
void renderer::increase_x(double dx) {
m_blocks[m_align].x += dx;
/*
* The width only increases when x becomes larger than the old width.
*/
m_blocks[m_align].width = std::max(m_blocks[m_align].width, m_blocks[m_align].x);
}
/**
* Fill background color
*/
@ -695,11 +679,17 @@ void renderer::render_text(const tags::context& ctxt, const string&& contents) {
origin.x = m_rect.x + m_blocks[m_align].x;
origin.y = m_rect.y + m_rect.height / 2.0;
double x_old = m_blocks[m_align].x;
/*
* This variable is increased by the text renderer
*/
double x_new = x_old;
cairo::textblock block{};
block.align = m_align;
block.contents = contents;
block.font = ctxt.get_font();
block.x_advance = &m_blocks[m_align].x;
block.x_advance = &x_new;
block.y_advance = &m_blocks[m_align].y;
block.bg_rect = cairo::rect{0.0, 0.0, 0.0, 0.0};
@ -724,7 +714,9 @@ void renderer::render_text(const tags::context& ctxt, const string&& contents) {
*m_context << block;
m_context->restore();
double dx = m_rect.x + m_blocks[m_align].x - origin.x;
double dx = x_new - x_old;
increase_x(dx);
if (dx > 0.0) {
if (ctxt.has_underline()) {
fill_underline(ctxt.get_ul(), origin.x, dx);
@ -736,9 +728,26 @@ void renderer::render_text(const tags::context& ctxt, const string&& contents) {
}
}
void renderer::render_offset(const tags::context&, int pixels) {
m_log.trace_x("renderer: offset_pixel(%f)", pixels);
m_blocks[m_align].x += pixels;
void renderer::draw_offset(rgba color, double x, double w) {
if (w > 0 && color != m_bar.background) {
m_log.trace_x("renderer: offset(x=%f, w=%f)", x, w);
m_context->save();
*m_context << m_comp_bg;
*m_context << color;
*m_context << cairo::rect{
m_rect.x + x, static_cast<double>(m_rect.y), w, static_cast<double>(m_rect.y + m_rect.height)};
m_context->fill();
m_context->restore();
}
}
void renderer::render_offset(const tags::context& ctxt, const extent_val offset) {
m_log.trace_x("renderer: offset_pixel(%f)", offset);
int offset_width = units_utils::extent_to_pixel(offset, m_bar.dpi_x);
rgba bg = ctxt.get_bg();
draw_offset(bg, m_blocks[m_align].x, offset_width);
increase_x(offset_width);
}
void renderer::change_alignment(const tags::context& ctxt) {
@ -754,6 +763,7 @@ void renderer::change_alignment(const tags::context& ctxt) {
m_align = align;
m_blocks[m_align].x = 0.0;
m_blocks[m_align].y = 0.0;
m_blocks[m_align].width = 0.;
m_context->push();
m_log.trace_x("renderer: push(%i)", static_cast<int>(m_align));

View File

@ -113,16 +113,16 @@ namespace drawtypes {
if (label->m_font != 0) {
m_font = label->m_font;
}
if (label->m_padding.left != 0U) {
if (label->m_padding.left) {
m_padding.left = label->m_padding.left;
}
if (label->m_padding.right != 0U) {
if (label->m_padding.right) {
m_padding.right = label->m_padding.right;
}
if (label->m_margin.left != 0U) {
if (label->m_margin.left) {
m_margin.left = label->m_margin.left;
}
if (label->m_margin.right != 0U) {
if (label->m_margin.right) {
m_margin.right = label->m_margin.right;
}
if (label->m_maxlen != 0_z) {
@ -147,16 +147,16 @@ namespace drawtypes {
if (m_font == 0 && label->m_font != 0) {
m_font = label->m_font;
}
if (m_padding.left == 0U && label->m_padding.left != 0U) {
if (!m_padding.left && label->m_padding.left) {
m_padding.left = label->m_padding.left;
}
if (m_padding.right == 0U && label->m_padding.right != 0U) {
if (!m_padding.right && label->m_padding.right) {
m_padding.right = label->m_padding.right;
}
if (m_margin.left == 0U && label->m_margin.left != 0U) {
if (!m_margin.left && label->m_margin.left) {
m_margin.left = label->m_margin.left;
}
if (m_margin.right == 0U && label->m_margin.right != 0U) {
if (!m_margin.right && label->m_margin.right) {
m_margin.right = label->m_margin.right;
}
if (m_maxlen == 0_z && label->m_maxlen != 0_z) {
@ -182,14 +182,23 @@ namespace drawtypes {
if (required) {
text = conf.get(section, name);
} else {
text = conf.get(section, name, move(def));
text = conf.get(section, name, def);
}
const auto get_left_right = [&](string key) {
auto value = conf.get(section, key, 0U);
auto left = conf.get(section, key + "-left", value);
auto right = conf.get(section, key + "-right", value);
return side_values{static_cast<unsigned short int>(left), static_cast<unsigned short int>(right)};
const auto get_left_right = [&](string&& key) {
const auto parse_or_throw = [&](const string& key, spacing_val default_value) {
try {
return conf.get(section, key, default_value);
} catch (const std::exception& err) {
throw application_error(
sstream() << "Failed to set " << section << "." << key << " (reason: " << err.what() << ")");
}
};
auto value = parse_or_throw(key, ZERO_SPACE);
auto left = parse_or_throw(key + "-left", value);
auto right = parse_or_throw(key + "-right", value);
return side_values{left, right};
};
padding = get_left_right(name + "-padding");
@ -271,10 +280,12 @@ namespace drawtypes {
}
bool ellipsis = conf.get(section, name + "-ellipsis", true);
// clang-format off
if (ellipsis && maxlen > 0 && maxlen < 3) {
throw application_error(sstream() << "Label " << section << "." << name << " has maxlen " << maxlen
<< ", which is smaller than length of ellipsis (3)");
}
// clang-format on
// clang-format off
return std::make_shared<label>(text,
@ -297,9 +308,9 @@ namespace drawtypes {
* Create a label by loading optional values from the configuration
*/
label_t load_optional_label(const config& conf, string section, string name, string def) {
return load_label(conf, move(section), move(name), false, move(def));
return load_label(conf, section, move(name), false, move(def));
}
} // namespace drawtypes
} // namespace drawtypes
POLYBAR_NS_END

View File

@ -162,7 +162,7 @@ int main(int argc, char** argv) {
reload = true;
}
} catch (const exception& err) {
logger.err(err.what());
logger.err("Uncaught exception, shutting down: %s", err.what());
exit_code = EXIT_FAILURE;
}

View File

@ -387,7 +387,7 @@ namespace modules {
string output;
for (m_index = 0U; m_index < m_monitors.size(); m_index++) {
if (m_index > 0) {
m_builder->space(m_formatter->get(DEFAULT_FORMAT)->spacing);
m_builder->spacing(m_formatter->get(DEFAULT_FORMAT)->spacing);
}
output += this->event_module::get_output();
}

View File

@ -1,14 +1,13 @@
#include "modules/cpu.hpp"
#include <fstream>
#include <istream>
#include "modules/cpu.hpp"
#include "drawtypes/label.hpp"
#include "drawtypes/progressbar.hpp"
#include "drawtypes/ramp.hpp"
#include "utils/math.hpp"
#include "modules/meta/base.inl"
#include "utils/math.hpp"
POLYBAR_NS
@ -18,7 +17,7 @@ namespace modules {
cpu_module::cpu_module(const bar_settings& bar, string name_) : timer_module<cpu_module>(bar, move(name_)) {
set_interval(1s);
m_totalwarn = m_conf.get(name(), "warn-percentage", m_totalwarn);
m_ramp_padding = m_conf.get<decltype(m_ramp_padding)>(name(), "ramp-coreload-spacing", 1);
m_ramp_padding = m_conf.get(name(), "ramp-coreload-spacing", m_ramp_padding);
m_formatter->add(DEFAULT_FORMAT, TAG_LABEL, {TAG_LABEL, TAG_BAR_LOAD, TAG_RAMP_LOAD, TAG_RAMP_LOAD_PER_CORE});
m_formatter->add_optional(FORMAT_WARN, {TAG_LABEL_WARN, TAG_BAR_LOAD, TAG_RAMP_LOAD, TAG_RAMP_LOAD_PER_CORE});
@ -73,7 +72,8 @@ namespace modules {
const auto replace_tokens = [&](label_t& label) {
label->reset_tokens();
label->replace_token("%percentage%", to_string(static_cast<int>(m_total + 0.5)));
label->replace_token("%percentage-sum%", to_string(static_cast<int>(m_total * static_cast<float>(cores_n) + 0.5)));
label->replace_token(
"%percentage-sum%", to_string(static_cast<int>(m_total * static_cast<float>(cores_n) + 0.5)));
label->replace_token("%percentage-cores%", string_util::join(percentage_cores, "% ") + "%");
for (size_t i = 0; i < percentage_cores.size(); i++) {
@ -99,7 +99,6 @@ namespace modules {
}
}
bool cpu_module::build(builder* builder, const string& tag) const {
if (tag == TAG_LABEL) {
builder->node(m_label);
@ -113,7 +112,7 @@ namespace modules {
auto i = 0;
for (auto&& load : m_load) {
if (i++ > 0) {
builder->space(m_ramp_padding);
builder->spacing(m_ramp_padding);
}
builder->node(m_rampload_core->get_by_percentage_with_borders(load, 0.0f, m_totalwarn));
}
@ -179,6 +178,6 @@ namespace modules {
return math_util::cap<float>(percentage, 0, 100);
}
}
} // namespace modules
POLYBAR_NS_END

View File

@ -143,7 +143,7 @@ namespace modules {
* are not empty and there is already other content in the module.
*/
if (!output.empty() && !mount_output.empty()) {
m_builder->space(m_spacing);
m_builder->spacing(m_spacing);
output += m_builder->flush();
}
output += mount_output;

View File

@ -76,7 +76,7 @@ namespace modules {
// Insert separator after menu-toggle and before menu-items for expand-right=true
if (m_expand_right && *m_labelseparator) {
builder->node(m_labelseparator);
builder->space(spacing);
builder->spacing(spacing);
}
auto&& items = m_levels[m_level]->items;
for (size_t i = 0; i < items.size(); i++) {
@ -84,14 +84,14 @@ namespace modules {
builder->action(
mousebtn::LEFT, *this, string(EVENT_EXEC), to_string(m_level) + "-" + to_string(i), item->label);
if (item != m_levels[m_level]->items.back()) {
builder->space(spacing);
builder->spacing(spacing);
if (*m_labelseparator) {
builder->node(m_labelseparator);
builder->space(spacing);
builder->spacing(spacing);
}
// Insert separator after last menu-item and before menu-toggle for expand-right=false
} else if (!m_expand_right && *m_labelseparator) {
builder->space(spacing);
builder->spacing(spacing);
builder->node(m_labelseparator);
}
}

View File

@ -15,11 +15,11 @@ namespace modules {
builder->flush();
return "";
}
if (offset != 0) {
if (offset) {
builder->offset(offset);
}
if (margin > 0) {
builder->space(margin);
if (margin) {
builder->spacing(margin);
}
if (bg.has_color()) {
builder->background(bg);
@ -36,8 +36,8 @@ namespace modules {
if (font > 0) {
builder->font(font);
}
if (padding > 0) {
builder->space(padding);
if (padding) {
builder->spacing(padding);
}
builder->node(prefix);
@ -58,8 +58,8 @@ namespace modules {
builder->append(move(output));
builder->node(suffix);
if (padding > 0) {
builder->space(padding);
if (padding) {
builder->spacing(padding);
}
if (font > 0) {
builder->font_close();
@ -76,8 +76,8 @@ namespace modules {
if (bg.has_color()) {
builder->background_close();
}
if (margin > 0) {
builder->space(margin);
if (margin) {
builder->spacing(margin);
}
return builder->flush();
@ -87,8 +87,9 @@ namespace modules {
// module_formatter {{{
void module_formatter::add_value(string&& name, string&& value, vector<string>&& tags, vector<string>&& whitelist) {
const auto formatdef = [&](
const string& param, const auto& fallback) { return m_conf.get("settings", "format-" + param, fallback); };
const auto formatdef = [&](const string& param, const auto& fallback) {
return m_conf.get("settings", "format-" + param, fallback);
};
auto format = make_unique<module_format>();
format->value = move(value);

View File

@ -187,7 +187,7 @@ namespace modules {
for (auto&& indicator : m_indicators) {
if (*indicator.second) {
if (n++) {
builder->space(m_formatter->get(DEFAULT_FORMAT)->spacing);
builder->spacing(m_formatter->get(DEFAULT_FORMAT)->spacing);
}
builder->node(indicator.second);
}

View File

@ -309,7 +309,7 @@ namespace modules {
string output;
for (m_index = 0; m_index < m_viewports.size(); m_index++) {
if (m_index > 0) {
m_builder->space(m_formatter->get(DEFAULT_FORMAT)->spacing);
m_builder->spacing(m_formatter->get(DEFAULT_FORMAT)->spacing);
}
output += module::get_output();
}

View File

@ -3,6 +3,8 @@
#include <cassert>
#include <cctype>
#include "utils/units.hpp"
POLYBAR_NS
namespace tags {
@ -340,26 +342,18 @@ namespace tags {
}
}
int parser::parse_offset() {
extent_val parser::parse_offset() {
string s = get_tag_value();
if (s.empty()) {
return 0;
return ZERO_PX_EXTENT;
}
try {
size_t ptr;
int ret = std::stoi(s, &ptr, 10);
if (ptr != s.size()) {
throw offset_error(s, "Offset contains non-number characters");
}
return ret;
return units_utils::parse_extent(string{s});
} catch (const std::exception& err) {
throw offset_error(s, err.what());
}
return 0;
}
controltag parser::parse_control() {
@ -508,6 +502,6 @@ namespace tags {
return s;
}
} // namespace tags
} // namespace tags
POLYBAR_NS_END

View File

@ -98,10 +98,18 @@ bool rgba::operator==(const rgba& other) const {
}
}
bool rgba::operator!=(const rgba& other) const {
return !(*this == other);
}
rgba::operator uint32_t() const {
return m_value;
}
rgba::operator bool() const {
return has_color();
}
uint32_t rgba::value() const {
return this->m_value;
}

137
src/utils/units.cpp Normal file
View File

@ -0,0 +1,137 @@
#include "utils/units.hpp"
#include "common.hpp"
#include "components/types.hpp"
#include "errors.hpp"
#include "utils/math.hpp"
POLYBAR_NS
namespace units_utils {
/**
* Converts points to pixels under the given DPI (PPI).
*
* 1 pt = 1/72in, so point / 72 * DPI = #pixels
*/
int point_to_pixel(double point, double dpi) {
return dpi * point / 72.0;
}
/**
* Converts an extent value to a pixel value according to the given DPI (if needed).
*/
int extent_to_pixel(const extent_val size, double dpi) {
if (size.type == extent_type::PIXEL) {
return size.value;
}
return point_to_pixel(size.value, dpi);
}
/**
* Same as extent_to_pixel but is capped below at 0 pixels.
*/
unsigned extent_to_pixel_nonnegative(const extent_val size, double dpi) {
return std::max(0, extent_to_pixel(size, dpi));
}
/**
* Converts a percentage with offset into pixels
*/
unsigned int percentage_with_offset_to_pixel(percentage_with_offset g_format, double max, double dpi) {
int offset_pixel = extent_to_pixel(g_format.offset, dpi);
return static_cast<unsigned int>(
std::max<double>(0, math_util::percentage_to_value<double, double>(g_format.percentage, max) + offset_pixel));
}
extent_type parse_extent_unit(const string& str) {
if (!str.empty()) {
if (str == "px") {
return extent_type::PIXEL;
} else if (str == "pt") {
return extent_type::POINT;
} else {
throw std::runtime_error("Unrecognized unit '" + str + "'");
}
} else {
return extent_type::PIXEL;
}
}
extent_val parse_extent(const string& str) {
size_t pos;
auto size_value = std::stof(str, &pos);
string unit = string_util::trim(str.substr(pos));
extent_type type = parse_extent_unit(unit);
// Pixel values should be integers
if (type == extent_type::PIXEL) {
size_value = std::trunc(size_value);
}
return {type, size_value};
}
string extent_to_string(extent_val extent) {
std::stringstream ss;
switch (extent.type) {
case extent_type::POINT:
ss << extent.value << "pt";
break;
case extent_type::PIXEL:
ss << static_cast<int>(extent.value) << "px";
break;
}
return ss.str();
}
spacing_type parse_spacing_unit(const string& str) {
if (!str.empty()) {
if (str == "px") {
return spacing_type::PIXEL;
} else if (str == "pt") {
return spacing_type::POINT;
} else {
throw std::runtime_error("Unrecognized unit '" + str + "'");
}
} else {
return spacing_type::SPACE;
}
}
spacing_val parse_spacing(const string& str) {
size_t pos;
auto size_value = std::stof(str, &pos);
if (size_value < 0) {
throw runtime_error(sstream() << "value '" << str << "' must not be negative");
}
spacing_type type;
string unit = string_util::trim(str.substr(pos));
if (!unit.empty()) {
if (unit == "px") {
type = spacing_type::PIXEL;
size_value = std::trunc(size_value);
} else if (unit == "pt") {
type = spacing_type::POINT;
} else {
throw runtime_error("Unrecognized unit '" + unit + "'");
}
} else {
type = spacing_type::SPACE;
size_value = std::trunc(size_value);
}
return {type, size_value};
}
} // namespace units_utils
POLYBAR_NS_END

View File

@ -14,6 +14,7 @@
#include "utils/math.hpp"
#include "utils/memory.hpp"
#include "utils/process.hpp"
#include "utils/units.hpp"
#include "x11/background_manager.hpp"
#include "x11/ewmh.hpp"
#include "x11/icccm.hpp"
@ -146,30 +147,23 @@ void tray_manager::setup(const bar_settings& bar_opts) {
m_opts.spacing += conf.get<unsigned int>(bs, "tray-padding", 0);
// Add user-defiend offset
auto offset_x_def = conf.get(bs, "tray-offset-x", ""s);
auto offset_y_def = conf.get(bs, "tray-offset-y", ""s);
auto offset_x = conf.get(bs, "tray-offset-x", percentage_with_offset{});
auto offset_y = conf.get(bs, "tray-offset-y", percentage_with_offset{});
auto offset_x = strtol(offset_x_def.c_str(), nullptr, 10);
auto offset_y = strtol(offset_y_def.c_str(), nullptr, 10);
int max_x;
int max_y;
if (offset_x != 0 && offset_x_def.find('%') != string::npos) {
if (m_opts.detached) {
offset_x = math_util::signed_percentage_to_value<int>(offset_x, bar_opts.monitor->w);
} else {
offset_x = math_util::signed_percentage_to_value<int>(offset_x, inner_area.width);
}
if (m_opts.detached) {
max_x = bar_opts.monitor->w;
max_y = bar_opts.monitor->h;
} else {
max_x = inner_area.width;
max_y = inner_area.height;
}
if (offset_y != 0 && offset_y_def.find('%') != string::npos) {
if (m_opts.detached) {
offset_y = math_util::signed_percentage_to_value<int>(offset_y, bar_opts.monitor->h);
} else {
offset_y = math_util::signed_percentage_to_value<int>(offset_y, inner_area.height);
}
}
m_opts.orig_x += offset_x;
m_opts.orig_y += offset_y;
m_opts.orig_x += units_utils::percentage_with_offset_to_pixel(offset_x, max_x, bar_opts.dpi_x);
m_opts.orig_y += units_utils::percentage_with_offset_to_pixel(offset_y, max_y, bar_opts.dpi_y);
;
m_opts.rel_x = m_opts.orig_x - bar_opts.pos.x;
m_opts.rel_y = m_opts.orig_y - bar_opts.pos.y;
@ -662,10 +656,10 @@ void tray_manager::set_tray_colors() {
const uint16_t b16 = (b << 8) | b;
const uint32_t colors[12] = {
r16, g16, b16, // normal
r16, g16, b16, // error
r16, g16, b16, // warning
r16, g16, b16, // success
r16, g16, b16, // normal
r16, g16, b16, // error
r16, g16, b16, // warning
r16, g16, b16, // success
};
m_connection.change_property(

View File

@ -57,8 +57,8 @@ add_unit_test(utils/scope)
add_unit_test(utils/string)
add_unit_test(utils/file)
add_unit_test(utils/process)
add_unit_test(utils/units)
add_unit_test(components/command_line)
add_unit_test(components/bar)
add_unit_test(components/config_parser)
add_unit_test(drawtypes/label)
add_unit_test(drawtypes/ramp)

View File

@ -1,48 +0,0 @@
#include "common/test.hpp"
#include "components/bar.hpp"
using namespace polybar;
/**
* \brief Class for parameterized tests on geom_format_to_pixels
*
* The first element in the tuple is the expected return value, the second
* value is the format string. The max value is always 1000
*/
class GeomFormatToPixelsTest :
public ::testing::Test,
public ::testing::WithParamInterface<pair<double, string>> {};
vector<pair<double, string>> to_pixels_no_offset_list = {
{1000, "100%"},
{0, "0%"},
{1000, "150%"},
{100, "10%"},
{0, "0"},
{1234, "1234"},
{1.234, "1.234"},
};
vector<pair<double, string>> to_pixels_with_offset_list = {
{1000, "100%:-0"},
{1000, "100%:+0"},
{1010, "100%:+10"},
{990, "100%:-10"},
{10, "0%:+10"},
{1000, "99%:+10"},
{0, "1%:-100"},
};
INSTANTIATE_TEST_SUITE_P(NoOffset, GeomFormatToPixelsTest,
::testing::ValuesIn(to_pixels_no_offset_list));
INSTANTIATE_TEST_SUITE_P(WithOffset, GeomFormatToPixelsTest,
::testing::ValuesIn(to_pixels_with_offset_list));
TEST_P(GeomFormatToPixelsTest, correctness) {
double exp = GetParam().first;
std::string str = GetParam().second;
EXPECT_DOUBLE_EQ(exp, geom_format_to_pixels(str, 1000));
}

View File

@ -15,11 +15,17 @@ using ::testing::Property;
using ::testing::Return;
using ::testing::Truly;
namespace polybar {
inline bool operator==(const extent_val& a, const extent_val& b) {
return a.type == b.type && a.value == b.value;
}
} // namespace polybar
class MockRenderer : public renderer_interface {
public:
MockRenderer(action_context& action_ctxt) : renderer_interface(action_ctxt){};
MOCK_METHOD(void, render_offset, (const context& ctxt, int pixels), (override));
MOCK_METHOD(void, render_offset, (const context& ctxt, const extent_val offset), (override));
MOCK_METHOD(void, render_text, (const context& ctxt, const string&& str), (override));
MOCK_METHOD(void, change_alignment, (const context& ctxt), (override));
MOCK_METHOD(double, get_x, (const context& ctxt), (const, override));
@ -53,7 +59,7 @@ class DispatchTest : public ::testing::Test {
TEST_F(DispatchTest, ignoreFormatting) {
{
InSequence seq;
EXPECT_CALL(r, render_offset(_, 10)).Times(1);
EXPECT_CALL(r, render_offset(_, extent_val{extent_type::PIXEL, 10})).Times(1);
EXPECT_CALL(r, render_text(_, string{"abc"})).Times(1);
EXPECT_CALL(r, render_text(_, string{"foo"})).Times(1);
}
@ -74,7 +80,7 @@ TEST_F(DispatchTest, formatting) {
{
InSequence seq;
EXPECT_CALL(r, render_offset(_, 10)).Times(1);
EXPECT_CALL(r, render_offset(_, extent_val{extent_type::PIXEL, 10})).Times(1);
EXPECT_CALL(r, render_text(match_fg(c1), string{"abc"})).Times(1);
EXPECT_CALL(r, render_text(match_fg(bar_fg), string{"foo"})).Times(1);
EXPECT_CALL(r, change_alignment(match_left_align)).Times(1);

View File

@ -75,10 +75,18 @@ class TestableTagParser : public parser {
EXPECT_EQ(exp, current.tag_data.font);
}
void expect_offset(int exp) {
void expect_offset_pixel(int exp) {
set_current();
assert_format(syntaxtag::O);
EXPECT_EQ(exp, current.tag_data.offset);
EXPECT_EQ(extent_type::PIXEL, current.tag_data.offset.type);
EXPECT_EQ(exp, current.tag_data.offset.value);
}
void expect_offset_points(float exp) {
set_current();
assert_format(syntaxtag::O);
EXPECT_EQ(extent_type::POINT, current.tag_data.offset.type);
EXPECT_EQ(exp, current.tag_data.offset.value);
}
void expect_ctrl(controltag exp) {
@ -308,19 +316,43 @@ TEST_F(TagParserTest, font) {
TEST_F(TagParserTest, offset) {
p.setup_parser_test("%{O}");
p.expect_offset(0);
p.expect_offset_pixel(0);
p.expect_done();
p.setup_parser_test("%{O0}");
p.expect_offset(0);
p.expect_offset_pixel(0);
p.expect_done();
p.setup_parser_test("%{O-112}");
p.expect_offset(-112);
p.expect_offset_pixel(-112);
p.expect_done();
p.setup_parser_test("%{O123}");
p.expect_offset(123);
p.expect_offset_pixel(123);
p.expect_done();
p.setup_parser_test("%{O0pt}");
p.expect_offset_points(0);
p.expect_done();
p.setup_parser_test("%{O-112pt}");
p.expect_offset_points(-112);
p.expect_done();
p.setup_parser_test("%{O123pt}");
p.expect_offset_points(123);
p.expect_done();
p.setup_parser_test("%{O1.5pt}");
p.expect_offset_points(1.5);
p.expect_done();
p.setup_parser_test("%{O1.1px}");
p.expect_offset_pixel(1);
p.expect_done();
p.setup_parser_test("%{O1.1}");
p.expect_offset_pixel(1);
p.expect_done();
}
@ -408,6 +440,9 @@ vector<exception_test> parse_error_test = {
{"%{P}", exc::CTRL},
{"%{PA}", exc::CTRL},
{"%{Oabc}", exc::OFFSET},
{"%{O123foo}", exc::OFFSET},
{"%{O0ptx}", exc::OFFSET},
{"%{O0a}", exc::OFFSET},
{"%{A2:cmd:cmd:}", exc::TAG_END},
{"%{A9}", exc::BTN},
{"%{rQ}", exc::TAG_END},

View File

@ -0,0 +1,146 @@
#include "utils/units.hpp"
#include "common/test.hpp"
#include "utils/units.hpp"
using namespace polybar;
using namespace units_utils;
namespace polybar {
bool operator==(const extent_val lhs, const extent_val rhs) {
return lhs.type == rhs.type && lhs.value == rhs.value;
}
bool operator==(const spacing_val lhs, const spacing_val rhs) {
return lhs.type == rhs.type && lhs.value == rhs.value;
}
} // namespace polybar
/**
* \brief Class for parameterized tests on geom_format_to_pixels
*
* The first element in the tuple is the expected return value, the second
* value represents the format string. The max value is always 1000 and dpi is always 96
*/
class GeomFormatToPixelsTest : public ::testing::Test,
public ::testing::WithParamInterface<pair<unsigned, percentage_with_offset>> {};
vector<pair<unsigned, percentage_with_offset>> to_pixels_no_offset_list = {
{1000, percentage_with_offset{100.}},
{0, percentage_with_offset{0.}},
{1000, percentage_with_offset{150.}},
{100, percentage_with_offset{10.}},
{0, percentage_with_offset{0., ZERO_PX_EXTENT}},
{1234, percentage_with_offset{0., extent_val{extent_type::PIXEL, 1234}}},
{1, percentage_with_offset{0., extent_val{extent_type::PIXEL, 1}}},
};
vector<pair<unsigned, percentage_with_offset>> to_pixels_with_offset_list = {
{1000, percentage_with_offset{100., ZERO_PX_EXTENT}},
{1010, percentage_with_offset{100., extent_val{extent_type::PIXEL, 10}}},
{990, percentage_with_offset{100., extent_val{extent_type::PIXEL, -10}}},
{10, percentage_with_offset{0., extent_val{extent_type::PIXEL, 10}}},
{1000, percentage_with_offset{99., extent_val{extent_type::PIXEL, 10}}},
{0, percentage_with_offset{1., extent_val{extent_type::PIXEL, -100}}},
};
vector<pair<unsigned, percentage_with_offset>> to_pixels_with_units_list = {
{1013, percentage_with_offset{100., extent_val{extent_type::POINT, 10}}},
{987, percentage_with_offset{100., extent_val{extent_type::POINT, -10}}},
{1003, percentage_with_offset{99., extent_val{extent_type::POINT, 10}}},
{13, percentage_with_offset{0., extent_val{extent_type::POINT, 10}}},
{0, percentage_with_offset{0, extent_val{extent_type::POINT, -10}}},
};
INSTANTIATE_TEST_SUITE_P(NoOffset, GeomFormatToPixelsTest, ::testing::ValuesIn(to_pixels_no_offset_list));
INSTANTIATE_TEST_SUITE_P(WithOffset, GeomFormatToPixelsTest, ::testing::ValuesIn(to_pixels_with_offset_list));
INSTANTIATE_TEST_SUITE_P(WithUnits, GeomFormatToPixelsTest, ::testing::ValuesIn(to_pixels_with_units_list));
static constexpr int MAX_WIDTH = 1000;
static constexpr int DPI = 96;
TEST_P(GeomFormatToPixelsTest, correctness) {
unsigned exp = GetParam().first;
percentage_with_offset geometry = GetParam().second;
EXPECT_DOUBLE_EQ(exp, percentage_with_offset_to_pixel(geometry, MAX_WIDTH, DPI));
}
TEST(UnitsUtils, point_to_pixel) {
EXPECT_EQ(72, point_to_pixel(72, 72));
EXPECT_EQ(96, point_to_pixel(72, 96));
EXPECT_EQ(48, point_to_pixel(36, 96));
EXPECT_EQ(-48, point_to_pixel(-36, 96));
}
TEST(UnitsUtils, extent_to_pixel) {
EXPECT_EQ(100, extent_to_pixel_nonnegative({extent_type::PIXEL, 100}, 0));
EXPECT_EQ(48, extent_to_pixel_nonnegative({extent_type::POINT, 36}, 96));
EXPECT_EQ(0, extent_to_pixel_nonnegative({extent_type::PIXEL, -100}, 0));
EXPECT_EQ(0, extent_to_pixel_nonnegative({extent_type::POINT, -36}, 96));
}
TEST(UnitsUtils, percentage_with_offset_to_pixel) {
EXPECT_EQ(1100, percentage_with_offset_to_pixel({100, {extent_type::PIXEL, 100}}, 1000, 0));
EXPECT_EQ(1048, percentage_with_offset_to_pixel({100, {extent_type::POINT, 36}}, 1000, 96));
EXPECT_EQ(900, percentage_with_offset_to_pixel({100, {extent_type::PIXEL, -100}}, 1000, 0));
EXPECT_EQ(952, percentage_with_offset_to_pixel({100, {extent_type::POINT, -36}}, 1000, 96));
EXPECT_EQ(0, percentage_with_offset_to_pixel({0, {extent_type::PIXEL, -100}}, 1000, 0));
EXPECT_EQ(100, percentage_with_offset_to_pixel({0, {extent_type::PIXEL, 100}}, 1000, 0));
}
TEST(UnitsUtils, parse_extent_unit) {
EXPECT_EQ(extent_type::PIXEL, parse_extent_unit("px"));
EXPECT_EQ(extent_type::POINT, parse_extent_unit("pt"));
EXPECT_EQ(extent_type::PIXEL, parse_extent_unit(""));
EXPECT_THROW(parse_extent_unit("foo"), std::runtime_error);
}
TEST(UnitsUtils, parse_extent) {
EXPECT_EQ((extent_val{extent_type::PIXEL, 100}), parse_extent("100px"));
EXPECT_EQ((extent_val{extent_type::POINT, 36}), parse_extent("36pt"));
EXPECT_EQ((extent_val{extent_type::PIXEL, -100}), parse_extent("-100px"));
EXPECT_EQ((extent_val{extent_type::POINT, -36}), parse_extent("-36pt"));
EXPECT_EQ((extent_val{extent_type::PIXEL, 100}), parse_extent("100"));
EXPECT_EQ((extent_val{extent_type::PIXEL, -100}), parse_extent("-100"));
EXPECT_THROW(parse_extent("100foo"), std::runtime_error);
}
TEST(UnitsUtils, extent_to_string) {
EXPECT_EQ("100px", extent_to_string({extent_type::PIXEL, 100}));
EXPECT_EQ("36pt", extent_to_string({extent_type::POINT, 36}));
EXPECT_EQ("-100px", extent_to_string({extent_type::PIXEL, -100}));
EXPECT_EQ("-36pt", extent_to_string({extent_type::POINT, -36}));
}
TEST(UnitsUtils, parse_spacing_unit) {
EXPECT_EQ(spacing_type::PIXEL, parse_spacing_unit("px"));
EXPECT_EQ(spacing_type::POINT, parse_spacing_unit("pt"));
EXPECT_EQ(spacing_type::SPACE, parse_spacing_unit(""));
EXPECT_THROW(parse_spacing_unit("foo"), std::runtime_error);
}
TEST(UnitsUtils, parse_spacing) {
EXPECT_EQ((spacing_val{spacing_type::PIXEL, 100}), parse_spacing("100px"));
EXPECT_EQ((spacing_val{spacing_type::POINT, 36}), parse_spacing("36pt"));
EXPECT_EQ((spacing_val{spacing_type::SPACE, 100}), parse_spacing("100"));
EXPECT_THROW(parse_spacing("-100px"), std::runtime_error);
EXPECT_THROW(parse_spacing("-36pt"), std::runtime_error);
EXPECT_THROW(parse_spacing("-100"), std::runtime_error);
EXPECT_THROW(parse_spacing("100foo"), std::runtime_error);
}