From 62553316a85202c375db618df10bfadd1762fc1d Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 5 Jun 2020 12:52:17 -0700 Subject: [PATCH] feat(components): add RichText component (#875) * Add RichText component * Use more efficient traversal * Fix traversal name * Clarify naming * Add DSL for RichText * Create DSL module * Include DSL module in RichText * Remove DSLType * Move smoothing to RichTextView * Remove unnecessary font change * Keep DSL in own module * Revert "Keep DSL in own module" This reverts commit 17e54452473fbbf3e8e42ea60ba0f5ff270692a3. Co-authored-by: Glenn Slotte --- examples/Examples.re | 5 + examples/RichTextExample.re | 40 +++++++ src/UI/Revery_UI.re | 1 + src/UI/RichText.re | 129 ++++++++++++++++++++++ src/UI/RichText.rei | 80 ++++++++++++++ src/UI_Components/Revery_UI_Components.re | 1 + src/UI_Components/RichTextView.re | 31 ++++++ 7 files changed, 287 insertions(+) create mode 100644 examples/RichTextExample.re create mode 100644 src/UI/RichText.re create mode 100644 src/UI/RichText.rei create mode 100644 src/UI_Components/RichTextView.re diff --git a/examples/Examples.re b/examples/Examples.re index c376e47ab..ec801ceb2 100644 --- a/examples/Examples.re +++ b/examples/Examples.re @@ -150,6 +150,11 @@ let examples = [ render: _ => FileDragAndDrop.render(), source: "FileDragAndDrop.re", }, + { + name: "Rich Text Example", + render: _ => RichTextExample.render(), + source: "RichTextExample.re", + }, { name: "Shell: Open URL", render: _ => URLFileOpen.render(), diff --git a/examples/RichTextExample.re b/examples/RichTextExample.re new file mode 100644 index 000000000..0e8cafd3e --- /dev/null +++ b/examples/RichTextExample.re @@ -0,0 +1,40 @@ +open Revery; +open Revery.UI; +open Revery.UI.Components; +open Revery.Font; + +let containerStyle = + Style.[ + position(`Absolute), + top(0), + bottom(0), + left(0), + right(0), + alignItems(`Center), + justifyContent(`Center), + flexDirection(`Column), + ]; + +module SampleRichText = { + let make = () => { + let richtext = + RichText.( + text("Hello ", ~color=Colors.red, ~fontWeight=Weight.Bold) + ++ text("world", ~color=Colors.green) + ++ text("!", ~color=Colors.yellow) + |> fontSize(20.) + |> italicized + ); + + let dimensions = RichText.measure(richtext); + let widthText = "Width: " ++ string_of_int(dimensions.width); + let heightText = "Height: " ++ string_of_int(dimensions.height); + + + + + + ; + }; +}; +let render = () => ; diff --git a/src/UI/Revery_UI.re b/src/UI/Revery_UI.re index 56fd343dd..29a478d1a 100644 --- a/src/UI/Revery_UI.re +++ b/src/UI/Revery_UI.re @@ -8,6 +8,7 @@ module LayoutTypes = Layout.LayoutTypes; module Style = Style; module Transform = Transform; module Selector = Selector; +module RichText = RichText; class node = class Node.node; class viewNode = class ViewNode.viewNode; diff --git a/src/UI/RichText.re b/src/UI/RichText.re new file mode 100644 index 000000000..072dc9af8 --- /dev/null +++ b/src/UI/RichText.re @@ -0,0 +1,129 @@ +open Revery_Core; +open Revery_Font; + +type textInfo = { + fontFamily: Family.t, + fontWeight: Weight.t, + italicized: bool, + monospaced: bool, + fontSize: float, + text: string, + color: Color.t, +}; +type t = + | Leaf(textInfo) + | Node(t, t); + +let (++) = (left: t, right: t) => Node(left, right); + +let rec foldRight = + (fn: ('acc, textInfo) => 'acc, accumulator: 'acc, richtext: t) => + switch (richtext) { + | Leaf(textInfo) => fn(accumulator, textInfo) + | Node(left, right) => + let rightAcc = foldRight(fn, accumulator, right); + let leftAcc = foldRight(fn, rightAcc, left); + leftAcc; + }; + +let rec map = (updateLeaf: textInfo => t, richtext: t) => + switch (richtext) { + | Leaf(textInfo) => updateLeaf(textInfo) + | Node(left, right) => + let newLeft = map(updateLeaf, left); + let newRight = map(updateLeaf, right); + Node(newLeft, newRight); + }; + +let measure = (~smoothing=Smoothing.default, richtext: t) => + foldRight( + (acc: Dimensions.t, textInfo) => { + let dimensions = + Revery_Draw.Text.measure( + ~smoothing, + ~fontFamily= + Family.toPath( + textInfo.fontFamily, + textInfo.fontWeight, + textInfo.italicized, + textInfo.monospaced, + ), + ~fontSize=textInfo.fontSize, + textInfo.text, + ); + let width = acc.width + int_of_float(dimensions.width); + let height = max(acc.height, int_of_float(dimensions.height)); + + Dimensions.create(~top=0, ~left=0, ~width, ~height, ()); + }, + Dimensions.create(~top=0, ~left=0, ~width=0, ~height=0, ()), + richtext, + ); + +module DSL = { + module Defaults = { + let fontFamily = Family.default; + let fontWeight = Weight.Normal; + let italicized = false; + let monospaced = false; + let fontSize = 14.; + let color = Colors.black; + }; + let text = + ( + ~fontFamily=Defaults.fontFamily, + ~fontWeight=Defaults.fontWeight, + ~italicized=Defaults.italicized, + ~monospaced=Defaults.monospaced, + ~fontSize=Defaults.fontSize, + ~color=Defaults.color, + text: string, + ) => + Leaf({ + fontFamily, + fontWeight, + italicized, + monospaced, + fontSize, + text, + color, + }); + + let fontWeight = (fontWeight: Weight.t, richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, fontWeight})); + let thin = (richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, fontWeight: Weight.Thin})); + let ultralight = (richtext: t) => + richtext + |> map(textInfo => Leaf({...textInfo, fontWeight: Weight.UltraLight})); + let light = (richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, fontWeight: Weight.Light})); + let normal = (richtext: t) => + richtext + |> map(textInfo => Leaf({...textInfo, fontWeight: Weight.Normal})); + let medium = (richtext: t) => + richtext + |> map(textInfo => Leaf({...textInfo, fontWeight: Weight.Medium})); + let semibold = (richtext: t) => + richtext + |> map(textInfo => Leaf({...textInfo, fontWeight: Weight.SemiBold})); + let bold = (richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, fontWeight: Weight.Bold})); + let ultrabold = (richtext: t) => + richtext + |> map(textInfo => Leaf({...textInfo, fontWeight: Weight.UltraBold})); + let heavy = (richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, fontWeight: Weight.Heavy})); + + let fontFamily = (fontFamily: Family.t, richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, fontFamily})); + let italicized = (richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, italicized: true})); + let monospaced = (richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, monospaced: true})); + let fontSize = (fontSize: float, richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, fontSize})); + let color = (color: Color.t, richtext: t) => + richtext |> map(textInfo => Leaf({...textInfo, color})); +}; +include DSL; diff --git a/src/UI/RichText.rei b/src/UI/RichText.rei new file mode 100644 index 000000000..b6a8d01c0 --- /dev/null +++ b/src/UI/RichText.rei @@ -0,0 +1,80 @@ +open Revery_Core; +open Revery_Font; + +type textInfo = { + fontFamily: Family.t, + fontWeight: Weight.t, + italicized: bool, + monospaced: bool, + fontSize: float, + text: string, + color: Color.t, +}; +type t = + | Leaf(textInfo) + | Node(t, t); + +let (++): (t, t) => t; + +let foldRight: (('acc, textInfo) => 'acc, 'acc, t) => 'acc; + +let map: (textInfo => t, t) => t; + +let measure: (~smoothing: Smoothing.t=?, t) => Dimensions.t; + +module DSL: { + let text: + ( + ~fontFamily: Family.t=?, + ~fontWeight: Weight.t=?, + ~italicized: bool=?, + ~monospaced: bool=?, + ~fontSize: float=?, + ~color: Color.t=?, + string + ) => + t; + let fontWeight: (Weight.t, t) => t; + let thin: t => t; + let ultralight: t => t; + let light: t => t; + let normal: t => t; + let medium: t => t; + let semibold: t => t; + let bold: t => t; + let ultrabold: t => t; + let heavy: t => t; + + let fontFamily: (Family.t, t) => t; + let italicized: t => t; + let monospaced: t => t; + let fontSize: (float, t) => t; + let color: (Color.t, t) => t; +}; +let text: + ( + ~fontFamily: Family.t=?, + ~fontWeight: Weight.t=?, + ~italicized: bool=?, + ~monospaced: bool=?, + ~fontSize: float=?, + ~color: Color.t=?, + string + ) => + t; +let fontWeight: (Weight.t, t) => t; +let thin: t => t; +let ultralight: t => t; +let light: t => t; +let normal: t => t; +let medium: t => t; +let semibold: t => t; +let bold: t => t; +let ultrabold: t => t; +let heavy: t => t; + +let fontFamily: (Family.t, t) => t; +let italicized: t => t; +let monospaced: t => t; +let fontSize: (float, t) => t; +let color: (Color.t, t) => t; diff --git a/src/UI_Components/Revery_UI_Components.re b/src/UI_Components/Revery_UI_Components.re index ec547ce55..34370e3a9 100644 --- a/src/UI_Components/Revery_UI_Components.re +++ b/src/UI_Components/Revery_UI_Components.re @@ -40,4 +40,5 @@ module RadioButtonsString = module ClickableText = ClickableText; module Ticker = Ticker; module Tree = Tree; +module RichTextView = RichTextView; module Markdown = Markdown; diff --git a/src/UI_Components/RichTextView.re b/src/UI_Components/RichTextView.re new file mode 100644 index 000000000..a963aa4ab --- /dev/null +++ b/src/UI_Components/RichTextView.re @@ -0,0 +1,31 @@ +open Revery_UI; +open Revery_UI_Primitives; +open Revery_Font; + +let make = + (~style=[], ~smoothing=Smoothing.default, ~richtext: RichText.t, ()) => { + let text = + RichText.foldRight( + (acc, textInfo) => + [ + , + ...acc, + ], + [], + richtext, + ); + + // TODO: Add alignItems(`Baseline) if that exists + + {text |> React.listToElement} + ; +};