mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-01 06:11:16 +00:00
Add Color::brighter/darker functions
These are exposed in .60 as well as in Rust and C++ and implemented by converting to HSV color space and adjusting the brightness (value).
This commit is contained in:
parent
a42c3a0b01
commit
391d0152f0
13 changed files with 271 additions and 3 deletions
|
@ -43,6 +43,7 @@ All notable changes to this project will be documented in this file.
|
||||||
- `return` statements
|
- `return` statements
|
||||||
- `Text` word wrap and elide
|
- `Text` word wrap and elide
|
||||||
- `drop-shadow-*` properties (limited to `Rectangle` at the moment)
|
- `drop-shadow-*` properties (limited to `Rectangle` at the moment)
|
||||||
|
- `Color.brighter` / `Color.darker`
|
||||||
|
|
||||||
|
|
||||||
## [0.0.4] - 2020-12-14
|
## [0.0.4] - 2020-12-14
|
||||||
|
|
|
@ -133,6 +133,9 @@ public:
|
||||||
/// Returns the alpha channel of the color as u8 in the range 0..255.
|
/// Returns the alpha channel of the color as u8 in the range 0..255.
|
||||||
uint8_t alpha() const { return inner.alpha; }
|
uint8_t alpha() const { return inner.alpha; }
|
||||||
|
|
||||||
|
inline Color brighter(float factor) const;
|
||||||
|
inline Color darker(float factor) const;
|
||||||
|
|
||||||
/// Returns true if \a lhs has the same values for the individual color channels as \rhs; false
|
/// Returns true if \a lhs has the same values for the individual color channels as \rhs; false
|
||||||
/// otherwise.
|
/// otherwise.
|
||||||
friend bool operator==(const Color &lhs, const Color &rhs)
|
friend bool operator==(const Color &lhs, const Color &rhs)
|
||||||
|
@ -156,7 +159,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: we need this to create GradientStop
|
// FIXME: we need this to create GradientStop
|
||||||
operator const cbindgen_private::types::Color&() { return inner; }
|
operator const cbindgen_private::types::Color &() { return inner; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
cbindgen_private::types::Color inner;
|
cbindgen_private::types::Color inner;
|
||||||
|
@ -164,6 +167,20 @@ private:
|
||||||
friend class Brush;
|
friend class Brush;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inline Color Color::brighter(float factor) const
|
||||||
|
{
|
||||||
|
Color result;
|
||||||
|
cbindgen_private::types::sixtyfps_color_brighter(&inner, factor, &result.inner);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Color Color::darker(float factor) const
|
||||||
|
{
|
||||||
|
Color result;
|
||||||
|
cbindgen_private::types::sixtyfps_color_darker(&inner, factor, &result.inner);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
RgbaColor<uint8_t>::RgbaColor(const Color &color)
|
RgbaColor<uint8_t>::RgbaColor(const Color &color)
|
||||||
{
|
{
|
||||||
|
|
|
@ -437,6 +437,21 @@ In addition to plain colors, many elements have properties that are of type `bru
|
||||||
A brush is a type that can be either a color or gradient. The brush is then used to fill an element or
|
A brush is a type that can be either a color or gradient. The brush is then used to fill an element or
|
||||||
draw the outline.
|
draw the outline.
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
All colors have methods that can be called on them:
|
||||||
|
|
||||||
|
* **`brighter(factor: float) -> Color`**
|
||||||
|
|
||||||
|
Returns a new color that is derived from this color but has its brightness increased by the specified factor.
|
||||||
|
For example if the factor is 0.5 (or for example 50%) the returned color is 50% brighter. Negative factors
|
||||||
|
decrease the brightness.
|
||||||
|
|
||||||
|
* **`darker(factor: float) -> Color`**
|
||||||
|
|
||||||
|
Returns a new color that is derived from this color but has its brightness decreased by the specified factor.
|
||||||
|
For example if the factor is .5 (or for example 50%) the returned color is 50% darker. Negative factors
|
||||||
|
increase the brightness.
|
||||||
#### Gradients
|
#### Gradients
|
||||||
|
|
||||||
Gradients allow creating smooth colorful surfaces. They are specified using an angle and a series of
|
Gradients allow creating smooth colorful surfaces. They are specified using an angle and a series of
|
||||||
|
|
|
@ -86,6 +86,8 @@ pub enum BuiltinFunction {
|
||||||
StringToFloat,
|
StringToFloat,
|
||||||
/// the "42".is_float()
|
/// the "42".is_float()
|
||||||
StringIsFloat,
|
StringIsFloat,
|
||||||
|
ColorBrighter,
|
||||||
|
ColorDarker,
|
||||||
ImplicitItemSize,
|
ImplicitItemSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +151,14 @@ impl BuiltinFunction {
|
||||||
}),
|
}),
|
||||||
args: vec![Type::ElementReference],
|
args: vec![Type::ElementReference],
|
||||||
},
|
},
|
||||||
|
BuiltinFunction::ColorBrighter => Type::Function {
|
||||||
|
return_type: Box::new(Type::Color),
|
||||||
|
args: vec![Type::Color, Type::Float32],
|
||||||
|
},
|
||||||
|
BuiltinFunction::ColorDarker => Type::Function {
|
||||||
|
return_type: Box::new(Type::Color),
|
||||||
|
args: vec![Type::Color, Type::Float32],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1434,6 +1434,12 @@ fn compile_expression(
|
||||||
BuiltinFunction::ImplicitItemSize => {
|
BuiltinFunction::ImplicitItemSize => {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
|
BuiltinFunction::ColorBrighter => {
|
||||||
|
"[](const auto &color, float factor) {{ return color.brighter(factor); }}".into()
|
||||||
|
}
|
||||||
|
BuiltinFunction::ColorDarker => {
|
||||||
|
"[](const auto &color, float factor) {{ return color.darker(factor); }}".into()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"),
|
Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"),
|
||||||
Expression::MemberFunction { .. } => panic!("member function expressions must not appear in the code generator anymore"),
|
Expression::MemberFunction { .. } => panic!("member function expressions must not appear in the code generator anymore"),
|
||||||
|
|
|
@ -1137,6 +1137,12 @@ fn compile_expression(expr: &Expression, component: &Rc<Component>) -> TokenStre
|
||||||
BuiltinFunction::StringIsFloat => {
|
BuiltinFunction::StringIsFloat => {
|
||||||
quote!((|x: SharedString| { <f64 as ::core::str::FromStr>::from_str(x.as_str()).is_ok() } ))
|
quote!((|x: SharedString| { <f64 as ::core::str::FromStr>::from_str(x.as_str()).is_ok() } ))
|
||||||
}
|
}
|
||||||
|
BuiltinFunction::ColorBrighter => {
|
||||||
|
quote!((|x: Color, factor| -> Color { x.brighter(factor as f32) }))
|
||||||
|
}
|
||||||
|
BuiltinFunction::ColorDarker => {
|
||||||
|
quote!((|x: Color, factor| -> Color { x.darker(factor as f32) }))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"),
|
Expression::ElementReference(_) => todo!("Element references are only supported in the context of built-in function calls at the moment"),
|
||||||
Expression::MemberFunction{ .. } => panic!("member function expressions must not appear in the code generator anymore"),
|
Expression::MemberFunction{ .. } => panic!("member function expressions must not appear in the code generator anymore"),
|
||||||
|
|
|
@ -1304,6 +1304,24 @@ fn maybe_lookup_object(
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Type::Color => {
|
||||||
|
return Expression::MemberFunction {
|
||||||
|
base: Box::new(base),
|
||||||
|
base_node: next.clone().into(), // Note that this is not the base_node, but the function's node
|
||||||
|
member: Box::new(match next_str.as_str() {
|
||||||
|
"lighter" => {
|
||||||
|
Expression::BuiltinFunctionReference(BuiltinFunction::ColorBrighter)
|
||||||
|
}
|
||||||
|
"darker" => {
|
||||||
|
Expression::BuiltinFunctionReference(BuiltinFunction::ColorDarker)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ctx.diag.push_error("Cannot access fields of color".into(), &next);
|
||||||
|
return Expression::Invalid;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
ctx.diag.push_error("Cannot access fields of property".into(), &next);
|
ctx.diag.push_error("Cannot access fields of property".into(), &next);
|
||||||
return Expression::Invalid;
|
return Expression::Invalid;
|
||||||
|
|
|
@ -33,7 +33,7 @@ pub type Size = euclid::default::Size2D<f32>;
|
||||||
/// 2D Transform
|
/// 2D Transform
|
||||||
pub type Transform = euclid::default::Transform2D<f32>;
|
pub type Transform = euclid::default::Transform2D<f32>;
|
||||||
|
|
||||||
mod color;
|
pub(crate) mod color;
|
||||||
pub use color::*;
|
pub use color::*;
|
||||||
|
|
||||||
mod path;
|
mod path;
|
||||||
|
|
|
@ -166,6 +166,33 @@ impl Color {
|
||||||
pub fn alpha(self) -> u8 {
|
pub fn alpha(self) -> u8 {
|
||||||
self.alpha
|
self.alpha
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a new version of this color that has the brightness increased
|
||||||
|
/// by the specified factor. This is done by converting the color to the HSV
|
||||||
|
/// color space and multiplying the brightness (value) with (1 + factor).
|
||||||
|
/// The result is converted back to RGB and the alpha channel is unchanged.
|
||||||
|
/// So for example `brighter(0.2)` will increase the brightness by 20%, and
|
||||||
|
/// calling `brighter(-0.5)` will return a color that's 50% darker.
|
||||||
|
pub fn brighter(&self, factor: f32) -> Self {
|
||||||
|
let rgbaf: RgbaColor<f32> = self.clone().into();
|
||||||
|
let mut hsva: HsvaColor = rgbaf.into();
|
||||||
|
hsva.v *= 1. + factor;
|
||||||
|
let rgbaf: RgbaColor<f32> = hsva.into();
|
||||||
|
rgbaf.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new version of this color that has the brightness decreased
|
||||||
|
/// by the specified factor. This is done by converting the color to the HSV
|
||||||
|
/// color space and dividing the brightness (value) by (1 + factor). The
|
||||||
|
/// result is converted back to RGB and the alpha channel is unchanged.
|
||||||
|
/// So for example `darker(0.3)` will decrease the brightness by 30%.
|
||||||
|
pub fn darker(&self, factor: f32) -> Self {
|
||||||
|
let rgbaf: RgbaColor<f32> = self.clone().into();
|
||||||
|
let mut hsva: HsvaColor = rgbaf.into();
|
||||||
|
hsva.v /= 1. + factor;
|
||||||
|
let rgbaf: RgbaColor<f32> = hsva.into();
|
||||||
|
rgbaf.into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InterpolatedPropertyValue for Color {
|
impl InterpolatedPropertyValue for Color {
|
||||||
|
@ -198,3 +225,109 @@ impl From<Color> for femtovg::Color {
|
||||||
Self::rgba(col.red, col.green, col.blue, col.alpha)
|
Self::rgba(col.red, col.green, col.blue, col.alpha)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
struct HsvaColor {
|
||||||
|
h: f32,
|
||||||
|
s: f32,
|
||||||
|
v: f32,
|
||||||
|
alpha: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RgbaColor<f32>> for HsvaColor {
|
||||||
|
fn from(col: RgbaColor<f32>) -> Self {
|
||||||
|
// RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
|
||||||
|
|
||||||
|
let red = col.red;
|
||||||
|
let green = col.green;
|
||||||
|
let blue = col.blue;
|
||||||
|
|
||||||
|
let min = red.min(green).min(blue);
|
||||||
|
let max = red.max(green).max(blue);
|
||||||
|
let chroma = max - min;
|
||||||
|
|
||||||
|
let hue = 60.
|
||||||
|
* if chroma == 0. {
|
||||||
|
0.0
|
||||||
|
} else if max == red {
|
||||||
|
((green - blue) / chroma) % 6.0
|
||||||
|
} else if max == green {
|
||||||
|
2. + (blue - red) / chroma
|
||||||
|
} else {
|
||||||
|
4. + (red - green) / chroma
|
||||||
|
};
|
||||||
|
|
||||||
|
let saturation = if max == 0. { 0. } else { chroma / max };
|
||||||
|
|
||||||
|
Self { h: hue, s: saturation, v: max, alpha: col.alpha }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HsvaColor> for RgbaColor<f32> {
|
||||||
|
fn from(col: HsvaColor) -> Self {
|
||||||
|
// RGB to HSL conversion from https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae
|
||||||
|
|
||||||
|
let chroma = col.s * col.v;
|
||||||
|
|
||||||
|
let x = chroma * (1. - ((col.h / 60.) % 2. - 1.).abs());
|
||||||
|
|
||||||
|
let (red, green, blue) = match (col.h / 60.0) as usize {
|
||||||
|
0 => (chroma, x, 0.),
|
||||||
|
1 => (x, chroma, 0.),
|
||||||
|
2 => (0., chroma, x),
|
||||||
|
3 => (0., x, chroma),
|
||||||
|
4 => (x, 0., chroma),
|
||||||
|
5 => (chroma, 0., x),
|
||||||
|
_ => (0., 0., 0.),
|
||||||
|
};
|
||||||
|
|
||||||
|
let m = col.v - chroma;
|
||||||
|
|
||||||
|
Self { red: red + m, green: green + m, blue: blue + m, alpha: col.alpha }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rgb_to_hsv() {
|
||||||
|
// White
|
||||||
|
assert_eq!(
|
||||||
|
HsvaColor::from(RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.5 }),
|
||||||
|
HsvaColor { h: 0., s: 0., v: 1., alpha: 0.5 }
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
RgbaColor::<f32>::from(HsvaColor { h: 0., s: 0., v: 1., alpha: 0.3 }),
|
||||||
|
RgbaColor::<f32> { red: 1., green: 1., blue: 1., alpha: 0.3 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bright greenish, verified via colorizer.org
|
||||||
|
assert_eq!(
|
||||||
|
HsvaColor::from(RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }),
|
||||||
|
HsvaColor { h: 120., s: 1., v: 0.9, alpha: 1.0 }
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
RgbaColor::<f32>::from(HsvaColor { h: 120., s: 1., v: 0.9, alpha: 1.0 }),
|
||||||
|
RgbaColor::<f32> { red: 0., green: 0.9, blue: 0., alpha: 1.0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_brighter_darker() {
|
||||||
|
let blue = Color::from_rgb_u8(0, 0, 128);
|
||||||
|
assert_eq!(blue.brighter(0.5), Color::from_rgb_u8(0, 0, 192));
|
||||||
|
assert_eq!(blue.darker(0.5), Color::from_rgb_u8(0, 0, 85));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) mod ffi {
|
||||||
|
#![allow(unsafe_code)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn sixtyfps_color_brighter(col: &Color, factor: f32, out: *mut Color) {
|
||||||
|
core::ptr::write(out, col.brighter(factor))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn sixtyfps_color_darker(col: &Color, factor: f32, out: *mut Color) {
|
||||||
|
core::ptr::write(out, col.darker(factor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -86,4 +86,5 @@ pub fn use_modules() -> usize {
|
||||||
+ window::ffi::sixtyfps_component_window_drop as usize
|
+ window::ffi::sixtyfps_component_window_drop as usize
|
||||||
+ component::ffi::sixtyfps_component_init_items as usize
|
+ component::ffi::sixtyfps_component_init_items as usize
|
||||||
+ timers::ffi::sixtyfps_timer_start as usize
|
+ timers::ffi::sixtyfps_timer_start as usize
|
||||||
|
+ graphics::color::ffi::sixtyfps_color_brighter as usize
|
||||||
}
|
}
|
||||||
|
|
|
@ -549,6 +549,34 @@ pub fn eval_expression(e: &Expression, local_context: &mut EvalLocalContext) ->
|
||||||
panic!("Argument not a string");
|
panic!("Argument not a string");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Expression::BuiltinFunctionReference(BuiltinFunction::ColorBrighter) => {
|
||||||
|
if arguments.len() != 2 {
|
||||||
|
panic!("internal error: incorrect argument count to ColorBrighter")
|
||||||
|
}
|
||||||
|
if let Value::Color(col) = eval_expression(&arguments[0], local_context) {
|
||||||
|
if let Value::Number(factor) = eval_expression(&arguments[1], local_context) {
|
||||||
|
Value::Color(col.brighter(factor as _))
|
||||||
|
} else {
|
||||||
|
panic!("Second argument not a number");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("First argument not a color");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expression::BuiltinFunctionReference(BuiltinFunction::ColorDarker) => {
|
||||||
|
if arguments.len() != 2 {
|
||||||
|
panic!("internal error: incorrect argument count to ColorDarker")
|
||||||
|
}
|
||||||
|
if let Value::Color(col) = eval_expression(&arguments[0], local_context) {
|
||||||
|
if let Value::Number(factor) = eval_expression(&arguments[1], local_context) {
|
||||||
|
Value::Color(col.darker(factor as _))
|
||||||
|
} else {
|
||||||
|
panic!("Second argument not a number");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("First argument not a color");
|
||||||
|
}
|
||||||
|
}
|
||||||
Expression::BuiltinFunctionReference(BuiltinFunction::ImplicitItemSize) => {
|
Expression::BuiltinFunctionReference(BuiltinFunction::ImplicitItemSize) => {
|
||||||
if arguments.len() != 1 {
|
if arguments.len() != 1 {
|
||||||
panic!("internal error: incorrect argument count to ImplicitItemSize")
|
panic!("internal error: incorrect argument count to ImplicitItemSize")
|
||||||
|
|
25
tests/cases/examples/color.60
Normal file
25
tests/cases/examples/color.60
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/* LICENSE BEGIN
|
||||||
|
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
||||||
|
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
||||||
|
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
This file is also available under commercial licensing terms.
|
||||||
|
Please contact info@sixtyfps.io for more information.
|
||||||
|
LICENSE END */
|
||||||
|
|
||||||
|
Win := Window {
|
||||||
|
property <color> base: #00007F;
|
||||||
|
GridLayout {
|
||||||
|
r := Rectangle {
|
||||||
|
background: base;
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
background: base.lighter(50%);
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
background: base.darker(50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -91,6 +91,8 @@ fn gen_corelib(root_dir: &Path, include_dir: &Path) -> anyhow::Result<()> {
|
||||||
"ComponentWindow",
|
"ComponentWindow",
|
||||||
"VoidArg",
|
"VoidArg",
|
||||||
"KeyEventArg",
|
"KeyEventArg",
|
||||||
|
"sixtyfps_color_brighter",
|
||||||
|
"sixtyfps_color_darker",
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x.to_string())
|
.map(|x| x.to_string())
|
||||||
|
@ -138,7 +140,11 @@ fn gen_corelib(root_dir: &Path, include_dir: &Path) -> anyhow::Result<()> {
|
||||||
|
|
||||||
for (rust_types, extra_excluded_types, internal_header) in [
|
for (rust_types, extra_excluded_types, internal_header) in [
|
||||||
(vec!["Resource"], vec![], "sixtyfps_resource_internal.h"),
|
(vec!["Resource"], vec![], "sixtyfps_resource_internal.h"),
|
||||||
(vec!["Color"], vec![], "sixtyfps_color_internal.h"),
|
(
|
||||||
|
vec!["Color", "sixtyfps_color_brighter", "sixtyfps_color_darker"],
|
||||||
|
vec![],
|
||||||
|
"sixtyfps_color_internal.h",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
vec![
|
vec![
|
||||||
"PathData",
|
"PathData",
|
||||||
|
@ -173,6 +179,8 @@ fn gen_corelib(root_dir: &Path, include_dir: &Path) -> anyhow::Result<()> {
|
||||||
"sixtyfps_component_window_show_popup",
|
"sixtyfps_component_window_show_popup",
|
||||||
"sixtyfps_new_path_elements",
|
"sixtyfps_new_path_elements",
|
||||||
"sixtyfps_new_path_events",
|
"sixtyfps_new_path_events",
|
||||||
|
"sixtyfps_color_brighter",
|
||||||
|
"sixtyfps_color_darker",
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|exclusion| rust_types.iter().find(|inclusion| inclusion == exclusion).is_none())
|
.filter(|exclusion| rust_types.iter().find(|inclusion| inclusion == exclusion).is_none())
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue