Bike
Bike 2 (Preview)
Bike 2 (Preview)
  • Bike 2 (Preview)
  • Using Bike 2
    • Outline Editing
    • Using Selection
    • Using Extensions
    • Using Outline Paths
    • Using Outline Filtering
  • Customizing Bike
    • Creating Extensions
    • Extensions Tutorial
      • App Context Tutorial
      • DOM Context Tutorial
      • Style Context Tutorial
  • Known Bugs
Powered by GitBook
On this page
  • Tutorial
  • Empty Outline Style
  • Visual Structure
  • Selection
  • Inline Formatting
  • More on Decorations
  • Editor
  • Next Steps
  • Add Rules
  • Import Rules
  • Copy / Modify
  1. Customizing Bike
  2. Extensions Tutorial

Style Context Tutorial

Last updated 1 day ago

Use the style context to create or modify Bike outline styles.

Summary

  • .

  • Entry point style/main.ts

  • Use to define custom stylesheets for Bike’s outline editor.

  • Import bike/style context API using import { SYMBOL } from 'bike/style'.

Tutorial

Note: Finished tutorial can be found in .

Each outline style is ordered list of style rules.

Each rule is composed of an and a callback function.

The following steps are followed to compute a style:

  1. Starts with a default style object

  2. Constructs an ordered list of rules matching the element

  3. Passes the style object into the callback function of each matching rule

  4. Through this process the default style object is transformed into a specific style

Notes

The order that you define your rules is important. Generally you want generic style rules listed first and refinements listed later.

The rule callbacks must be pure functions. Given the same editor state and style input values they must always generate the same end style state. They should only read values from the editor and style parameters when deciding what style values to set.

Styles are not inherited from parents as they are in CSS. Instead you should create a generic rule that matches all elements and sets defaults there. Put this generic rule at the start of your theme and then follow it with more specific rules.

Empty Outline Style

Let's create a new completely empty outline style. Then we'll add in the essential rules that will make your outline look like an outline.

First, define a new outline style in style/main.ts:

import { defineOutlineStyle } from 'bike/style'

let style = defineOutlineStyle('tutorial', 'Tutorial')
  1. Build and install your extension

  2. Select the style in Bike > Window > Style Sheets > Tutorial

Things to notice...

Your outline has no indentation or formatting. Those things are all defined by outline styles, and your style has no rules.

Outline editing behavior is still there. You can still expand/collapse lines. You can still insert text. You can still select and edit text, though it will be difficult because you won't see selection marks.

Visual Structure

Modify style/main.ts to add back visual structure:

import { defineOutlineStyle, Insets } from 'bike/style'

let style = defineOutlineStyle('tutorial', 'Tutorial')

style.layer('base', (row, run, caret, viewport, include) => {
  row(`.*`, (editor, row) => {
    row.padding = new Insets(10, 10, 10, 28)
  })
})

When adding rules we always add them to a layer. This helps to organize them, and makes it possible to modify existing styles by adding new rules to a specific layer.

Once you have rebuilt and installed your extension you should see outline structure.

This rule that you've added matches all rows and adds padding. Parent rows contain their child rows, so the extra padding added to the left edge indents those children.

To see the structure more clearly replace that rule with this rule:

import { defineOutlineStyle, Insets, Color } from 'bike/style'

let style = defineOutlineStyle('tutorial', 'Tutorial')

style.layer('base', (row, run, caret, viewport, include) => {
  row(`.*`, (editor, row) => {
    row.padding = new Insets(10, 10, 10, 28)

    row.decoration('background', (background, layout) => {
      background.border.width = 1
      background.border.color = Color.systemBlue()
    })

    row.text.padding = new Insets(5, 5, 5, 5)
    row.text.decoration('background', (background, layout) => {
      background.border.width = 1
      background.border.color = Color.systemGreen()
    })
  })
})

The visual structure of your outline is now be apparent. Rows (blue border) contain text (green border) and potentially other child rows.

The blue and green rectangles are created by attaching decorations to the row and to the row's text. Decorations can also be attached to runs of the row's text. Decorations are attached to the underlying text layout, but they don't effect it. If you need to make space for a decoration add padding to the element you are decorating.

Selection

