1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936
use std::{fmt, num::NonZeroU32, time::Duration};
use zng_app::render::FontSynthesis;
use zng_color::COLOR_SCHEME_VAR;
use zng_ext_font::{font_features::*, *};
use zng_ext_l10n::{Langs, LANG_VAR};
use zng_view_api::config::FontAntiAliasing;
use zng_wgt::prelude::*;
use zng_wgt_layer::AnchorOffset;
/// Basic text font properties.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// See also [`FontFeaturesMix<P>`] for the other font properties.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct FontMix<P>(P);
context_var! {
/// Font family of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_FAMILY_VAR: FontNames = FontNames::default();
/// Font size of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_SIZE_VAR: FontSize = FontSize::Pt(11.0);
/// Font weight of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_WEIGHT_VAR: FontWeight = FontWeight::NORMAL;
/// Font style of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_STYLE_VAR: FontStyle = FontStyle::Normal;
/// Font stretch of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_STRETCH_VAR: FontStretch = FontStretch::NORMAL;
/// Font synthesis of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_SYNTHESIS_VAR: FontSynthesis = FontSynthesis::ENABLED;
/// Font anti-aliasing of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_AA_VAR: FontAntiAliasing = FontAntiAliasing::Default;
}
impl FontMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&FONT_FAMILY_VAR);
set.insert(&FONT_SIZE_VAR);
set.insert(&FONT_WEIGHT_VAR);
set.insert(&FONT_STYLE_VAR);
set.insert(&FONT_STRETCH_VAR);
set.insert(&FONT_SYNTHESIS_VAR);
set.insert(&FONT_AA_VAR);
}
}
/// Font family name or list of names for texts in this widget or descendants.
///
/// All fonts in the list are resolved according to the [`font_style`], [`font_weight`] and [`font_stretch`] config.
/// During text shaping the first font on the list is preferred, but if the font does not cover a character or word, that
/// character or word to the second font in the list and so on.
///
/// Sets the [`FONT_FAMILY_VAR`].
///
/// [`font_style`]: fn@font_style
/// [`font_weight`]: fn@font_weight
/// [`font_stretch`]: fn@font_stretch
#[property(CONTEXT, default(FONT_FAMILY_VAR), widget_impl(FontMix<P>))]
pub fn font_family(child: impl UiNode, names: impl IntoVar<FontNames>) -> impl UiNode {
with_context_var(child, FONT_FAMILY_VAR, names)
}
/// Sets the font size for the widget and descendants.
///
/// This property affects all texts inside the widget and the [`Length::Em`] unit.
///
/// Sets the [`FONT_SIZE_VAR`] context var and the [`LayoutMetrics::font_size`].
///
/// [`LayoutMetrics::font_size`]: zng_wgt::prelude::LayoutMetrics::font_size
/// [`Length::Em`]: zng_wgt::prelude::Length::Em
#[property(CONTEXT, default(FONT_SIZE_VAR), widget_impl(FontMix<P>))]
pub fn font_size(child: impl UiNode, size: impl IntoVar<FontSize>) -> impl UiNode {
let child = match_node(child, |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&FONT_SIZE_VAR);
}
UiNodeOp::Measure { wm, desired_size } => {
let font_size = FONT_SIZE_VAR.get();
let font_size_px = font_size.layout_dft_x(LAYOUT.root_font_size());
*desired_size = if font_size_px >= 0 {
LAYOUT.with_font_size(font_size_px, || child.measure(wm))
} else {
tracing::error!("invalid font size {font_size:?} => {font_size_px:?}");
child.measure(wm)
};
}
UiNodeOp::Layout { wl, final_size } => {
let font_size = FONT_SIZE_VAR.get();
let font_size_px = font_size.layout_dft_x(LAYOUT.root_font_size());
*final_size = if font_size_px >= 0 {
LAYOUT.with_font_size(font_size_px, || child.layout(wl))
} else {
tracing::error!("invalid font size {font_size:?} => {font_size_px:?}");
child.layout(wl)
};
}
_ => {}
});
with_context_var(child, FONT_SIZE_VAR, size)
}
/// Defines the thickness or boldness the preferred font should have.
///
/// This value influences font resolution, the variant within the font family that is closest to this config will be selected.
///
/// Sets the [`FONT_WEIGHT_VAR`].
#[property(CONTEXT, default(FONT_WEIGHT_VAR), widget_impl(FontMix<P>))]
pub fn font_weight(child: impl UiNode, weight: impl IntoVar<FontWeight>) -> impl UiNode {
with_context_var(child, FONT_WEIGHT_VAR, weight)
}
/// Defines the skew style of the font glyphs.
///
/// This value influences font resolution, the variant within the font family that is closest to this config will be selected.
///
/// Sets the [`FONT_STYLE_VAR`].
#[property(CONTEXT, default(FONT_STYLE_VAR), widget_impl(FontMix<P>))]
pub fn font_style(child: impl UiNode, style: impl IntoVar<FontStyle>) -> impl UiNode {
with_context_var(child, FONT_STYLE_VAR, style)
}
/// Defines how condensed or expanded the preferred font should be.
///
/// This value influences font resolution, the variant within the font family that is closest to this config will be selected.
///
/// Sets the [`FONT_STRETCH_VAR`].
#[property(CONTEXT, default(FONT_STRETCH_VAR), widget_impl(FontMix<P>))]
pub fn font_stretch(child: impl UiNode, stretch: impl IntoVar<FontStretch>) -> impl UiNode {
with_context_var(child, FONT_STRETCH_VAR, stretch)
}
/// Configure if a synthetic font is generated for fonts that do not implement **bold** or *oblique* variants.
///
/// Not all fonts implement the requested [`font_weight`] and [`font_style`], this config allows the renderer
/// to try and generate the style and weight anyway, using transforms and the glyph outlines.
///
/// Sets the [`FONT_SYNTHESIS_VAR`].
///
/// [`font_weight`]: fn@font_weight
/// [`font_style`]: fn@font_style
#[property(CONTEXT, default(FONT_SYNTHESIS_VAR), widget_impl(FontMix<P>))]
pub fn font_synthesis(child: impl UiNode, enabled: impl IntoVar<FontSynthesis>) -> impl UiNode {
with_context_var(child, FONT_SYNTHESIS_VAR, enabled)
}
/// Configure the anti-aliasing used to render text glyphs inside the widget.
///
/// Uses the operating system configuration by default.
///
/// Sets the [`FONT_AA_VAR`].
#[property(CONTEXT, default(FONT_AA_VAR), widget_impl(FontMix<P>))]
pub fn font_aa(child: impl UiNode, aa: impl IntoVar<FontAntiAliasing>) -> impl UiNode {
with_context_var(child, FONT_AA_VAR, aa)
}
/// Text color properties.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct TextFillMix<P>(P);
context_var! {
/// Color of [`Text!`] glyphs that are not colored by palette.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_COLOR_VAR: Rgba = COLOR_SCHEME_VAR.map(|s| match s {
ColorScheme::Light => colors::BLACK,
ColorScheme::Dark => colors::WHITE,
});
/// Color of [`Text!`] glyphs that are colored by palette, mostly Emoji.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_PALETTE_VAR: FontColorPalette = COLOR_SCHEME_VAR.map_into();
/// Overrides of specific colors in the selected colored glyph palette.
pub static FONT_PALETTE_COLORS_VAR: Vec<(u16, Rgba)> = vec![];
}
impl TextFillMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&FONT_COLOR_VAR);
set.insert(&FONT_PALETTE_VAR);
set.insert(&FONT_PALETTE_COLORS_VAR);
}
}
/// Defines the color the most text glyphs are filled with.
///
/// Colored glyphs (Emoji) are not affected by this, you can use [`font_palette`] to modify
/// Emoji colors.
///
/// Sets the [`FONT_COLOR_VAR`].
///
/// [`font_palette`]: fn@font_palette
#[property(CONTEXT, default(FONT_COLOR_VAR), widget_impl(TextFillMix<P>))]
pub fn font_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
with_context_var(child, FONT_COLOR_VAR, color)
}
/// Defines the palette used to render colored glyphs (Emoji).
///
/// This property only affects Emoji from fonts using COLR v0. You can use [`font_color`] to set
/// the base color, and [`font_palette_colors`] to change specific colors.
///
/// Sets the [`FONT_PALETTE_VAR`].
///
/// [`font_color`]: fn@font_color
/// [`font_palette_colors`]: fn@font_palette_colors
#[property(CONTEXT, default(FONT_PALETTE_VAR), widget_impl(TextFillMix<P>))]
pub fn font_palette(child: impl UiNode, palette: impl IntoVar<FontColorPalette>) -> impl UiNode {
with_context_var(child, FONT_PALETTE_VAR, palette)
}
/// Set the palette color in the font palette colors.
///
/// The `index` is pushed or replaced on the context [`FONT_PALETTE_COLORS_VAR`].
///
/// This function is a helper for declaring properties that configure the colors of a specific font, you
/// can use [`font_palette_colors`] to set all color overrides directly.
///
/// [`font_palette_colors`]: fn@font_palette_colors
pub fn with_font_palette_color(child: impl UiNode, index: u16, color: impl IntoVar<Rgba>) -> impl UiNode {
with_context_var(
child,
FONT_PALETTE_COLORS_VAR,
merge_var!(FONT_PALETTE_COLORS_VAR, color.into_var(), move |set, color| {
let mut set = set.clone();
if let Some(i) = set.iter().position(|(i, _)| *i == index) {
set[i].1 = *color;
} else {
set.push((index, *color));
}
set
}),
)
}
/// Defines custom palette colors that affect Emoji colors.
///
/// The palette is selected by [`font_palette`] and then each valid index entry in this property replaces
/// the selected color.
///
/// Sets the [`FONT_PALETTE_COLORS_VAR`].
///
/// [`font_palette`]: fn@font_palette
#[property(CONTEXT, default(FONT_PALETTE_COLORS_VAR), widget_impl(TextFillMix<P>))]
pub fn font_palette_colors(child: impl UiNode, colors: impl IntoVar<Vec<(u16, Rgba)>>) -> impl UiNode {
with_context_var(child, FONT_PALETTE_COLORS_VAR, colors)
}
/// Text align, justify.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct TextAlignMix<P>(P);
context_var! {
/// Text alignment inside the available space.
pub static TEXT_ALIGN_VAR: Align = Align::START;
/// Text alignment inside the available space when it overflows.
pub static TEXT_OVERFLOW_ALIGN_VAR: Align = Align::TOP_START;
/// Text justify mode when text align is fill.
pub static JUSTIFY_VAR: Option<Justify> = None;
}
impl TextAlignMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&TEXT_ALIGN_VAR);
set.insert(&TEXT_OVERFLOW_ALIGN_VAR);
set.insert(&JUSTIFY_VAR);
}
}
/// Alignment of text inside available space.
///
/// Horizontal alignment is applied for each line independently, vertical alignment is applied for the entire
/// text block together.
///
/// Note that the [`Text!`] widget only implements this for text inside each instance in isolation, multiple
/// text instances in an inline row will not all align together by the [`Text!`] layout implementation alone.
///
/// Sets the [`TEXT_ALIGN_VAR`].
///
/// See also [`txt_overflow_align`], used when the text overflows.
///
/// [`Text!`]: struct@crate::Text
/// [`txt_overflow_align`]: fn@txt_overflow_align
#[property(CONTEXT, default(TEXT_ALIGN_VAR), widget_impl(TextAlignMix<P>))]
pub fn txt_align(child: impl UiNode, mode: impl IntoVar<Align>) -> impl UiNode {
with_context_var(child, TEXT_ALIGN_VAR, mode)
}
/// Alignment of text inside available space when the text overflows.
///
/// Note that the [`Text!`] widget only implements this for text inside each instance in isolation, multiple
/// text instances in an inline row will not all align together. Also note that [`txt_overflow`] truncation
/// only applies to the end of the text after it is aligned, so unless this is [`Align::TOP_START`] (default) the
/// start of the text maybe still be clipped after truncation.
///
/// Sets the [`TEXT_OVERFLOW_ALIGN_VAR`].
///
/// [`Text!`]: struct@crate::Text
/// [`txt_overflow`]: fn@txt_overflow
/// [`Align::TOP_START`]: zng_wgt::prelude::Align::TOP_START
#[property(CONTEXT, default(TEXT_OVERFLOW_ALIGN_VAR), widget_impl(TextAlignMix<P>))]
pub fn txt_overflow_align(child: impl UiNode, mode: impl IntoVar<Align>) -> impl UiNode {
with_context_var(child, TEXT_OVERFLOW_ALIGN_VAR, mode)
}
/// Config the automatic spacing inserted between words and letters when text is aligned to fill.
///
/// Text alignment can be set to [`Align::FILL`], if this config is set to `Some(mode)` when that happens
/// the text layout will automatically insert spaces to try and *fill* the text block. When justify is not
/// enabled, that is set to `None`, fill alignment is the same as [`Align::START`].
///
/// Sets the [`JUSTIFY_VAR`].
///
/// [`Align::FILL`]: zng_wgt::prelude::Align::FILL
/// [`Align::START`]: zng_wgt::prelude::Align::START
#[property(CONTEXT, default(JUSTIFY_VAR), widget_impl(TextAlignMix<P>))]
pub fn justify(child: impl UiNode, mode: impl IntoVar<Option<Justify>>) -> impl UiNode {
with_context_var(child, JUSTIFY_VAR, mode)
}
/// Text wrap, hyphenation.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct TextWrapMix<P>(P);
context_var! {
/// If line breaks are automatically inserted to fill the available space.
///
/// The [`LINE_BREAK_VAR`], [`WORD_BREAK_VAR`] and [`HYPHENS_VAR`] configure how the text is split.
///
/// Is `true` by default.
pub static TEXT_WRAP_VAR: bool = true;
/// Configuration of line breaks inside words during text wrap.
pub static WORD_BREAK_VAR: WordBreak = WordBreak::Normal;
/// Configuration of line breaks in Chinese, Japanese, or Korean text.
pub static LINE_BREAK_VAR: LineBreak = LineBreak::Auto;
/// Text hyphenation config.
pub static HYPHENS_VAR: Hyphens = Hyphens::default();
/// Hyphen text rendered when auto-hyphenating.
pub static HYPHEN_CHAR_VAR: Txt = Txt::from_char('-');
/// Text overflow handling.
pub static TEXT_OVERFLOW_VAR: TextOverflow = TextOverflow::Ignore;
}
impl TextWrapMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&TEXT_WRAP_VAR);
set.insert(&WORD_BREAK_VAR);
set.insert(&LINE_BREAK_VAR);
set.insert(&HYPHENS_VAR);
set.insert(&HYPHEN_CHAR_VAR);
set.insert(&TEXT_OVERFLOW_VAR);
}
}
/// Defines how text overflow is handled by the text widgets.
#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum TextOverflow {
/// Text is allowed to overflow.
///
/// Note that the text widget can still [`clip_to_bounds`], and text widgets also clip any text
/// that overflows over one line-height in any direction. Text overflow is tracked even if `Ignore`
/// is set, so custom properties may also implement some form of overflow handling.
///
/// [`clip_to_bounds`]: fn@zng_wgt::clip_to_bounds
Ignore,
/// Truncate the text so it will fit, the associated `Txt` is a suffix appended to the truncated text.
///
/// Note that if the suffix is not empty the text will truncated more to reserve space for the suffix. If
/// the suffix itself is too wide it will overflow.
Truncate(Txt),
}
impl fmt::Debug for TextOverflow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "TextOverflow")?
}
match self {
Self::Ignore => write!(f, "Ignore"),
Self::Truncate(arg0) => f.debug_tuple("Truncate").field(arg0).finish(),
}
}
}
impl TextOverflow {
/// Truncate without suffix.
pub fn truncate() -> Self {
Self::Truncate(Txt::from_static(""))
}
/// Truncate with the ellipses `'…'` char as suffix.
pub fn ellipses() -> Self {
Self::Truncate(Txt::from_char('…'))
}
}
impl_from_and_into_var! {
/// Truncate (no suffix), or ignore.
fn from(truncate: bool) -> TextOverflow {
if truncate {
TextOverflow::truncate()
} else {
TextOverflow::Ignore
}
}
fn from(truncate: Txt) -> TextOverflow {
TextOverflow::Truncate(truncate)
}
fn from(s: &'static str) -> TextOverflow {
Txt::from(s).into()
}
fn from(s: String) -> TextOverflow {
Txt::from(s).into()
}
fn from(c: char) -> TextOverflow {
Txt::from(c).into()
}
}
/// Enables or disables text wrap.
///
/// If enabled, line-breaks and hyphens are automatically inserted to flow the text to fill the available width. Wrap
/// can be configured using the [`line_break`], [`word_break`] and [`hyphens`] properties.
///
/// Sets the [`TEXT_WRAP_VAR`].
///
/// [`line_break`]: fn@line_break
/// [`word_break`]: fn@word_break
/// [`hyphens`]: fn@hyphens
#[property(CONTEXT, default(TEXT_WRAP_VAR), widget_impl(TextWrapMix<P>))]
pub fn txt_wrap(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, TEXT_WRAP_VAR, enabled)
}
/// Configure line breaks inside words during text wrap.
///
/// This value is only considered if it is impossible to fit a full word to a line.
///
/// Hyphens can be inserted in word breaks using the [`hyphens`] configuration.
///
/// Sets the [`WORD_BREAK_VAR`].
///
/// [`hyphens`]: fn@hyphens
#[property(CONTEXT, default(WORD_BREAK_VAR), widget_impl(TextWrapMix<P>))]
pub fn word_break(child: impl UiNode, mode: impl IntoVar<WordBreak>) -> impl UiNode {
with_context_var(child, WORD_BREAK_VAR, mode)
}
/// Configuration of text wrapping for Chinese, Japanese, or Korean text.
///
/// Sets the [`LINE_BREAK_VAR`].
#[property(CONTEXT, default(LINE_BREAK_VAR), widget_impl(TextWrapMix<P>))]
pub fn line_break(child: impl UiNode, mode: impl IntoVar<LineBreak>) -> impl UiNode {
with_context_var(child, LINE_BREAK_VAR, mode)
}
/// Configure hyphenation.
///
/// Note that for automatic hyphenation to work the [`lang`] must also be set and the [`HYPHENATION`] service must support it.
///
/// The auto hyphenation char can be defined using [`hyphen_char`].
///
/// [`HYPHENATION`]: zng_ext_font::HYPHENATION
/// [`lang`]: fn@lang
/// [`hyphen_char`]: fn@hyphen_char
#[property(CONTEXT, default(HYPHENS_VAR), widget_impl(TextWrapMix<P>))]
pub fn hyphens(child: impl UiNode, hyphens: impl IntoVar<Hyphens>) -> impl UiNode {
with_context_var(child, HYPHENS_VAR, hyphens)
}
/// The char or small string that is rendered when text is auto-hyphenated.
///
/// Note that hyphenation is enabled by the [`hyphens`] property.
///
/// [`hyphens`]: fn@hyphens
#[property(CONTEXT, default(HYPHEN_CHAR_VAR), widget_impl(TextWrapMix<P>))]
pub fn hyphen_char(child: impl UiNode, hyphen: impl IntoVar<Txt>) -> impl UiNode {
with_context_var(child, HYPHEN_CHAR_VAR, hyphen)
}
/// Defines if text overflow is truncated, with optional suffix append.
///
/// When enabled overflow is truncated by character or by the wrap rules if [`txt_wrap`] is enabled (it is by default).
///
/// Overflow is always ignored when the text is editable.
///
/// [`txt_wrap`]: fn@txt_wrap
#[property(CONTEXT, default(TEXT_OVERFLOW_VAR), widget_impl(TextWrapMix<P>))]
pub fn txt_overflow(child: impl UiNode, overflow: impl IntoVar<TextOverflow>) -> impl UiNode {
with_context_var(child, TEXT_OVERFLOW_VAR, overflow)
}
/// Gets if the text is overflown.
#[property(CHILD_LAYOUT+100, widget_impl(TextWrapMix<P>))]
pub fn is_overflown(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let state = state.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Deinit => {
let _ = state.set(false);
}
UiNodeOp::Layout { .. } => {
let is_o = super::node::TEXT.laidout().overflow.is_some();
if is_o != state.get() {
let _ = state.set(is_o);
}
}
_ => {}
})
}
/// Gets if the text has an entire line overflown.
///
/// This is `true` when the text has multiple lines, either due to line-break or wrap, and at
/// least one line overflows the allowed height, partially or fully.
#[property(CHILD_LAYOUT+100, widget_impl(TextWrapMix<P>))]
pub fn is_line_overflown(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let state = state.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Deinit => {
let _ = state.set(false);
}
UiNodeOp::Layout { .. } => {
let txt = super::node::TEXT.laidout();
let is_o = if let Some(info) = &txt.overflow {
info.line < txt.shaped_text.lines_len().saturating_sub(1) as _
} else {
false
};
if is_o != state.get() {
let _ = state.set(is_o);
}
}
_ => {}
})
}
/// Gets the overflow text, that is a clone of the text starting from the first overflow character.
///
/// Note that overflow is tracked even if [`txt_overflow`] is set to [`TextOverflow::Ignore`].
///
/// [`txt_overflow`]: fn@txt_overflow
#[property(CHILD_LAYOUT+100, widget_impl(TextWrapMix<P>))]
pub fn get_overflow(child: impl UiNode, txt: impl IntoVar<Txt>) -> impl UiNode {
let txt = txt.into_var();
match_node(child, move |_, op| {
if let UiNodeOp::Layout { .. } = op {
let l_txt = super::node::TEXT.laidout();
if let Some(info) = &l_txt.overflow {
let r = super::node::TEXT.resolved();
let tail = &r.segmented_text.text()[info.text_char..];
if txt.with(|t| t != tail) {
let _ = txt.set(Txt::from_str(tail));
}
} else if txt.with(|t| !t.is_empty()) {
let _ = txt.set(Txt::from_static(""));
}
}
})
}
/// Text underline, overline and strikethrough lines.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct TextDecorationMix<P>(P);
bitflags! {
/// Represents what parts of a text the underline must skip over.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct UnderlineSkip: u8 {
/// Underline spans the entire text length.
const NONE = 0;
/// Skip white space.
const SPACES = 0b0001;
/// Skip over glyph descenders that intersect with the underline.
const GLYPHS = 0b0010;
/// Default value, skip glyphs.
const DEFAULT = Self::GLYPHS.bits();
}
}
impl Default for UnderlineSkip {
fn default() -> Self {
Self::DEFAULT
}
}
/// Defines what line gets traced by the text underline decoration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum UnderlinePosition {
/// Underline is positioned using the offset defined in the font file.
#[default]
Font,
/// Underline is positioned after the text *descent*, avoiding crossover with all glyph descenders.
Descent,
}
context_var! {
/// Underline thickness.
pub static UNDERLINE_THICKNESS_VAR: UnderlineThickness = 0;
/// Underline style.
pub static UNDERLINE_STYLE_VAR: LineStyle = LineStyle::Hidden;
/// Underline color, inherits from [`FONT_COLOR_VAR`].
pub static UNDERLINE_COLOR_VAR: Rgba = FONT_COLOR_VAR;
/// Parts of text skipped by underline.
pub static UNDERLINE_SKIP_VAR: UnderlineSkip = UnderlineSkip::DEFAULT;
/// Position of the underline.
pub static UNDERLINE_POSITION_VAR: UnderlinePosition = UnderlinePosition::Font;
/// Overline thickness.
pub static OVERLINE_THICKNESS_VAR: TextLineThickness = 0;
/// Overline style.
pub static OVERLINE_STYLE_VAR: LineStyle = LineStyle::Hidden;
/// Overline color, inherits from [`FONT_COLOR_VAR`].
pub static OVERLINE_COLOR_VAR: Rgba = FONT_COLOR_VAR;
/// Strikethrough thickness.
pub static STRIKETHROUGH_THICKNESS_VAR: TextLineThickness = 0;
/// Strikethrough style.
pub static STRIKETHROUGH_STYLE_VAR: LineStyle = LineStyle::Hidden;
/// Strikethrough color, inherits from [`FONT_COLOR_VAR`].
pub static STRIKETHROUGH_COLOR_VAR: Rgba = FONT_COLOR_VAR;
/// Underline thickness for the IME preview underline.
pub static IME_UNDERLINE_THICKNESS_VAR: UnderlineThickness = 1;
/// Underline style for the IME preview underline.
pub static IME_UNDERLINE_STYLE_VAR: LineStyle = LineStyle::Dotted;
}
impl TextDecorationMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&UNDERLINE_THICKNESS_VAR);
set.insert(&UNDERLINE_STYLE_VAR);
set.insert(&UNDERLINE_COLOR_VAR);
set.insert(&UNDERLINE_SKIP_VAR);
set.insert(&UNDERLINE_POSITION_VAR);
set.insert(&OVERLINE_THICKNESS_VAR);
set.insert(&OVERLINE_STYLE_VAR);
set.insert(&OVERLINE_COLOR_VAR);
set.insert(&STRIKETHROUGH_THICKNESS_VAR);
set.insert(&STRIKETHROUGH_STYLE_VAR);
set.insert(&STRIKETHROUGH_COLOR_VAR);
set.insert(&IME_UNDERLINE_THICKNESS_VAR);
set.insert(&IME_UNDERLINE_STYLE_VAR);
}
}
/// Draw lines *under* each text line.
///
/// Sets the [`UNDERLINE_THICKNESS_VAR`] and [`UNDERLINE_STYLE_VAR`].
#[property(CONTEXT, default(UNDERLINE_THICKNESS_VAR, UNDERLINE_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
pub fn underline(child: impl UiNode, thickness: impl IntoVar<UnderlineThickness>, style: impl IntoVar<LineStyle>) -> impl UiNode {
let child = with_context_var(child, UNDERLINE_THICKNESS_VAR, thickness);
with_context_var(child, UNDERLINE_STYLE_VAR, style)
}
/// Custom [`underline`](fn@underline) color, if not set
/// the [`font_color`](fn@font_color) is used.
///
/// Sets the [`UNDERLINE_COLOR_VAR`].
#[property(CONTEXT, default(UNDERLINE_COLOR_VAR), widget_impl(TextDecorationMix<P>))]
pub fn underline_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
with_context_var(child, UNDERLINE_COLOR_VAR, color)
}
/// Defines what segments of each text line are skipped when tracing the [`underline`](fn@underline).
///
/// By default skips glyphs that intercept the underline.
///
/// Sets the [`UNDERLINE_SKIP_VAR`].
#[property(CONTEXT, default(UNDERLINE_SKIP_VAR), widget_impl(TextDecorationMix<P>))]
pub fn underline_skip(child: impl UiNode, skip: impl IntoVar<UnderlineSkip>) -> impl UiNode {
with_context_var(child, UNDERLINE_SKIP_VAR, skip)
}
/// Defines what font line gets traced by the underline.
///
/// By default uses the font configuration, but it usually crosses over glyph *descents* causing skips on
/// the line, you can set this [`UnderlinePosition::Descent`] to fully clear all glyph *descents*.
///
/// Sets the [`UNDERLINE_POSITION_VAR`].
#[property(CONTEXT, default(UNDERLINE_POSITION_VAR), widget_impl(TextDecorationMix<P>))]
pub fn underline_position(child: impl UiNode, position: impl IntoVar<UnderlinePosition>) -> impl UiNode {
with_context_var(child, UNDERLINE_POSITION_VAR, position)
}
/// Draw lines *above* each text line.
///
/// Sets the [`OVERLINE_THICKNESS_VAR`] and [`OVERLINE_STYLE_VAR`].
#[property(CONTEXT, default(OVERLINE_THICKNESS_VAR, OVERLINE_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
pub fn overline(child: impl UiNode, thickness: impl IntoVar<TextLineThickness>, style: impl IntoVar<LineStyle>) -> impl UiNode {
let child = with_context_var(child, OVERLINE_THICKNESS_VAR, thickness);
with_context_var(child, OVERLINE_STYLE_VAR, style)
}
/// Custom [`overline`](fn@overline) color, if not set
/// the [`font_color`](fn@font_color) is used.
///
/// Sets the [`OVERLINE_COLOR_VAR`].
#[property(CONTEXT, default(OVERLINE_COLOR_VAR), widget_impl(TextDecorationMix<P>))]
pub fn overline_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
with_context_var(child, OVERLINE_COLOR_VAR, color)
}
/// Draw lines across each text line.
///
/// Sets the [`STRIKETHROUGH_THICKNESS_VAR`] and [`STRIKETHROUGH_STYLE_VAR`].
#[property(CONTEXT, default(STRIKETHROUGH_THICKNESS_VAR, STRIKETHROUGH_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
pub fn strikethrough(child: impl UiNode, thickness: impl IntoVar<TextLineThickness>, style: impl IntoVar<LineStyle>) -> impl UiNode {
let child = with_context_var(child, STRIKETHROUGH_THICKNESS_VAR, thickness);
with_context_var(child, STRIKETHROUGH_STYLE_VAR, style)
}
/// Custom [`strikethrough`](fn@strikethrough) color, if not set
/// the [`font_color`](fn@font_color) is used.
///
/// Sets the [`STRIKETHROUGH_COLOR_VAR`].
#[property(CONTEXT, default(STRIKETHROUGH_COLOR_VAR), widget_impl(TextDecorationMix<P>))]
pub fn strikethrough_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
with_context_var(child, STRIKETHROUGH_COLOR_VAR, color)
}
/// Style and thickness of the line drawn *under* the IME preview text.
///
/// Sets the [`IME_UNDERLINE_THICKNESS_VAR`] and [`IME_UNDERLINE_STYLE_VAR`].
#[property(CONTEXT, default(IME_UNDERLINE_THICKNESS_VAR, IME_UNDERLINE_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
pub fn ime_underline(child: impl UiNode, thickness: impl IntoVar<UnderlineThickness>, style: impl IntoVar<LineStyle>) -> impl UiNode {
let child = with_context_var(child, IME_UNDERLINE_THICKNESS_VAR, thickness);
with_context_var(child, IME_UNDERLINE_STYLE_VAR, style)
}
/// Text spacing properties.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// See also [`ParagraphMix<P>`] for paragraph spacing.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct TextSpacingMix<P>(P);
context_var! {
/// Text line height of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static LINE_HEIGHT_VAR: LineHeight = LineHeight::Default;
/// Extra spacing in between lines of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static LINE_SPACING_VAR: LineSpacing = LineSpacing::Default;
/// Extra letter spacing of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static LETTER_SPACING_VAR: LetterSpacing = LetterSpacing::Default;
/// Extra word spacing of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static WORD_SPACING_VAR: WordSpacing = WordSpacing::Default;
/// Length of the `TAB` space.
pub static TAB_LENGTH_VAR: TabLength = 400.pct();
}
impl TextSpacingMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&LINE_HEIGHT_VAR);
set.insert(&LINE_SPACING_VAR);
set.insert(&LETTER_SPACING_VAR);
set.insert(&WORD_SPACING_VAR);
set.insert(&TAB_LENGTH_VAR);
}
}
/// Height of each text line. If not set inherits the `line_height` from the parent widget.
///
/// The [`Default`] value is computed from the font metrics, `ascent - descent + line_gap`, this is
/// usually similar to `1.2.em()`. Relative values are computed from the default value, so `200.pct()` is double
/// the default line height.
///
/// The text is vertically centered inside the height.
///
/// [`Default`]: Length::Default
///
/// Sets the [`LINE_HEIGHT_VAR`].
#[property(CONTEXT, default(LINE_HEIGHT_VAR), widget_impl(TextSpacingMix<P>))]
pub fn line_height(child: impl UiNode, height: impl IntoVar<LineHeight>) -> impl UiNode {
with_context_var(child, LINE_HEIGHT_VAR, height)
}
/// Extra spacing added in between text letters. If not set inherits the `letter_spacing` from the parent widget.
///
/// Letter spacing is computed using the font data, this unit represents
/// extra space added to the computed spacing.
///
/// A "letter" is a character glyph cluster, e.g.: `a`, `â`, `1`, `-`, `漢`.
///
/// The [`Default`] value signals that letter spacing can be tweaked when text *justification* is enabled, all other
/// values disable automatic adjustments for justification inside words.
///
/// Relative values are computed from the length of the space `' '` character.
///
/// [`Default`]: Length::Default
///
/// This property sets the [`LETTER_SPACING_VAR`] context var that affects all inner texts.
#[property(CONTEXT, default(LETTER_SPACING_VAR), widget_impl(TextSpacingMix<P>))]
pub fn letter_spacing(child: impl UiNode, extra: impl IntoVar<LetterSpacing>) -> impl UiNode {
with_context_var(child, LETTER_SPACING_VAR, extra)
}
/// Extra spacing in-between text lines. If not set inherits the `line_spacing` from the parent widget.
///
/// The [`Default`] value is zero. Relative values are calculated from the [`LineHeight`], so `50.pct()` is half
/// the computed line height. If the text only has one line this property is not used.
///
/// [`Default`]: Length::Default
///
/// Sets the [`LINE_SPACING_VAR`].
///
/// [`LineHeight`]: zng_ext_font::LineHeight
#[property(CONTEXT, default(LINE_SPACING_VAR), widget_impl(TextSpacingMix<P>))]
pub fn line_spacing(child: impl UiNode, extra: impl IntoVar<LineSpacing>) -> impl UiNode {
with_context_var(child, LINE_SPACING_VAR, extra)
}
/// Extra spacing added to the Unicode `U+0020 SPACE` character. If not set inherits the `word_spacing` from the parent widget.
///
/// Word spacing is done using the space character "advance" as defined in the font,
/// this unit represents extra spacing added to that default spacing.
///
/// A "word" is the sequence of characters in-between space characters. This extra
/// spacing is applied per space character not per word, if there are three spaces between words
/// the extra spacing is applied thrice. Usually the number of spaces between words is collapsed to one,
/// see [`WhiteSpace`], resulting in only one extra spacing.
///
/// The [`Default`] value signals that word spacing can be tweaked when text *justification* is enabled, all other
/// values disable automatic adjustments for justification. Relative values are computed from the length of the space `' '` character,
/// so a word spacing of `100.pct()` visually adds *another* space in between words.
///
/// [`Default`]: Length::Default
///
/// This property sets the [`WORD_SPACING_VAR`] context var that affects all inner widgets.
///
/// [`WhiteSpace`]: zng_ext_font::WhiteSpace
#[property(CONTEXT, default(WORD_SPACING_VAR), widget_impl(TextSpacingMix<P>))]
pub fn word_spacing(child: impl UiNode, extra: impl IntoVar<WordSpacing>) -> impl UiNode {
with_context_var(child, WORD_SPACING_VAR, extra)
}
/// Length of the TAB character space, relative to the normal space advance.
///
/// Is set to `400.pct()` by default, so 4 times a space.
///
/// Sets the [`TAB_LENGTH_VAR`].
#[property(CONTEXT, default(TAB_LENGTH_VAR), widget_impl(TextSpacingMix<P>))]
pub fn tab_length(child: impl UiNode, length: impl IntoVar<TabLength>) -> impl UiNode {
with_context_var(child, TAB_LENGTH_VAR, length)
}
/// Text transform properties.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct TextTransformMix<P>(P);
context_var! {
/// Text white space transform of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static WHITE_SPACE_VAR: WhiteSpace = WhiteSpace::Preserve;
/// Text transformation function applied to [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static TEXT_TRANSFORM_VAR: TextTransformFn = TextTransformFn::None;
}
impl TextTransformMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&WHITE_SPACE_VAR);
set.insert(&TEXT_TRANSFORM_VAR);
}
}
/// Text white space transform.
///
/// Can be used to collapse a sequence of spaces into a single one, or to ignore line-breaks.
/// Is [`WhiteSpace::Preserve`] by default.
///
/// This property is not applied when the text is [`txt_editable`].
///
/// Sets the [`WHITE_SPACE_VAR`].
///
/// [`txt_editable`]: fn@txt_editable
/// [`WhiteSpace::Preserve`]: zng_ext_font::WhiteSpace::Preserve
#[property(CONTEXT, default(WHITE_SPACE_VAR), widget_impl(TextTransformMix<P>))]
pub fn white_space(child: impl UiNode, transform: impl IntoVar<WhiteSpace>) -> impl UiNode {
with_context_var(child, WHITE_SPACE_VAR, transform)
}
/// Text transform, character replacement applied to the text before it is processed by the text widget.
///
/// This property is not applied when the text is [`txt_editable`].
///
/// Sets the [`TEXT_TRANSFORM_VAR`].
///
/// [`txt_editable`]: fn@txt_editable
#[property(CONTEXT, default(TEXT_TRANSFORM_VAR), widget_impl(TextTransformMix<P>))]
pub fn txt_transform(child: impl UiNode, transform: impl IntoVar<TextTransformFn>) -> impl UiNode {
with_context_var(child, TEXT_TRANSFORM_VAR, transform)
}
/// Language and text direction properties.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct LangMix<P>(P);
/// Sets the text language and script for the widget and descendants.
///
/// This property affects all texts inside the widget and the layout direction.
///
/// Sets the [`LANG_VAR`] and [`DIRECTION_VAR`] context vars and the [`LayoutMetrics::direction`].
/// Also sets the [`access::lang`] when accessibility is enabled.
///
/// [`access::lang`]: fn@zng_wgt_access::lang
/// [`LANG_VAR`]: zng_ext_l10n::LANG_VAR
/// [`DIRECTION_VAR`]: zng_wgt::prelude::DIRECTION_VAR
/// [`LayoutMetrics::direction`]: zng_wgt::prelude::LayoutMetrics::direction
#[property(CONTEXT, default(LANG_VAR), widget_impl(LangMix<P>))]
pub fn lang(child: impl UiNode, lang: impl IntoVar<Langs>) -> impl UiNode {
let lang = lang.into_var();
let child = direction(child, lang.map(|l| l.best().direction()));
let child = zng_wgt_access::lang(child, lang.map(|l| l.best().clone()));
with_context_var(child, LANG_VAR, lang)
}
/// Sets the layout direction used in the layout of the widget and descendants.
///
/// Note that the [`lang`] property already sets the direction, this property can be used to directly override the direction.
///
/// Sets the [`DIRECTION_VAR`] context var and the [`LayoutMetrics::direction`].
///
/// [`lang`]: fn@lang
///
/// [`DIRECTION_VAR`]: zng_wgt::prelude::DIRECTION_VAR
/// [`LayoutMetrics::direction`]: zng_wgt::prelude::LayoutMetrics::direction
#[property(CONTEXT+1, default(DIRECTION_VAR), widget_impl(LangMix<P>))]
pub fn direction(child: impl UiNode, direction: impl IntoVar<LayoutDirection>) -> impl UiNode {
let child = match_node(child, |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&DIRECTION_VAR);
}
UiNodeOp::Measure { wm, desired_size } => {
*desired_size = LAYOUT.with_direction(DIRECTION_VAR.get(), || child.measure(wm));
}
UiNodeOp::Layout { wl, final_size } => *final_size = LAYOUT.with_direction(DIRECTION_VAR.get(), || child.layout(wl)),
_ => {}
});
with_context_var(child, DIRECTION_VAR, direction)
}
impl LangMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&LANG_VAR);
set.insert(&DIRECTION_VAR);
}
}
/// Advanced font config, features, kerning, variations and more.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct FontFeaturesMix<P>(P);
context_var! {
/// Font features of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_FEATURES_VAR: FontFeatures = FontFeatures::new();
/// Font variations of [`Text!`] spans.
///
/// [`Text!`]: struct@crate::Text
pub static FONT_VARIATIONS_VAR: FontVariations = FontVariations::new();
}
impl FontFeaturesMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&FONT_FEATURES_VAR);
set.insert(&FONT_VARIATIONS_VAR);
}
}
/// Includes the font variation config in the widget context.
///
/// The variation `name` is set for the [`FONT_VARIATIONS_VAR`] in this context, variations already set in the parent
/// context that are not the same `name` are also included.
pub fn with_font_variation(child: impl UiNode, name: FontVariationName, value: impl IntoVar<f32>) -> impl UiNode {
with_context_var(
child,
FONT_VARIATIONS_VAR,
merge_var!(FONT_VARIATIONS_VAR, value.into_var(), move |variations, value| {
let mut variations = variations.clone();
variations.insert(name, *value);
variations
}),
)
}
/// Include the font feature config in the widget context.
///
/// The modifications done in `set_feature` are visible only in the [`FONT_FEATURES_VAR`] in this context, and features
/// already set in a parent context are included.
pub fn with_font_feature<C, S, V, D>(child: C, state: V, set_feature: D) -> impl UiNode
where
C: UiNode,
S: VarValue,
V: IntoVar<S>,
D: FnMut(&mut FontFeatures, S) -> S + Send + 'static,
{
let mut set_feature = set_feature;
with_context_var(
child,
FONT_FEATURES_VAR,
merge_var!(FONT_FEATURES_VAR, state.into_var(), move |features, state| {
let mut features = features.clone();
set_feature(&mut features, state.clone());
features
}),
)
}
/// Sets font variations.
///
/// **Note:** This property fully replaces the font variations for the widget and descendants, use [`with_font_variation`]
/// to create a property that sets a variation but retains others from the context.
#[property(CONTEXT, default(FONT_VARIATIONS_VAR), widget_impl(FontFeaturesMix<P>))]
pub fn font_variations(child: impl UiNode, variations: impl IntoVar<FontVariations>) -> impl UiNode {
with_context_var(child, FONT_VARIATIONS_VAR, variations)
}
/// Sets font features.
///
/// **Note:** This property fully replaces the font variations for the widget and descendants, use [`with_font_variation`]
/// to create a property that sets a variation but retains others from the context.
#[property(CONTEXT, default(FONT_FEATURES_VAR), widget_impl(FontFeaturesMix<P>))]
pub fn font_features(child: impl UiNode, features: impl IntoVar<FontFeatures>) -> impl UiNode {
with_context_var(child, FONT_FEATURES_VAR, features)
}
/// Sets the font kerning feature.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_kerning(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.kerning().set(s))
}
/// Sets the font common ligatures features.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_common_lig(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.common_lig().set(s))
}
/// Sets the font discretionary ligatures feature.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_discretionary_lig(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.discretionary_lig().set(s))
}
/// Sets the font historical ligatures feature.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_historical_lig(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.historical_lig().set(s))
}
/// Sets the font contextual alternatives feature.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_contextual_alt(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.contextual_alt().set(s))
}
/// Sets the font capital variant features.
#[property(CONTEXT, default(CapsVariant::Auto), widget_impl(FontFeaturesMix<P>))]
pub fn font_caps(child: impl UiNode, state: impl IntoVar<CapsVariant>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.caps().set(s))
}
/// Sets the font numeric variant features.
#[property(CONTEXT, default(NumVariant::Auto), widget_impl(FontFeaturesMix<P>))]
pub fn font_numeric(child: impl UiNode, state: impl IntoVar<NumVariant>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.numeric().set(s))
}
/// Sets the font numeric spacing features.
#[property(CONTEXT, default(NumSpacing::Auto), widget_impl(FontFeaturesMix<P>))]
pub fn font_num_spacing(child: impl UiNode, state: impl IntoVar<NumSpacing>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.num_spacing().set(s))
}
/// Sets the font numeric fraction features.
#[property(CONTEXT, default(NumFraction::Auto), widget_impl(FontFeaturesMix<P>))]
pub fn font_num_fraction(child: impl UiNode, state: impl IntoVar<NumFraction>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.num_fraction().set(s))
}
/// Sets the font swash features.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_swash(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.swash().set(s))
}
/// Sets the font stylistic alternative feature.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_stylistic(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.stylistic().set(s))
}
/// Sets the font historical forms alternative feature.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_historical_forms(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.historical_forms().set(s))
}
/// Sets the font ornaments alternative feature.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_ornaments(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.ornaments().set(s))
}
/// Sets the font annotation alternative feature.
#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_annotation(child: impl UiNode, state: impl IntoVar<FontFeatureState>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.annotation().set(s))
}
/// Sets the font stylistic set alternative feature.
#[property(CONTEXT, default(FontStyleSet::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_style_set(child: impl UiNode, state: impl IntoVar<FontStyleSet>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.style_set().set(s))
}
/// Sets the font character variant alternative feature.
#[property(CONTEXT, default(CharVariant::auto()), widget_impl(FontFeaturesMix<P>))]
pub fn font_char_variant(child: impl UiNode, state: impl IntoVar<CharVariant>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.char_variant().set(s))
}
/// Sets the font sub/super script position alternative feature.
#[property(CONTEXT, default(FontPosition::Auto), widget_impl(FontFeaturesMix<P>))]
pub fn font_position(child: impl UiNode, state: impl IntoVar<FontPosition>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.position().set(s))
}
/// Sets the Japanese logographic set.
#[property(CONTEXT, default(JpVariant::Auto), widget_impl(FontFeaturesMix<P>))]
pub fn font_jp_variant(child: impl UiNode, state: impl IntoVar<JpVariant>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.jp_variant().set(s))
}
/// Sets the Chinese logographic set.
#[property(CONTEXT, default(CnVariant::Auto), widget_impl(FontFeaturesMix<P>))]
pub fn font_cn_variant(child: impl UiNode, state: impl IntoVar<CnVariant>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.cn_variant().set(s))
}
/// Sets the East Asian figure width.
#[property(CONTEXT, default(EastAsianWidth::Auto), widget_impl(FontFeaturesMix<P>))]
pub fn font_ea_width(child: impl UiNode, state: impl IntoVar<EastAsianWidth>) -> impl UiNode {
with_font_feature(child, state, |f, s| f.ea_width().set(s))
}
/// Text edit properties.
///
/// All properties in this mixin affects [`Text!`] nodes inside the widget where they are set.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct TextEditMix<P>(P);
context_var! {
/// Text is editable.
pub static TEXT_EDITABLE_VAR: bool = false;
/// Text is selectable.
pub static TEXT_SELECTABLE_VAR: bool = false;
/// Accepts `'\t'` input when editable.
pub static ACCEPTS_TAB_VAR: bool = false;
/// Accepts `'\n'` input when editable.
pub static ACCEPTS_ENTER_VAR: bool = false;
/// Caret color, inherits from [`FONT_COLOR_VAR`].
pub static CARET_COLOR_VAR: Rgba = FONT_COLOR_VAR;
/// Interactive caret visual.
pub static INTERACTIVE_CARET_VISUAL_VAR: WidgetFn<CaretShape> = wgt_fn!(|s| super::node::default_interactive_caret_visual(s));
/// Interactive caret mode.
pub static INTERACTIVE_CARET_VAR: InteractiveCaretMode = InteractiveCaretMode::default();
/// Selection background color.
pub static SELECTION_COLOR_VAR: Rgba = colors::AZURE.with_alpha(30.pct());
/// If text parse updated for every text change.
pub static TXT_PARSE_LIVE_VAR: bool = true;
/// Debounce time for change stop.
pub static CHANGE_STOP_DELAY_VAR: Duration = 1.secs();
/// Auto selection on keyboard focus.
pub static AUTO_SELECTION_VAR: AutoSelection = AutoSelection::default();
/// Maximum number of characters that can be input.
///
/// Zero means no limit. Is zero by default.
pub static MAX_CHARS_COUNT_VAR: usize = 0;
/// Replacement character used when obscuring text.
pub static OBSCURING_CHAR_VAR: char = '•';
/// If text characters are replaced with [`OBSCURING_CHAR_VAR`] for rendering.
pub static OBSCURE_TXT_VAR: bool = false;
pub(super) static TXT_PARSE_PENDING_VAR: bool = false;
}
impl TextEditMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&TEXT_EDITABLE_VAR);
set.insert(&TEXT_SELECTABLE_VAR);
set.insert(&ACCEPTS_ENTER_VAR);
set.insert(&CARET_COLOR_VAR);
set.insert(&INTERACTIVE_CARET_VISUAL_VAR);
set.insert(&INTERACTIVE_CARET_VAR);
set.insert(&SELECTION_COLOR_VAR);
set.insert(&TXT_PARSE_LIVE_VAR);
set.insert(&CHANGE_STOP_DELAY_VAR);
set.insert(&AUTO_SELECTION_VAR);
set.insert(&MAX_CHARS_COUNT_VAR);
set.insert(&OBSCURING_CHAR_VAR);
set.insert(&OBSCURE_TXT_VAR);
}
}
/// Defines the position of an interactive caret in relation to the selection.
///
/// See [`interactive_caret_visual`](fn@interactive_caret_visual) for more details.
#[derive(Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum CaretShape {
/// Caret defines the selection start in LTR and end in RTL text.
SelectionLeft,
/// Caret defines the selection end in LTR and start in RTL text.
SelectionRight,
/// Caret defines the insert point, when there is no selection.
Insert,
}
impl fmt::Debug for CaretShape {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "CaretShape::")?;
}
match self {
Self::SelectionLeft => write!(f, "SelectionLeft"),
Self::SelectionRight => write!(f, "SelectionRight"),
Self::Insert => write!(f, "Insert"),
}
}
}
/// Defines when the interactive carets are used.
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum InteractiveCaretMode {
/// Uses interactive carets only for touch selections, uses non-interactive caret for other selections.
#[default]
TouchOnly,
/// Uses interactive carets for all selections.
Enabled,
/// Uses non-interactive carets for all selections.
Disabled,
}
impl fmt::Debug for InteractiveCaretMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "InteractiveCaretMode::")?;
}
match self {
Self::TouchOnly => write!(f, "TouchOnly"),
Self::Enabled => write!(f, "Enabled"),
Self::Disabled => write!(f, "Disabled"),
}
}
}
impl_from_and_into_var! {
fn from(enabled: bool) -> InteractiveCaretMode {
if enabled {
InteractiveCaretMode::Enabled
} else {
InteractiveCaretMode::Disabled
}
}
}
/// Enable text caret, input and makes the widget focusable.
///
/// If the `txt` variable is read-only, this is ignored, if the var is writeable this
/// enables text input and modifies the variable.
///
/// Sets the [`TEXT_EDITABLE_VAR`].
#[property(CONTEXT, default(TEXT_EDITABLE_VAR), widget_impl(TextEditMix<P>))]
pub fn txt_editable(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, TEXT_EDITABLE_VAR, enabled)
}
/// Enable text selection, copy and makes the widget focusable.
///
/// Sets the [`TEXT_SELECTABLE_VAR`].
#[property(CONTEXT, default(TEXT_SELECTABLE_VAR), widget_impl(TextEditMix<P>))]
pub fn txt_selectable(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, TEXT_SELECTABLE_VAR, enabled)
}
/// If the `'\t'` character is inserted when tab is pressed and the text is editable.
///
/// If not enabled or the text is not editable, then pressing tab moves the focus like normal.
///
/// Sets the [`ACCEPTS_TAB_VAR`].
#[property(CONTEXT, default(ACCEPTS_TAB_VAR), widget_impl(TextEditMix<P>))]
pub fn accepts_tab(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, ACCEPTS_TAB_VAR, enabled)
}
/// If the `'\n'` character is inserted when enter is pressed and the text is editable.
///
/// Sets the [`ACCEPTS_ENTER_VAR`].
#[property(CONTEXT, default(ACCEPTS_ENTER_VAR), widget_impl(TextEditMix<P>))]
pub fn accepts_enter(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, ACCEPTS_ENTER_VAR, enabled)
}
/// Defines the color of the non-interactive caret.
///
/// Sets the [`CARET_COLOR_VAR`].
#[property(CONTEXT, default(CARET_COLOR_VAR), widget_impl(TextEditMix<P>))]
pub fn caret_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
with_context_var(child, CARET_COLOR_VAR, color)
}
/// Defines custom caret visual for interactive caret.
///
/// The `visual` node becomes the content of a [layered widget] at the `ADORNER+1` layer, the text widget context is
/// propagated so contextual variables and value work seamless inside the node.
///
/// The `visual` node must set one special value during layout, the [`set_interactive_caret_spot`] must be called to
/// set the offset to the middle of the caret line in the visual inner-bounds, this is used to position the caret.
///
/// Sets the [`INTERACTIVE_CARET_VISUAL_VAR`].
///
/// [layered widget]: zng_wgt_layer
/// [`set_interactive_caret_spot`]: super::node::set_interactive_caret_spot
#[property(CONTEXT, default(INTERACTIVE_CARET_VISUAL_VAR), widget_impl(TextEditMix<P>))]
pub fn interactive_caret_visual(child: impl UiNode, visual: impl IntoVar<WidgetFn<CaretShape>>) -> impl UiNode {
with_context_var(child, INTERACTIVE_CARET_VISUAL_VAR, visual)
}
/// Defines when the interactive carets are used.
///
/// By default only uses interactive carets for touch selections.
#[property(CONTEXT, default(INTERACTIVE_CARET_VAR), widget_impl(TextEditMix<P>))]
pub fn interactive_caret(child: impl UiNode, mode: impl IntoVar<InteractiveCaretMode>) -> impl UiNode {
with_context_var(child, INTERACTIVE_CARET_VAR, mode)
}
/// Sets the [`SELECTION_COLOR_VAR`].
#[property(CONTEXT, default(SELECTION_COLOR_VAR), widget_impl(TextEditMix<P>))]
pub fn selection_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
with_context_var(child, SELECTION_COLOR_VAR, color)
}
/// If [`txt_parse`] tries to parse after any text change immediately.
///
/// This is enabled by default, if disabled the [`PARSE_CMD`] can be used to update pending parse.
///
/// This property sets the [`TXT_PARSE_LIVE_VAR`].
///
/// [`txt_parse`]: fn@super::txt_parse
/// [`PARSE_CMD`]: super::cmd::PARSE_CMD
#[property(CONTEXT, default(TXT_PARSE_LIVE_VAR), widget_impl(TextEditMix<P>))]
pub fn txt_parse_live(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, TXT_PARSE_LIVE_VAR, enabled)
}
/// Shorthand property, disables live parsing and parse on change stop.
///
/// This property sets [`txt_parse_live`] and [`on_change_stop`] on the widget.
///
/// [`txt_parse_live`]: fn@txt_parse_live
/// [`on_change_stop`]: fn@on_change_stop
#[property(EVENT, widget_impl(TextEditMix<P>))]
pub fn txt_parse_on_stop(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
let enabled = enabled.into_var();
let child = txt_parse_live(child, enabled.map(|&b| !b));
on_change_stop(
child,
hn!(|_| {
if enabled.get() {
super::cmd::PARSE_CMD.scoped(WIDGET.id()).notify();
}
}),
)
}
/// Maximum number of characters that can be input.
///
/// Zero means no limit. Is zero by default.
///
/// This property sets the [`MAX_CHARS_COUNT_VAR`].
#[property(CONTEXT, default(MAX_CHARS_COUNT_VAR), widget_impl(TextEditMix<P>))]
pub fn max_chars_count(child: impl UiNode, max: impl IntoVar<usize>) -> impl UiNode {
with_context_var(child, MAX_CHARS_COUNT_VAR, max)
}
/// If text has changed but [`txt_parse`] has not tried to parse the new text yet.
///
/// This can only be `true` if [`txt_parse_live`] is `false`.
///
/// [`txt_parse`]: fn@super::txt_parse
/// [`txt_parse_live`]: fn@txt_parse_live
#[property(CONTEXT, default(false), widget_impl(TextEditMix<P>))]
pub fn is_parse_pending(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
// reverse context, `txt_parse` sets `TXT_PARSE_PENDING_VAR`
with_context_var(child, TXT_PARSE_PENDING_VAR, state)
}
/// Called after the text changed and interaction has stopped.
///
/// The `handler` will be called after change and [`change_stop_delay`] elapses, or the widget loses focus,
/// or the [`Key::Enter`] is pressed and [`accepts_enter`] is `false`.
///
/// [`change_stop_delay`]: fn@change_stop_delay
/// [`accepts_enter`]: fn@accepts_enter
/// [`Key::Enter`]: zng_ext_input::keyboard::Key::Enter
#[property(EVENT, widget_impl(TextEditMix<P>))]
pub fn on_change_stop(child: impl UiNode, handler: impl WidgetHandler<ChangeStopArgs>) -> impl UiNode {
super::node::on_change_stop(child, handler)
}
/// Debounce time for [`on_change_stop`].
///
/// After the text stops changing and `delay` is elapsed the change stop handled is called, even
/// if the widget is still focused.
///
/// Is `1.secs()` by default.
///
/// Sets [`CHANGE_STOP_DELAY_VAR`].
///
/// [`on_change_stop`]: fn@on_change_stop
#[property(CONTEXT, default(CHANGE_STOP_DELAY_VAR), widget_impl(TextEditMix<P>))]
pub fn change_stop_delay(child: impl UiNode, delay: impl IntoVar<Duration>) -> impl UiNode {
with_context_var(child, CHANGE_STOP_DELAY_VAR, delay)
}
/// Auto-selection on focus when the text is selectable.
///
/// If enabled on keyboard focus all text is selected and on blur any selection is cleared.
#[property(CONTEXT, default(AUTO_SELECTION_VAR), widget_impl(TextEditMix<P>))]
pub fn auto_selection(child: impl UiNode, mode: impl IntoVar<AutoSelection>) -> impl UiNode {
with_context_var(child, AUTO_SELECTION_VAR, mode)
}
/// Replacement character used when obscuring text.
///
/// When [`obscure_txt`] is enabled the text characters are replaced by this one.
///
/// [`obscure_txt`]: fn@obscure_txt
#[property(CONTEXT, default(OBSCURING_CHAR_VAR), widget_impl(TextEditMix<P>))]
pub fn obscuring_char(child: impl UiNode, character: impl IntoVar<char>) -> impl UiNode {
with_context_var(child, OBSCURING_CHAR_VAR, character)
}
/// If the typed text is obscured in render.
///
/// When enabled each text character is replaced with [`obscuring_char`], cut, copy and undo commands are disabled.
///
/// Note that the text variable is still **plain text** in memory, a memory dump while the widget is filled can leak
/// the password, this is a potential security problem shared by apps that accept typed passwords. To mitigate the problem
/// don't use automatic crash reports with memory dump, drop the widget and the text variable as soon as possible,
/// design the app to show the password widget last to minimize its lifetime.
///
/// [`obscuring_char`]: fn@obscuring_char
#[property(CONTEXT, default(OBSCURE_TXT_VAR), widget_impl(TextEditMix<P>))]
pub fn obscure_txt(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, OBSCURE_TXT_VAR, enabled)
}
bitflags! {
/// Defines when text is auto-selected on focus.
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct AutoSelection: u8 {
/// No auto-selection.
const DISABLED = 0;
/// Clear selection on blur if the widget is not the ALT return focus and is not the parent scope return focus.
///
/// This is the default behavior.
const CLEAR_ON_BLUR = 0b0000_0001;
/// Select all on keyboard initiated focus.
const ALL_ON_FOCUS_KEYBOARD = 0b0000_00010;
/// Select all on pointer release if the pointer press event caused the focus to happen and it did not change
/// the selection and there is no selection on release.
const ALL_ON_FOCUS_POINTER = 0b0000_00100;
/// All auto-selection features enabled.
const ENABLED = 0b1111_1111;
}
}
impl_from_and_into_var! {
fn from(enabled: bool) -> AutoSelection {
if enabled {
AutoSelection::ENABLED
} else {
AutoSelection::DISABLED
}
}
}
impl Default for AutoSelection {
fn default() -> Self {
Self::CLEAR_ON_BLUR
}
}
/// Arguments for [`on_change_stop`].
///
/// [`on_change_stop`]: fn@on_change_stop
#[derive(Debug, Clone)]
pub struct ChangeStopArgs {
/// Event cause.
pub cause: ChangeStopCause,
}
/// Cause of an [`on_change_stop`].
///
/// [`on_change_stop`]: fn@on_change_stop
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChangeStopCause {
/// The [`change_stop_delay`] elapsed.
///
/// [`change_stop_delay`]: fn@change_stop_delay
DelayElapsed,
/// The [`Key::Enter`] was pressed and [`accepts_enter`] is `false`.
///
/// [`Key::Enter`]: zng_ext_input::keyboard::Key::Enter
/// [`accepts_enter`]: fn@accepts_enter
Enter,
/// The widget lost keyboard focus.
Blur,
}
/// Display info of edit caret position.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub struct CaretStatus {
index: usize,
line: u32,
column: u32,
}
impl CaretStatus {
/// Status for text without caret.
pub fn none() -> Self {
Self {
index: usize::MAX,
line: 0,
column: 0,
}
}
/// New position from char index and text.
///
/// # Panics
///
/// Panics if `index` is greater then `text` length.
pub fn new(index: usize, text: &SegmentedText) -> Self {
assert!(index <= text.text().len());
if text.text().is_empty() {
Self { line: 1, column: 1, index }
} else {
let mut line = 1;
let mut line_start = 0;
for seg in text.segs() {
if seg.end > index {
break;
}
if let TextSegmentKind::LineBreak = seg.kind {
line += 1;
line_start = seg.end;
}
}
let column = text.text()[line_start..index].chars().count() + 1;
Self {
line,
column: column as _,
index,
}
}
}
/// Char index on the text string, starts a 0, can be the length of the text.
pub fn index(&self) -> Option<usize> {
match self.index {
usize::MAX => None,
i => Some(i),
}
}
/// Display line, starts at 1.
///
/// Note that this does not count soft line breaks (wrapped lines), this is the actual text line.
pub fn line(&self) -> Option<NonZeroU32> {
NonZeroU32::new(self.line)
}
/// Display column, starts at 1.
///
/// This is the char count from the start of the text line to the index.
pub fn column(&self) -> Option<NonZeroU32> {
NonZeroU32::new(self.column)
}
}
impl fmt::Display for CaretStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.index().is_some() {
write!(f, "Ln {}, Col {}", self.line, self.column)
} else {
Ok(())
}
}
}
/// Represents the number of lines and number of wrap lines in a text.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinesWrapCount {
/// No line wrap.
///
/// The associated value is the number of lines.
NoWrap(usize),
/// Some text lines have more than one wrap line.
///
/// The associated value is a vec of wrap-line count for each text line, is `1` for lines that don't wrap.
Wrap(Vec<u32>),
}
impl LinesWrapCount {
/// Gets the number of text lines.
pub fn lines_len(&self) -> usize {
match self {
Self::NoWrap(l) => *l,
Self::Wrap(lns) => lns.len(),
}
}
}
/// Text paragraph properties.
///
/// Note that the [`Text!`] widget does not include this mix-in, as raw text does not encode
/// paragraph breaks, other rich text widgets can include it to configure paragraphs.
///
/// [`Text!`]: struct@crate::Text
#[widget_mixin]
pub struct ParagraphMix<P>(P);
context_var! {
/// Extra paragraph spacing of text blocks.
pub static PARAGRAPH_SPACING_VAR: ParagraphSpacing = 1.em();
}
impl ParagraphMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&PARAGRAPH_SPACING_VAR);
}
}
/// Extra spacing in-between paragraphs.
///
/// The default value is `1.em()`. Note that the [`Text!`] widget does not implement this property, as raw text does not encode
/// paragraph breaks, this property and context var exists to configure *rich-text* widgets, like the `Markdown!` widget.
///
/// Sets the [`PARAGRAPH_SPACING_VAR`].
///
/// [`Text!`]: struct@crate::Text
#[property(CONTEXT, default(PARAGRAPH_SPACING_VAR), widget_impl(ParagraphMix<P>))]
pub fn paragraph_spacing(child: impl UiNode, extra: impl IntoVar<ParagraphSpacing>) -> impl UiNode {
with_context_var(child, PARAGRAPH_SPACING_VAR, extra)
}
/// Selection toolbar properties.
#[widget_mixin]
pub struct SelectionToolbarMix<P>(P);
context_var! {
/// Selection toolbar function.
pub static SELECTION_TOOLBAR_FN_VAR: WidgetFn<SelectionToolbarArgs> = WidgetFn::nil();
/// Position the selection toolbar in relation to the bounding box of all selection rectangles.
pub static SELECTION_TOOLBAR_ANCHOR_VAR: AnchorOffset = AnchorOffset::out_top();
}
impl SelectionToolbarMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
set.insert(&SELECTION_TOOLBAR_FN_VAR);
set.insert(&SELECTION_TOOLBAR_ANCHOR_VAR);
}
}
/// Defines the floating mini-toolbar that shows near a new text selection.
///
/// The `toolbar` is used
#[property(CONTEXT, widget_impl(SelectionToolbarMix<P>))]
pub fn selection_toolbar(child: impl UiNode, toolbar: impl UiNode) -> impl UiNode {
selection_toolbar_fn(child, WidgetFn::singleton(toolbar))
}
/// Defines the floating mini-toolbar that shows near a new text selection.
///
/// Sets the [`SELECTION_TOOLBAR_FN_VAR`].
#[property(CONTEXT, default(SELECTION_TOOLBAR_FN_VAR), widget_impl(SelectionToolbarMix<P>))]
pub fn selection_toolbar_fn(child: impl UiNode, toolbar: impl IntoVar<WidgetFn<SelectionToolbarArgs>>) -> impl UiNode {
with_context_var(child, SELECTION_TOOLBAR_FN_VAR, toolbar)
}
/// Arguments for [`selection_toolbar_fn`].
///
/// [`selection_toolbar_fn`]: fn@selection_toolbar_fn
pub struct SelectionToolbarArgs {
/// ID of the widget the toolbar is anchored to.
pub anchor_id: WidgetId,
/// Text was selected through touch interaction.
pub is_touch: bool,
}
/// Position the selection toolbar in relation to the bounding box of all selection rectangles.
///
/// See [`selection_toolbar_fn`](fn@selection_toolbar_fn).
///
/// Sets the [`SELECTION_TOOLBAR_ANCHOR_VAR`].
#[property(CONTEXT, default(SELECTION_TOOLBAR_ANCHOR_VAR), widget_impl(SelectionToolbarMix<P>))]
pub fn selection_toolbar_anchor(child: impl UiNode, offset: impl IntoVar<AnchorOffset>) -> impl UiNode {
with_context_var(child, SELECTION_TOOLBAR_ANCHOR_VAR, offset)
}
/// Properties that probes various state from the text widget.
#[widget_mixin]
pub struct TextInspectMix<P>(P);
impl TextInspectMix<()> {
/// Insert context variables used by properties in this mix-in.
pub fn context_vars_set(set: &mut ContextValueSet) {
let _ = set;
}
}
/// Gets the caret char index, if the text is editable.
#[property(EVENT, default(None), widget_impl(TextInspectMix<P>))]
pub fn get_caret_index(child: impl UiNode, index: impl IntoVar<Option<CaretIndex>>) -> impl UiNode {
super::node::get_caret_index(child, index)
}
/// Gets the caret display status, if the text is editable.
#[property(EVENT, default(CaretStatus::none()), widget_impl(TextInspectMix<P>))]
pub fn get_caret_status(child: impl UiNode, status: impl IntoVar<CaretStatus>) -> impl UiNode {
super::node::get_caret_status(child, status)
}
/// Gets the number of lines in the text, including wrap lines.
///
/// This is very cheap, the text widget already has the length, but it does include wrapped lines. You
/// can use [`get_lines_wrap_count`] to get text lines and a count of wrapped lines for each.
///
/// [`get_lines_wrap_count`]: fn@get_lines_wrap_count
#[property(CHILD_LAYOUT+100, default(0), widget_impl(TextInspectMix<P>))]
pub fn get_lines_len(child: impl UiNode, len: impl IntoVar<usize>) -> impl UiNode {
super::node::get_lines_len(child, len)
}
/// Gets the number of wrap lines per text lines.
#[property(CHILD_LAYOUT+100, default(LinesWrapCount::NoWrap(0)), widget_impl(TextInspectMix<P>))]
pub fn get_lines_wrap_count(child: impl UiNode, lines: impl IntoVar<LinesWrapCount>) -> impl UiNode {
super::node::get_lines_wrap_count(child, lines)
}
/// Gets the number of character in the text.
#[property(EVENT, default(0), widget_impl(TextInspectMix<P>))]
pub fn get_chars_count(child: impl UiNode, chars: impl IntoVar<usize>) -> impl UiNode {
let chars = chars.into_var();
match_node(child, move |_, op| {
if let UiNodeOp::Init = op {
let ctx = super::node::TEXT.resolved();
let _ = chars.set_from_map(&ctx.txt, |t| t.chars().count());
let handle = ctx.txt.bind_map(&chars, |t| t.chars().count());
WIDGET.push_var_handle(handle);
}
})
}
/// Highlight a text range.
///
/// This property must be set in the text widget.
#[property(CHILD_LAYOUT+100, widget_impl(TextInspectMix<P>))]
pub fn txt_highlight(child: impl UiNode, range: impl IntoVar<std::ops::Range<CaretIndex>>, color: impl IntoVar<Rgba>) -> impl UiNode {
let range = range.into_var();
let color = color.into_var();
let color_key = FrameValueKey::new_unique();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&range).sub_var_render_update(&color);
}
UiNodeOp::Render { frame } => {
let l_txt = super::node::TEXT.laidout();
let r_txt = super::node::TEXT.resolved();
let r_txt = r_txt.segmented_text.text();
for line_rect in l_txt.shaped_text.highlight_rects(range.get(), r_txt) {
frame.push_color(line_rect, color_key.bind_var(&color, |c| *c));
}
}
UiNodeOp::RenderUpdate { update } => {
if let Some(color_update) = color_key.update_var(&color, |c| *c) {
update.update_color(color_update)
}
}
_ => {}
})
}
/// Gets a vector of font and ranges.
///
/// This property must be set in the text widget.
#[property(CHILD_LAYOUT+100, widget_impl(TextInspectMix<P>))]
pub fn get_font_use(child: impl UiNode, font_use: impl IntoVar<Vec<(Font, std::ops::Range<usize>)>>) -> impl UiNode {
let font_use = font_use.into_var();
let mut shaped_text_version = u32::MAX;
match_node(child, move |c, op| {
if let UiNodeOp::Layout { wl, final_size } = op {
*final_size = c.layout(wl);
let ctx = crate::node::TEXT.laidout();
if ctx.shaped_text_version != shaped_text_version && font_use.capabilities().can_modify() {
shaped_text_version = ctx.shaped_text_version;
let mut r = vec![];
for seg in ctx.shaped_text.lines().flat_map(|l| l.segs()) {
let mut seg_glyph_i = 0;
let seg_range = seg.text_range();
for (font, glyphs) in seg.glyphs() {
if r.is_empty() {
r.push((font.clone(), 0..seg_range.end));
} else {
let last_i = r.len() - 1;
if &r[last_i].0 != font {
let seg_char_i = seg.clusters()[seg_glyph_i] as usize;
let char_i = seg_range.start + seg_char_i;
r[last_i].1.end = char_i;
r.push((font.clone(), char_i..seg_range.end));
}
}
seg_glyph_i += glyphs.len();
}
}
let _ = font_use.set(r);
}
}
})
}