| 54 |
54 |
|
//! Unlike spacing, type does not move with [`Density`]. The reason is in
|
| 55 |
55 |
|
//! [`Text`], and it is the same one that keeps shells out of the touch preset.
|
| 56 |
56 |
|
//!
|
|
57 |
+ |
//! # Corners, same move again
|
|
58 |
+ |
//!
|
|
59 |
+ |
//! [`Radius`] names what the corner belongs to. The scale is deliberately the
|
|
60 |
+ |
//! shortest of the three, because rounding carries one bit of meaning —
|
|
61 |
+ |
//! whether the thing is meant to be pressed — and a long radius scale is one
|
|
62 |
+ |
//! nobody can choose from. `Square` is a rung rather than the absence of one,
|
|
63 |
+ |
//! so a container can state that it is square and a reader can tell that from
|
|
64 |
+ |
//! a rule nobody wrote.
|
|
65 |
+ |
//!
|
| 57 |
66 |
|
//! # Density presets
|
| 58 |
67 |
|
//!
|
| 59 |
68 |
|
//! Naming relationships instead of sizes is what makes a density preset
|
| 770 |
779 |
|
}
|
| 771 |
780 |
|
}
|
| 772 |
781 |
|
|
|
782 |
+ |
/// How rounded a corner is, named for what the corner belongs to.
|
|
783 |
+ |
///
|
|
784 |
+ |
/// The third axis to make the same move as [`Gap`] and [`Text`]: name the
|
|
785 |
+ |
/// thing and let the value follow. Whether a corner should be 3px or 4px is
|
|
786 |
+ |
/// unanswerable in isolation, and answering it once per component is how a
|
|
787 |
+ |
/// stylesheet ends up with 2, 3, 4, 6, 8 and 12 all meaning "slightly
|
|
788 |
+ |
/// rounded".
|
|
789 |
+ |
///
|
|
790 |
+ |
/// # Rounding is an affordance
|
|
791 |
+ |
///
|
|
792 |
+ |
/// The scale is deliberately short, because a corner radius carries one bit
|
|
793 |
+ |
/// of meaning: whether the thing is meant to be pressed. [`Self::Square`]
|
|
794 |
+ |
/// exists as a named rung rather than as the absence of a radius so that a
|
|
795 |
+ |
/// container states that it is square, and a reader can tell a deliberate
|
|
796 |
+ |
/// zero from a rule nobody wrote.
|
|
797 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
798 |
+ |
pub enum Radius {
|
|
799 |
+ |
/// No rounding. Containers: cards, panels, dropdowns, page shells.
|
|
800 |
+ |
Square,
|
|
801 |
+ |
/// An eighth of the base. The tightest corner still visible: inline code,
|
|
802 |
+ |
/// small badges, status chips.
|
|
803 |
+ |
Fine,
|
|
804 |
+ |
/// A quarter of the base. Controls: buttons, inputs, selects.
|
|
805 |
+ |
Control,
|
|
806 |
+ |
/// Half the base. Surfaces that round rather than sit square: media
|
|
807 |
+ |
/// covers, callout boxes, feature cards.
|
|
808 |
+ |
Panel,
|
|
809 |
+ |
/// A circle, whatever the element's size.
|
|
810 |
+ |
Round,
|
|
811 |
+ |
}
|
|
812 |
+ |
|
|
813 |
+ |
impl Radius {
|
|
814 |
+ |
/// This corner as a fraction of the base unit.
|
|
815 |
+ |
///
|
|
816 |
+ |
/// [`Self::Round`] has none, and that is not an oversight: 50% is a
|
|
817 |
+ |
/// proportion of the element's own box rather than of the base, so it
|
|
818 |
+ |
/// does not scale with `--geometry-base` and cannot be written as a
|
|
819 |
+ |
/// [`Ratio`]. Use [`Self::css`], which spells every rung.
|
|
820 |
+ |
#[must_use]
|
|
821 |
+ |
pub const fn ratio(self) -> Option<Ratio> {
|
|
822 |
+ |
let (numerator, denominator) = match self {
|
|
823 |
+ |
Self::Square => (0, 1),
|
|
824 |
+ |
Self::Fine => (1, 8),
|
|
825 |
+ |
Self::Control => (1, 4),
|
|
826 |
+ |
Self::Panel => (1, 2),
|
|
827 |
+ |
Self::Round => return None,
|
|
828 |
+ |
};
|
|
829 |
+ |
Some(Ratio {
|
|
830 |
+ |
numerator,
|
|
831 |
+ |
denominator,
|
|
832 |
+ |
})
|
|
833 |
+ |
}
|
|
834 |
+ |
|
|
835 |
+ |
/// Size in CSS pixels at the default base, or `None` for [`Self::Round`].
|
|
836 |
+ |
#[must_use]
|
|
837 |
+ |
pub const fn px(self) -> Option<u16> {
|
|
838 |
+ |
match self.ratio() {
|
|
839 |
+ |
Some(r) => Some(r.px_at(DEFAULT_BASE_PX)),
|
|
840 |
+ |
None => None,
|
|
841 |
+ |
}
|
|
842 |
+ |
}
|
|
843 |
+ |
|
|
844 |
+ |
/// The CSS value for this rung.
|
|
845 |
+ |
///
|
|
846 |
+ |
/// `Square` emits a bare `0` rather than a `calc()` that multiplies the
|
|
847 |
+ |
/// base by nothing, and `Round` emits the percentage.
|
|
848 |
+ |
#[must_use]
|
|
849 |
+ |
pub fn css(self) -> String {
|
|
850 |
+ |
match self {
|
|
851 |
+ |
Self::Square => "0".to_owned(),
|
|
852 |
+ |
Self::Round => "50%".to_owned(),
|
|
853 |
+ |
other => other
|
|
854 |
+ |
.ratio()
|
|
855 |
+ |
.expect("every rung but Round has a ratio")
|
|
856 |
+ |
.css(),
|
|
857 |
+ |
}
|
|
858 |
+ |
}
|
|
859 |
+ |
|
|
860 |
+ |
/// The CSS custom-property name, without the leading `--`.
|
|
861 |
+ |
#[must_use]
|
|
862 |
+ |
pub const fn token(self) -> &'static str {
|
|
863 |
+ |
match self {
|
|
864 |
+ |
Self::Square => "radius-square",
|
|
865 |
+ |
Self::Fine => "radius-fine",
|
|
866 |
+ |
Self::Control => "radius-control",
|
|
867 |
+ |
Self::Panel => "radius-panel",
|
|
868 |
+ |
Self::Round => "radius-round",
|
|
869 |
+ |
}
|
|
870 |
+ |
}
|
|
871 |
+ |
|
|
872 |
+ |
/// Every rung, squarest first.
|
|
873 |
+ |
#[must_use]
|
|
874 |
+ |
pub const fn all() -> [Self; 5] {
|
|
875 |
+ |
[
|
|
876 |
+ |
Self::Square,
|
|
877 |
+ |
Self::Fine,
|
|
878 |
+ |
Self::Control,
|
|
879 |
+ |
Self::Panel,
|
|
880 |
+ |
Self::Round,
|
|
881 |
+ |
]
|
|
882 |
+ |
}
|
|
883 |
+ |
}
|
|
884 |
+ |
|
| 773 |
885 |
|
/// Emit the base unit and the raw scale as CSS declarations, no selector.
|
| 774 |
886 |
|
///
|
| 775 |
887 |
|
/// Density-invariant: the steps are the vocabulary, and only which step a
|
| 807 |
919 |
|
out
|
| 808 |
920 |
|
}
|
| 809 |
921 |
|
|
|
922 |
+ |
/// Emit the corner scale as CSS declarations, no selector.
|
|
923 |
+ |
///
|
|
924 |
+ |
/// Takes no [`Density`] for the same reason [`text_css_declarations`] does
|
|
925 |
+ |
/// not: a corner is not a tap target.
|
|
926 |
+ |
#[must_use]
|
|
927 |
+ |
pub fn radius_css_declarations() -> String {
|
|
928 |
+ |
let mut out = String::new();
|
|
929 |
+ |
out.push_str(" /* Corners. Rounding says a thing is meant to be pressed,\n");
|
|
930 |
+ |
out.push_str(" so the scale is short on purpose and square is a rung\n");
|
|
931 |
+ |
out.push_str(" rather than the absence of one. */\n");
|
|
932 |
+ |
for radius in Radius::all() {
|
|
933 |
+ |
let _ = writeln!(out, " --{}: {};", radius.token(), radius.css());
|
|
934 |
+ |
}
|
|
935 |
+ |
out
|
|
936 |
+ |
}
|
|
937 |
+ |
|
| 810 |
938 |
|
/// Emit the relational layer for one density as CSS declarations, no selector.
|
| 811 |
939 |
|
///
|
| 812 |
940 |
|
/// Gaps reference their step rather than repeating a value, so the scale has
|
| 885 |
1013 |
|
#[must_use]
|
| 886 |
1014 |
|
pub fn geometry_css_vars(density: Density) -> String {
|
| 887 |
1015 |
|
format!(
|
| 888 |
|
- |
":root {{\n{}\n{}\n{}}}\n",
|
|
1016 |
+ |
":root {{\n{}\n{}\n{}\n{}}}\n",
|
| 889 |
1017 |
|
scale_css_declarations(),
|
| 890 |
1018 |
|
gap_css_declarations(density),
|
| 891 |
|
- |
text_css_declarations()
|
|
1019 |
+ |
text_css_declarations(),
|
|
1020 |
+ |
radius_css_declarations()
|
| 892 |
1021 |
|
)
|
| 893 |
1022 |
|
}
|
| 894 |
1023 |
|
|
| 1003 |
1132 |
|
assert!(Text::all().iter().all(|t| t.px() >= 12));
|
| 1004 |
1133 |
|
}
|
| 1005 |
1134 |
|
|
|
1135 |
+ |
#[test]
|
|
1136 |
+ |
fn the_corner_scale_is_short_and_ordered() {
|
|
1137 |
+ |
// A radius carries one bit of meaning, whether the thing is meant to
|
|
1138 |
+ |
// be pressed, so a long scale is a scale nobody can choose from.
|
|
1139 |
+ |
let px: Vec<Option<u16>> = Radius::all().iter().map(|r| r.px()).collect();
|
|
1140 |
+ |
assert_eq!(px, vec![Some(0), Some(2), Some(4), Some(8), None]);
|
|
1141 |
+ |
}
|
|
1142 |
+ |
|
|
1143 |
+ |
#[test]
|
|
1144 |
+ |
fn square_and_round_are_spelled_not_calculated() {
|
|
1145 |
+ |
// `calc(var(--geometry-base) * 0 / 1)` is a zero nobody can read, and
|
|
1146 |
+ |
// 50% is a proportion of the element rather than of the base.
|
|
1147 |
+ |
assert_eq!(Radius::Square.css(), "0");
|
|
1148 |
+ |
assert_eq!(Radius::Round.css(), "50%");
|
|
1149 |
+ |
assert_eq!(Radius::Round.ratio(), None);
|
|
1150 |
+ |
assert!(Radius::Control.css().contains(BASE_TOKEN));
|
|
1151 |
+ |
}
|
|
1152 |
+ |
|
| 1006 |
1153 |
|
#[test]
|
| 1007 |
1154 |
|
fn type_does_not_move_with_density() {
|
| 1008 |
1155 |
|
// Density is a claim about the contact patch, and text is not a
|
| 1314 |
1461 |
|
let mut names: Vec<&str> = Step::all().iter().map(|s| s.token()).collect();
|
| 1315 |
1462 |
|
names.extend(Gap::all().iter().map(|g| g.token()));
|
| 1316 |
1463 |
|
names.extend(Text::all().iter().map(|t| t.token()));
|
|
1464 |
+ |
names.extend(Radius::all().iter().map(|r| r.token()));
|
| 1317 |
1465 |
|
let count = names.len();
|
| 1318 |
1466 |
|
names.sort_unstable();
|
| 1319 |
1467 |
|
names.dedup();
|