Let's make it so that you can see what text is selected. To do that we'll create a new kind of rule that matches runs of text. We'll also add this rule to the "selection" layer for organization.

Try adding this rule, and then select some text in Bike:

style.layer('selection', (row, run, caret, viewport, include) => {
  run(`.@view-selected-range`, (editor, run) => {
    run.backgroundColor = Color.textBackgroundSelected()
  })
})

You should see text selections now when you select within a single paragraph. They will disappear when you select multiple paragraphs, but we'll fix that eventually.

A potential question, how would you even know about that @view-selected-range attribute? This is where the outline path explorer is useful. Select Window > Outline Path Explorer. Then make sure that "Show View Attributes" is selected. Now make some selections.

You should see the @view-selected-range attribute show up in the outline path explorer when you select a range of text. You can also type .@view-selected-range into the outline path explorers search field, and then the selected range of text will be highlighted green.

You can use the outline path explorer to find attributes that you can use when styling, and you can also see which elements will be selected by your outline paths when the editor is in various states.

Improving the selection

In our current selection rule we are setting the runs background color directly. This is setting core text's background color attribute. This works, but it's more flexible to use decorations.

Replace the selection layer with a decoration based approach like this:

style.layer('selection', (row, run, caret, viewport, include) => {
  run(`.@view-selected-range`, (editor, run) => {
    run.decoration('selection', (selection, layout) => {
      selection.zPosition = 1
      selection.color = Color.textBackgroundSelected().withAlpha(0.5)
    })
  })
})

This is similar to how we used decorations to draw boxes around rows and row's text. One difference is that in this case we had to set the zPosition property. We want to make sure that the text selection draws behind the run's text. An alternative design would be to draw in front of the row's text, but make the selection color partially transparent.

In Bike there are two selection modes–text selection mode and block selection mode. So far we are only showing text selections. When you extend the selection beyond a single paragraph Bike starts using block selection mode.

Lets show block selections by replacing the selection layer with:

style.layer('selection', (row, run, caret, viewport, include) => {
  run(`.@view-selected-range`, (editor, run) => {
    run.decoration('selection', (selection, layout) => {
      selection.zPosition = 1
      selection.color = Color.textBackgroundSelected().withAlpha(0.5)
    })
  })
  
  row(`.selection() = block`, (editor, row) => {
    row.text.color = Color.white()
    row.text.decoration('background', (background, layout) => {
      background.color = Color.selectedContentBackground()
      background.border.color = Color.systemRed()
    })
  })
})

A few interesting things are happening here.

  1. The matching outline path is calling the selection function with a block parameter value. This function will return true if the current node has block selection.

  2. For this example I am reusing the "background" decoration and changing its style. For example if I give the background rounded corners in my first rule, then the block selection will also have rounded corners. Alternativly I could have created a new decoration with a new ID to indicate block selection.

Inline Formatting

Inline formatting, such as bold and italic, is applied at the text run level.

Add support for bold and italic text:

style.layer(`run-formatting`, (row, run, caret, viewport, include) => {
  run('.@emphasized', (editor, text) => {
    text.font = text.font.withItalics()
  })
  
  run(`.@strong`, (editor, text) => {
    text.font = text.font.withBold()
  })
})

While these rules are simple you can add decorations to text runs just like you can add them to rows and row's text. Try creating a rule to show text with the @highlight attribute. You can add/remove that attribute from text by using Format > Highlight.

More on Decorations

So far we've used decorations as simple backgrounds, but they can do more. They can be placed and sized. They can contain images, symbols, and text. Let's use decorations to style a task item with a checkbox. Start by adding this rule:

style.layer('row-formatting', (row, run, caret, viewport, include) => {
  row(`.@type = task`, (editor, row) => {
    row.text.decoration('mark', (mark, layout) => {
      mark.color = Color.systemRed()
    })
  })
})

With that rule in place when you create a task in your outline it should show up with a red background.

Decorations are more then just colors, they can also show images using the contents property. Update the row formatting layer to show a checkbox image:

style.layer('row-formatting', (row, run, caret, viewport, include) => {
  row(`.@type = task`, (editor, row) => {
    row.text.decoration('mark', (mark, layout) => {
      mark.contents.gravity = 'center'
      mark.contents.image = Image.fromSymbol(
        new SymbolConfiguration('square')
          .withHierarchicalColor(Color.text())
          .withFont(Font.systemBody())
      )
    })
  })
})

