diff --git a/CHANGES.md b/CHANGES.md index ad4e9800067..74b45bd1638 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Changes ### Unreleased +* 2024-07-07 - Upgrade: Adopt changes for coloring the activity icons, moving from background-colors to CSS filters, resolves #631. * 2024-07-04 - Upgrade: Fix Behat tests which broke due to the introduction of section pages in Moodle core. * 2024-07-04 - Upgrade: Adopt changes in boostnavbar.php from Boost core. * 2024-07-04 - Upgrade: Fix Behat tests which broke due to changes in the section naming in Moodle core. diff --git a/classes/lib/hextocssfilter/color.php b/classes/lib/hextocssfilter/color.php new file mode 100644 index 00000000000..a4f1045f3b2 --- /dev/null +++ b/classes/lib/hextocssfilter/color.php @@ -0,0 +1,192 @@ +. + +/** + * Theme Boost Union - Hex to CSS Filter. + * + * @package theme_boost_union + * @copyright 2024 Alexander Bias + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union\lib\hextocssfilter; + +// This code is obtained from a third party source. Let's ignore the missing PHPDoc comments. +// phpcs:disable moodle.Commenting.VariableComment.Missing +// phpcs:disable moodle.Commenting.MissingDocblock.Missing +// phpcs:disable moodle.Commenting.MissingDocblock.Function + +/** + * Class which represents a color to be transformed to CSS filters. + * + * @package theme_boost_union + * @copyright 2024 Alexander Bias + * based on code on https://wiki.cgx.me/code/php/colorsolver + * based on code on https://stackoverflow.com/questions/42966641/ + * how-to-transform-black-into-any-given-color-using-only-css-filters/43960991#43960991 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class color { + public float $r; + public float $g; + public float $b; + + public function __construct($r, $g, $b) { + $this->r = $r; + $this->g = $g; + $this->b = $b; + } + + private function clamp($value) { + if ($value > 255) { + $value = 255; + } else if ($value < 0) { + $value = 0; + } + return $value; + } + + private function multiply($matrix) { + $newr = $this->clamp($this->r * $matrix[0] + $this->g * $matrix[1] + $this->b * $matrix[2]); + $newg = $this->clamp($this->r * $matrix[3] + $this->g * $matrix[4] + $this->b * $matrix[5]); + $newb = $this->clamp($this->r * $matrix[6] + $this->g * $matrix[7] + $this->b * $matrix[8]); + + $this->r = $newr; + $this->g = $newg; + $this->b = $newb; + } + + public function huerotate($angle = 0) { + $angle = $angle / 180 * M_PI; + $angsin = sin($angle); + $angcos = cos($angle); + + $this->multiply([0.213 + $angcos * 0.787 - $angsin * 0.213, + 0.715 - $angcos * 0.715 - $angsin * 0.715, + 0.072 - $angcos * 0.072 + $angsin * 0.928, + 0.213 - $angcos * 0.213 + $angsin * 0.143, + 0.715 + $angcos * 0.285 + $angsin * 0.140, + 0.072 - $angcos * 0.072 - $angsin * 0.283, + 0.213 - $angcos * 0.213 - $angsin * 0.787, + 0.715 - $angcos * 0.715 + $angsin * 0.715, + 0.072 + $angcos * 0.928 + $angsin * 0.072, + ]); + } + + private function grayscale($value = 1) { + $this->multiply([0.2126 + 0.7874 * (1 - $value), + 0.7152 - 0.7152 * (1 - $value), + 0.0722 - 0.0722 * (1 - $value), + 0.2126 - 0.2126 * (1 - $value), + 0.7152 + 0.2848 * (1 - $value), + 0.0722 - 0.0722 * (1 - $value), + 0.2126 - 0.2126 * (1 - $value), + 0.7152 - 0.7152 * (1 - $value), + 0.0722 + 0.9278 * (1 - $value), + ]); + } + + public function sepia($value = 1) { + $this->multiply([0.393 + 0.607 * (1 - $value), + 0.769 - 0.769 * (1 - $value), + 0.189 - 0.189 * (1 - $value), + 0.349 - 0.349 * (1 - $value), + 0.686 + 0.314 * (1 - $value), + 0.168 - 0.168 * (1 - $value), + 0.272 - 0.272 * (1 - $value), + 0.534 - 0.534 * (1 - $value), + 0.131 + 0.869 * (1 - $value), + ]); + } + + public function saturate($value = 1) { + $this->multiply([0.213 + 0.787 * $value, + 0.715 - 0.715 * $value, + 0.072 - 0.072 * $value, + 0.213 - 0.213 * $value, + 0.715 + 0.285 * $value, + 0.072 - 0.072 * $value, + 0.213 - 0.213 * $value, + 0.715 - 0.715 * $value, + 0.072 + 0.928 * $value, + ]); + } + + public function brightness($value = 1) { + $this->linear($value); + } + + public function contrast($value = 1) { + $this->linear($value, -(0.5 * $value) + 0.5); + } + + private function linear($slope = 1, $intercept = 0) { + $this->r = $this->clamp($this->r * $slope + $intercept * 255); + $this->g = $this->clamp($this->g * $slope + $intercept * 255); + $this->b = $this->clamp($this->b * $slope + $intercept * 255); + } + + public function invert($value = 1) { + $this->r = $this->clamp(($value + $this->r / 255 * (1 - 2 * $value)) * 255); + $this->g = $this->clamp(($value + $this->g / 255 * (1 - 2 * $value)) * 255); + $this->b = $this->clamp(($value + $this->b / 255 * (1 - 2 * $value)) * 255); + } + + public static function hextorgb($hex) { + $r = hexdec(substr($hex, 1, 2)); + $g = hexdec(substr($hex, 3, 2)); + $b = hexdec(substr($hex, 5, 2)); + + return [$r, $g, $b]; + } + + public static function hsl($r, $g, $b) { + $r = $r / 255; + $g = $g / 255; + $b = $b / 255; + + $max = max($r, $g, $b); + $min = min($r, $g, $b); + + $h = ($max + $min) / 2; + $s = ($max + $min) / 2; + $l = ($max + $min) / 2; + + if ($max == $min) { + $h = 0; + $s = 0; + } else { + $d = $max - $min; + $s = ($l > 0.5) ? $d / (2 - $max - $min) : $d / ($max + $min); + + switch ($max) { + case $r: + $h = ($g - $b) / $d + ($g < $b ? 6 : 0); + break; + case $g: + $h = ($b - $r) / $d + 2; + break; + case $b: + $h = ($r - $g) / $d + 4; + break; + } + + $h /= 6; + } + + return ["h" => $h * 100, "s" => $s * 100, "l" => $l * 100]; + } +} diff --git a/classes/lib/hextocssfilter/solver.php b/classes/lib/hextocssfilter/solver.php new file mode 100644 index 00000000000..4814929c6c8 --- /dev/null +++ b/classes/lib/hextocssfilter/solver.php @@ -0,0 +1,266 @@ +. + +/** + * Theme Boost Union - Hex to CSS Filter. + * + * @package theme_boost_union + * @copyright 2024 Alexander Bias + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union\lib\hextocssfilter; + +// This code is obtained from a third party source. Let's ignore the missing PHPDoc comments. +// phpcs:disable moodle.Commenting.VariableComment.Missing +// phpcs:disable moodle.Commenting.MissingDocblock.Missing +// phpcs:disable moodle.Commenting.MissingDocblock.Function + +/** + * Class which is the solver to transform a color to CSS filters. + * + * @package theme_boost_union + * @copyright 2024 Alexander Bias + * based on code on https://wiki.cgx.me/code/php/colorsolver + * based on code on https://stackoverflow.com/questions/42966641/ + * how-to-transform-black-into-any-given-color-using-only-css-filters/43960991#43960991 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class solver { + private array $targetrgb; + private array $targethsl; + private color $targetcolor; + + public function __construct($rgb) { + $this->targetrgb = color::hexToRgb($rgb); + $this->targethsl = color::hsl($this->targetrgb[0], $this->targetrgb[1], $this->targetrgb[2]); + $this->targetcolor = new color($this->targetrgb[0], $this->targetrgb[1], $this->targetrgb[2]); + } + + public function loss($filters) { + $color = new color(0, 0, 0); + + $color->invert($filters[0] / 100); + $color->sepia($filters[1] / 100); + $color->saturate($filters[2] / 100); + $color->hueRotate($filters[3] * 3.6); + $color->brightness($filters[4] / 100); + $color->contrast($filters[5] / 100); + + $colorhsl = color::hsl($color->r, $color->g, $color->b); + + return ( + abs($color->r - $this->targetcolor->r) + + abs($color->g - $this->targetcolor->g) + + abs($color->b - $this->targetcolor->b) + + abs($colorhsl["h"] - $this->targethsl["h"]) + + abs($colorhsl["s"] - $this->targethsl["s"]) + + abs($colorhsl["l"] - $this->targethsl["l"]) + ); + } + + private function spsa($a, $a2, $c, $values, $iters) { + $alpha = 1; + $gamma = 0.16666666666666666; + + $best = null; + $bestloss = INF; + + $deltas = []; + $highargs = []; + $lowargs = []; + + for ($k = 0; $k < $iters; $k++) { + $ck = $c / pow($k + 1, $gamma); + + for ($i = 0; $i < 6; $i++) { + $deltas[$i] = (rand(0, 1) > 0.5) ? 1 : -1; + $highargs[$i] = $values[$i] + $ck * $deltas[$i]; + $lowargs[$i] = $values[$i] - $ck * $deltas[$i]; + } + + $lossdiff = $this->loss($highargs) - $this->loss($lowargs); + + for ($i = 0; $i < 6; $i++) { + $g = $lossdiff / (2 * $ck) * $deltas[$i]; + $ak = $a2[$i] / pow($a + $k + 1, $alpha); + $values[$i] = $this->fix($values[$i] - $ak * $g, $i); + } + + $loss = $this->loss($values); + + if ($loss < $bestloss) { + $best = $values; + $bestloss = $loss; + } + } + + return ["values" => $best, "loss" => $bestloss]; + } + + private function fix($value, $idx) { + $max = 100; + + if ($idx == 2 /* saturate */) { + $max = 7500; + } else if ($idx == 4 /* brightness */ || $idx == 5 /* contrast */) { + $max = 200; + } + + if ($idx == 3 /* hue-rotate */) { + if ($value > $max) { + // Original code which led to the 'Deprecated: Implicit conversion from float to int loses precision' debug message: + // $value %= $max. + $value = (int)$value % $max; + } else if ($value < 0) { + // Original code which led to the 'Deprecated: Implicit conversion from float to int loses precision' debug message: + // $value = $max + $value % $max. + $value = $max + (int)$value % $max; + } + } else if ($value < 0) { + $value = 0; + } else if ($value > $max) { + $value = $max; + } + + return $value; + } + + private function solvewide() { + $a = 5; + $c = 15; + $a2 = [60, 180, 18000, 600, 1.2, 1.2]; + + $best = ["loss" => INF]; + for ($i = 0; $best["loss"] > 25 && $i < 3; $i++) { + $initial = [50, 20, 3750, 50, 100, 100]; + $result = $this->spsa($a, $a2, $c, $initial, 1000); + if ($result["loss"] < $best["loss"]) { + $best = $result; + } + } + + return $best; + } + + private function solvenarrow($wide) { + $a = $wide["loss"]; + $c = 2; + $a1 = $a + 1; + $a2 = [0.25 * $a1, 0.25 * $a1, $a1, 0.25 * $a1, 0.2 * $a1, 0.2 * $a1]; + return $this->spsa($a, $a2, $c, $wide["values"], 500); + } + + private function css($filters) { + return "invert(" . round($filters[0]) . "%) sepia(" . round($filters[1]) . "%) saturate(" . + round($filters[2]) . "%) hue-rotate(" . round($filters[3] * 3.6) . "deg) brightness(" . + round($filters[4]) . "%) contrast(" . round($filters[5]) . "%)"; + } + + public function solve() { + // The original code tries to find _one_ filter set and returns it. + // Unfortunately, as the calculation is based on a random component, this filter set does not necessarily + // need to be perfect. + // In theory, the admin who sees that a color is off, could easily purge the theme cache to trigger a + // recalculation. But the more colors have been changed by the admin, the probability that at least one + // color is off with every cache purge gets higher and higher. + // Thus, we kill this with iron (as the filters are cached afterwards and we do not really have to care about time), + // calculate multiple colors and pick the one with least loss. + + // Initialize the best filter up to now. + $bestfilteruptonow = ''; + $leastlossuptonow = 9999; + + // Get the number of iterations which the admin wanted us to do. + $imax = get_config('theme_boost_union', 'activityiconcolorfidelity'); + + // Try the configured number of iterations to find a filter which fits really well (i.e. loss < 0.005). + $i = 0; + while ($leastlossuptonow > 0.005 && $i < $imax) { + // Test another color. + $i++; + $nexttrycolor = $this->solveNarrow($this->solveWide()); + + // If the loss is smaller than the one which is the best up to now. + if ($nexttrycolor["loss"] < $leastlossuptonow) { + // Remember this color as new best color. + $bestfilteruptonow = $nexttrycolor; + $leastlossuptonow = $nexttrycolor["loss"]; + } + } + + // Return the best color which we have found. + return [ + "values" => $bestfilteruptonow["values"], + "loss" => $bestfilteruptonow["loss"], + "filter" => $this->css($bestfilteruptonow["values"]), + ]; + } + + /** + * Helper function to check if the given CSS filter is close enough to the color. + * This function is not contained in the original library and was implemented just for Behat testing. + * + * @param string $cssfilters The CSS filter string. + * @param float $loss The loss which is acceptable. + * @return bool + */ + public function filter_is_close_enough($cssfilters, $loss) { + // Verify input data. + if (strlen($cssfilters) < 1 || + strpos($cssfilters, 'invert') === false || + strpos($cssfilters, 'sepia') === false || + strpos($cssfilters, 'saturate') === false || + strpos($cssfilters, 'hue-rotate') === false || + strpos($cssfilters, 'brightness') === false || + strpos($cssfilters, 'contrast') === false || + is_numeric($loss) == false) { + return false; + } + + // Split the provided filter by spaces. + $filters = explode(' ', $cssfilters); + + // Extract only the values of the filters. + foreach ($filters as &$f) { + preg_match('#\((.*?)\)#', $f, $match); + $f = $match[1]; + } + + // Remove the 'deg' suffix from the fourth filter (hue-rotate). + $filters[3] = substr($filters[3], 0, -3); + + // Divide the fourth filter (hue-rotate) by 3.6 to revert the calculation from the css() function. + $filters[3] = $filters[3] / 3.6; + + // Multiply all other filters by 100 to revert the percent calculation from the css() function. + $filters[0] *= 100; + $filters[1] *= 100; + $filters[2] *= 100; + $filters[4] *= 100; + $filters[5] *= 100; + + // Compute the loss of the given color. + $calculatedloss = $this->loss($filters); + + // Compare and return the result. + if ($calculatedloss <= $loss) { + return true; + } else { + return false; + } + } +} diff --git a/lang/en/theme_boost_union.php b/lang/en/theme_boost_union.php index b61a5576a95..03020145d28 100644 --- a/lang/en/theme_boost_union.php +++ b/lang/en/theme_boost_union.php @@ -200,6 +200,13 @@ // ... ... Setting: Activity icon color for 'Interface'. $string['activityiconcolorinterfacesetting'] = 'Activity icon color for "Interface"'; $string['activityiconcolorinterfacesetting_desc'] = 'The activity icon color for "Interface"'; +// ... ... Setting: Activity icon color fidelity'. +$string['activityiconcolorfidelitysetting'] = 'Activity icon color fidelity'; +$string['activityiconcolorfidelitysetting_desc'] = 'With the settings above, you set a hex color which will be used to tint the particular activity icon. However, technically, the activity icon is tinted with a CSS filter. Boost Union uses a sophisticated algorithm to determine a CSS filter which matches the given hex color visually, but this algorithm is based on a randomized search and might produce suboptimal results when it is run just once. With this setting, you can allow Boost Union to run the algorithm multiple times and pick the filter which deviates least from the hex color at the end. Please note that this setting has an impact on the cache purging times (the more iterations you allow, the longer Moodle will take to purge the theme cache), but it will not have an impact on page load times.'; +$string['activityiconcolorfidelity_oneshot'] = 'One shot (1 iteration)'; +$string['activityiconcolorfidelity_sometries'] = 'Some tries (up to 10 iterations)'; +$string['activityiconcolorfidelity_detailled'] = 'Detailled research (up to 100 iterations)'; +$string['activityiconcolorfidelity_insane'] = 'Insane quest (up to 500 iterations)'; // ... Section: Activity icon purposes. $string['activitypurposeheading'] = 'Activity icon purposes'; $string['activitypurposeheading_desc'] = 'With these settings, you can override the activity icon background color which is defined by the activity\'s purpose (and which is a hardcoded plugin feature in each activity).'; diff --git a/lib.php b/lib.php index 78421090b07..85dbfeb8dfc 100644 --- a/lib.php +++ b/lib.php @@ -187,12 +187,6 @@ function theme_boost_union_get_pre_scss($theme) { 'bootstrapcolorinfo' => ['info'], 'bootstrapcolorwarning' => ['warning'], 'bootstrapcolordanger' => ['danger'], - 'activityiconcoloradministration' => ['activity-icon-administration-bg'], - 'activityiconcolorassessment' => ['activity-icon-assessment-bg'], - 'activityiconcolorcollaboration' => ['activity-icon-collaboration-bg'], - 'activityiconcolorcommunication' => ['activity-icon-communication-bg'], - 'activityiconcolorcontent' => ['activity-icon-content-bg'], - 'activityiconcolorinterface' => ['activity-icon-interface-bg'], ]; // Prepend variables first. @@ -229,6 +223,31 @@ function theme_boost_union_get_pre_scss($theme) { $scss .= '$drawer-right-width: '.get_config('theme_boost_union', 'blockdrawerwidth').";\n"; } + // Set variables which are influenced by the activityiconcolor* settings. + $purposes = [MOD_PURPOSE_ADMINISTRATION, + MOD_PURPOSE_ASSESSMENT, + MOD_PURPOSE_COLLABORATION, + MOD_PURPOSE_COMMUNICATION, + MOD_PURPOSE_CONTENT, + MOD_PURPOSE_INTERFACE]; + // Iterate over all purposes. + foreach ($purposes as $purpose) { + // Get color setting. + $activityiconcolor = get_config('theme_boost_union', 'activityiconcolor'.$purpose); + + // If a color is set. + if (!empty($activityiconcolor)) { + // Set the activity-icon-*-bg variable which was replaced by the CSS filters in Moodle 4.4 but which is still part + // of the codebase. + $scss .= '$activity-icon-'.$purpose.'-bg: '.$activityiconcolor.";\n"; + + // Set the activity-icon-*-filter variable which holds the CSS filters for the activity icon colors now. + $solver = new \theme_boost_union\lib\hextocssfilter\solver($activityiconcolor); + $cssfilterresult = $solver->solve(); + $scss .= '$activity-icon-'.$purpose.'-filter: '.$cssfilterresult['filter'].";\n"; + } + } + // Set custom Boost Union SCSS variable: The block region outside left width. $blockregionoutsideleftwidth = get_config('theme_boost_union', 'blockregionoutsideleftwidth'); // If the setting is not set. diff --git a/settings.php b/settings.php index dacbf70273f..8c795acad74 100644 --- a/settings.php +++ b/settings.php @@ -548,51 +548,35 @@ $setting = new admin_setting_heading($name, $title, null); $tab->add($setting); - // Setting: Activity icon color for 'administration'. - $name = 'theme_boost_union/activityiconcoloradministration'; - $title = get_string('activityiconcoloradministrationsetting', 'theme_boost_union', null, true); - $description = get_string('activityiconcoloradministrationsetting_desc', 'theme_boost_union', null, true); - $setting = new admin_setting_configcolourpicker($name, $title, $description, ''); - $setting->set_updatedcallback('theme_reset_all_caches'); - $tab->add($setting); - - // Setting: Activity icon color for 'assessment'. - $name = 'theme_boost_union/activityiconcolorassessment'; - $title = get_string('activityiconcolorassessmentsetting', 'theme_boost_union', null, true); - $description = get_string('activityiconcolorassessmentsetting_desc', 'theme_boost_union', null, true); - $setting = new admin_setting_configcolourpicker($name, $title, $description, ''); - $setting->set_updatedcallback('theme_reset_all_caches'); - $tab->add($setting); - - // Setting: Activity icon color for 'collaboration'. - $name = 'theme_boost_union/activityiconcolorcollaboration'; - $title = get_string('activityiconcolorcollaborationsetting', 'theme_boost_union', null, true); - $description = get_string('activityiconcolorcollaborationsetting_desc', 'theme_boost_union', null, true); - $setting = new admin_setting_configcolourpicker($name, $title, $description, ''); - $setting->set_updatedcallback('theme_reset_all_caches'); - $tab->add($setting); - - // Setting: Activity icon color for 'communication'. - $name = 'theme_boost_union/activityiconcolorcommunication'; - $title = get_string('activityiconcolorcommunicationsetting', 'theme_boost_union', null, true); - $description = get_string('activityiconcolorcommunicationsetting_desc', 'theme_boost_union', null, true); - $setting = new admin_setting_configcolourpicker($name, $title, $description, ''); - $setting->set_updatedcallback('theme_reset_all_caches'); - $tab->add($setting); - - // Setting: Activity icon color for 'content'. - $name = 'theme_boost_union/activityiconcolorcontent'; - $title = get_string('activityiconcolorcontentsetting', 'theme_boost_union', null, true); - $description = get_string('activityiconcolorcontentsetting_desc', 'theme_boost_union', null, true); - $setting = new admin_setting_configcolourpicker($name, $title, $description, ''); - $setting->set_updatedcallback('theme_reset_all_caches'); - $tab->add($setting); + // Define all activity icon purposes (without the 'other' purpose as this is not branded). + $purposes = [MOD_PURPOSE_ADMINISTRATION, + MOD_PURPOSE_ASSESSMENT, + MOD_PURPOSE_COLLABORATION, + MOD_PURPOSE_COMMUNICATION, + MOD_PURPOSE_CONTENT, + MOD_PURPOSE_INTERFACE]; + // Iterate over all purposes. + foreach ($purposes as $purpose) { + // Setting: Activity icon color. + $name = 'theme_boost_union/activityiconcolor'.$purpose; + $title = get_string('activityiconcolor'.$purpose.'setting', 'theme_boost_union', null, true); + $description = get_string('activityiconcolor'.$purpose.'setting_desc', 'theme_boost_union', null, true); + $setting = new admin_setting_configcolourpicker($name, $title, $description, ''); + $setting->set_updatedcallback('theme_reset_all_caches'); + $tab->add($setting); + } - // Setting: Activity icon color for 'interface'. - $name = 'theme_boost_union/activityiconcolorinterface'; - $title = get_string('activityiconcolorinterfacesetting', 'theme_boost_union', null, true); - $description = get_string('activityiconcolorinterfacesetting_desc', 'theme_boost_union', null, true); - $setting = new admin_setting_configcolourpicker($name, $title, $description, ''); + // Setting: Activity icon color fidelity. + $name = 'theme_boost_union/activityiconcolorfidelity'; + $title = get_string('activityiconcolorfidelitysetting', 'theme_boost_union', null, true); + $description = get_string('activityiconcolorfidelitysetting_desc', 'theme_boost_union', null, true); + $activityiconcolorfidelityoptions = [ + 1 => get_string('activityiconcolorfidelity_oneshot', 'theme_boost_union'), + 10 => get_string('activityiconcolorfidelity_sometries', 'theme_boost_union'), + 100 => get_string('activityiconcolorfidelity_detailled', 'theme_boost_union'), + 500 => get_string('activityiconcolorfidelity_insane', 'theme_boost_union'), + ]; + $setting = new admin_setting_configselect($name, $title, $description, 1, $activityiconcolorfidelityoptions); $setting->set_updatedcallback('theme_reset_all_caches'); $tab->add($setting); diff --git a/tests/behat/behat_theme_boost_union_base_general.php b/tests/behat/behat_theme_boost_union_base_general.php index 798c5a1b1b8..0f338cd24d4 100644 --- a/tests/behat/behat_theme_boost_union_base_general.php +++ b/tests/behat/behat_theme_boost_union_base_general.php @@ -58,6 +58,56 @@ public function dom_element_should_have_computed_style($selector, $style, $value } } + /** + * Checks if the given DOM element does not have the given computed style. + * + * @copyright 2024 Alexander Bias + * @Then DOM element :arg1 should not have computed style :arg2 :arg3 + * @param string $selector + * @param string $style + * @param string $value + * @throws ExpectationException + */ + public function dom_element_should_not_have_computed_style($selector, $style, $value) { + $stylejs = " + return ( + window.getComputedStyle(document.querySelector('$selector')).getPropertyValue('$style') + ) + "; + $computedstyle = $this->evaluate_script($stylejs); + if ($computedstyle == $value) { + throw new ExpectationException('The \''.$selector.'\' DOM element does have the computed style \''. + $style.'\'=\''.$computedstyle.'\', but it should not have it.', $this->getSession()); + } + } + + /** + * Checks if the given DOM element has a CSS filter which is close enough to the given hex color. + * + * @copyright 2024 Alexander Bias + * @Then DOM element :arg1 should have a CSS filter close enough to hex color :arg2 + * @param string $selector + * @param string $color + * @throws ExpectationException + */ + public function dom_element_should_have_css_filter_close_to_hex($selector, $color) { + $stylejs = " + return ( + window.getComputedStyle(document.querySelector('$selector')).getPropertyValue('filter') + ) + "; + $computedfilter = $this->evaluate_script($stylejs); + + // Check if the computed filter is close enough to the given color. + $solver = new \theme_boost_union\lib\hextocssfilter\solver($color); + $closeenough = $solver->filter_is_close_enough($computedfilter, '2'); + + if ($closeenough != true) { + throw new ExpectationException('The \''.$selector.'\' DOM element with the CSS filter \''. + $computedfilter.'\', is not close enough to the color \''.$color.'\'.', $this->getSession()); + } + } + /** * Scroll the page to a given coordinate. * diff --git a/tests/behat/theme_boost_union_looksettings_activitybranding.feature b/tests/behat/theme_boost_union_looksettings_activitybranding.feature index c718c15409f..58666157f52 100644 --- a/tests/behat/theme_boost_union_looksettings_activitybranding.feature +++ b/tests/behat/theme_boost_union_looksettings_activitybranding.feature @@ -14,21 +14,25 @@ Feature: Configuring the theme_boost_union plugin for the "Activity branding" ta Given the following config values are set as admin: | config | value | plugin | | activityiconcolor | | theme_boost_union | + | activityiconcolorfidelity | 500 | theme_boost_union | And the theme cache is purged and the theme is reloaded When I log in as "admin" And I am on "Course 1" course homepage And I turn editing mode on And I click on "Add an activity or resource" "button" in the "New section" "section" - Then DOM element ".chooser-container .activityiconcontainer.modicon_" should have computed style "background-color" "" + # First, we test that the default filter is _not_ set anymore. + Then DOM element ".chooser-container .activityiconcontainer.modicon_ img" should not have computed style "filter" "" + # And then, as the hex color to CSS filter conversion results are not reproducible, we test if the applied filter is close enough to the hex color. + And DOM element ".chooser-container .activityiconcontainer.modicon_ img" should have a CSS filter close enough to hex color "" # Unfortunately, we can only test 4 out of 6 purpose types as Moodle does does not ship with any activity with the # administration and interface types. But this should be an acceptable test coverage anyway. Examples: - | purposename | modname | colorhex | colorrgb | - | assessment | assign | #FF0000 | rgb(255, 0, 0) | - | collaboration | data | #00FF00 | rgb(0, 255, 0) | - | communication | choice | #0000FF | rgb(0, 0, 255) | - | content | book | #FFFF00 | rgb(255, 255, 0) | + | purposename | modname | colorhex | originalfilter | + | assessment | assign | #FF0000 | invert(0.36) sepia(0.98) saturate(69.69) hue-rotate(315deg) brightness(0.9) contrast(1.19) | + | collaboration | data | #00FF00 | invert(0.25) sepia(0.54) saturate(62.26) hue-rotate(245deg) brightness(1) contrast(1.02) | + | communication | choice | #0000FF | invert(0.48) sepia(0.74) saturate(48.87) hue-rotate(11deg) brightness(1.02) contrast(1.01) | + | content | book | #FFFF00 | invert(0.49) sepia(0.52) saturate(46.75) hue-rotate(156deg) brightness(0.89) contrast(1.02) | @javascript Scenario Outline: Setting: Activity icon purposes - Setting the purpose