We are creating a SF Symbol configuration. Then we use that configuration to create an image. Finally we assign that image to the decorations contents. When you save this new rule you should see an empty checkbox centered over all task rows.

Next we need to position the checkbox in a more reasonable location:

style.layer('row-formatting', (row, run, caret, viewport, include) => {
  row(`.@type = task`, (editor, row) => {
    row.text.decoration('mark', (mark, layout) => {
      let lineHeight = layout.firstLine.height
      mark.x = layout.leading.offset(-28 / 2)
      mark.y = layout.firstLine.centerY
      mark.width = lineHeight
      mark.height = lineHeight
      mark.contents.gravity = 'center'
      mark.contents.image = Image.fromSymbol(
        new SymbolConfiguration('square')
          .withHierarchicalColor(Color.text())
          .withFont(Font.systemBody())
      )
    })
  })
})

Decorations have x, y, width, and height properties of type LayoutValue. You get layout values from the passed in layout parameter. These are logical values that are resolved later in the layout process to position the decoration.

The checkbox is looking good!

Next we need to make it so that the box is checked when the tasks is done. When a task is marked done that is indicated by adding a @done attribute to the task's row.

Here's the final styling, supporting both checked and unchecked tasks:

style.layer('row-formatting', (row, run, caret, viewport, include) => {
  row(`.@type = task`, (editor, row) => {
    row.text.decoration('mark', (mark, layout) => {
      let lineHeight = layout.firstLine.height
      mark.x = layout.leading.offset(-28 / 2)
      mark.y = layout.firstLine.centerY
      mark.width = lineHeight
      mark.height = lineHeight
      mark.contents.gravity = 'center'
      mark.contents.image = Image.fromSymbol(
        new SymbolConfiguration('square')
          .withHierarchicalColor(Color.text())
          .withFont(Font.systemBody())
      )
    })
  })

  row(`.@type = task and @done`, (editor, row) => {
    row.text.decoration('mark', (mark, layout) => {
      mark.contents.image = Image.fromSymbol(
        new SymbolConfiguration('checkmark.square')
          .withHierarchicalColor(Color.text())
          .withFont(Font.systemBody())
      )
    })
  })
})

You can test the task style by selecting a task, press the escape key to enter block selection mode, and then type m d to toggle the done status of the task. This is made possible by a keybinding defined in the !bike.bkext extension.

The task checkbox should toggle!

One more thing.

row.text.strikethrough.thick = true

Add that line to the @done state rule and done tasks will get strikethrough text.

Editor

Each rule callback function takes two parameters–editor and style object. So far we've just been modifying the style object, but we can also read from the editor object to get editor settings, user theme settings, and more.

Your outline style should try to include the editor settings where it makes sense.

For example you might add these lines to the first match all .* rule:

row.text.font = editor.theme.font
row.text.lineHeightMultiple = editor.theme.lineHeightMultiple

Now when you View > Text Size > Zoom In/Out the outline text size will change in your theme. In addition to user settings the editor also includes system state such as isKey (does the view have keyboard focus) or isTyping (true when user is typing, false once they move mouse).

Next Steps

I hope the tutorial has helped you understand how styles work. Creating a full style that supports all of Bike's features is a lot of work. You can see Bike's standard style in src/!bike.bkext/style/bike-style.ts.

Here are some ways to approach outline styles:

Add Rules

It may be that you don't need to create a whole new style, maybe you just want to add a few rules to existing style(s). You can do this using the defineOutlineStyleModifier API. This allows you to insert rules into existing outline styles.

Import Rules

It maybe be that you do want to create a whole new style, but you want to import rules into that style from other outline styles. For example maybe you want to include the "run-formatting" rules from the standard style.

You can include rules from other styles like this:

let style = defineOutlineStyle('tutorial', 'Tutorial')
style.layer('row-formatting', (row, run, caret, viewport, include) => {
  include('bike', 'run-formatting')
})

Copy / Modify

Last, you can copy an existing style. Rename it, and make modifications.

Style Context API
src/tutorial.bkext
outline path