1#' Add low-level theming customizations
2#'
3#' Compared to higher-level theme customization available in [bs_theme()], these functions
4#' are a more direct interface to Bootstrap Sass, and therefore, do nothing to
5#' ensure theme customizations are portable between major Bootstrap versions.
6#'
7#' @inheritParams bs_theme_update
8#' @param ...
9#'  * `bs_add_variables()`: Should be named Sass variables or values that can be passed in directly to the `defaults` argument of a [sass::sass_layer()].
10#'  * `bs_bundle()`: Should be arguments that can be handled by [sass::sass_bundle()] to be appended to the `theme`
11#' @param .where Whether to place the variable definitions before other Sass
12#'   `"defaults"`, after other Sass `"declarations"`, or after other Sass
13#'   `"rules"`.
14#' @param .default_flag Whether or not to add a `!default` flag (if missing) to
15#'   variable expressions. It's recommended to keep this as `TRUE` when `.where
16#'   = "defaults"`.
17#'
18#' @return a modified [bs_theme()] object.
19#'
20#' @references \url{https://getbootstrap.com/docs/4.4/getting-started/theming/}
21#' @references \url{https://rstudio.github.io/sass/articles/sass.html#layering}
22#' @examples
23#'
24#' # Function to preview the styling a (primary) Bootstrap button
25#' library(htmltools)
26#' button <- tags$a(class = "btn btn-primary", href = "#", role = "button", "Hello")
27#' preview_button <- function(theme) {
28#'   if (interactive()) {
29#'     browsable(tags$body(bs_theme_dependencies(theme), button))
30#'   }
31#' }
32#'
33#' # Here we start with a theme based on a Bootswatch theme,
34#' # then override some variable defaults
35#' theme <- bs_add_variables(
36#'   bs_theme(bootswatch = "sketchy", primary = "orange"),
37#'   "body-bg" = "#EEEEEE",
38#'   "font-family-base" = "monospace",
39#'   "font-size-base" = "1.4rem",
40#'   "btn-padding-y" = ".16rem",
41#'   "btn-padding-x" = "2rem"
42#' )
43#'
44#' preview_button(theme)
45#'
46#' # If you need to set a variable based on another Bootstrap variable
47#' theme <- bs_add_variables(theme, "body-color" = "$success", .where = "declarations")
48#' preview_button(theme)
49#'
50#' # Start a new global theme and add some custom rules that
51#' # use Bootstrap variables to define a custom styling for a
52#' # 'person card'
53#' person_rules <- system.file("custom", "person.scss", package = "bslib")
54#' theme <- bs_add_rules(bs_theme(), sass::sass_file(person_rules))
55#' # Include custom CSS that leverages bootstrap Sass variables
56#' person <- function(name, title, company) {
57#'   tags$div(
58#'     class = "person",
59#'     h3(class = "name", name),
60#'     div(class = "title", title),
61#'     div(class = "company", company)
62#'   )
63#' }
64#' if (interactive()) {
65#'   browsable(shiny::fluidPage(
66#'     theme = theme,
67#'     person("Andrew Carnegie", "Owner", "Carnegie Steel Company"),
68#'     person("John D. Rockefeller", "Chairman", "Standard Oil")
69#'   ))
70#' }
71#'
72#' @export
73#' @describeIn bs_bundle Add Bootstrap Sass [variable defaults](https://getbootstrap.com/docs/4.4/getting-started/theming/#variable-defaults)
74bs_add_variables <- function(theme, ..., .where = "defaults", .default_flag = identical(.where, "defaults")) {
75  assert_bs_theme(theme)
76
77  vars <- rlang::list2(...)
78  if (any(names2(vars) == "")) stop("Variables must be named.", call. = FALSE)
79
80  # Workaround to the problem of 'blue' winning in the scenario of:
81  # bs_add_variables("body-bg" = "blue")
82  # bs_add_variables("body-bg" = "red")
83  if (.default_flag) {
84    vars <- ensure_default_flag(vars)
85  }
86
87  bs_bundle(
88    theme, do.call(sass_layer, rlang::list2(!!.where := vars))
89  )
90}
91
92# Given a named list of variable definitions,
93# searches each variable's expression for a !default flag,
94# and if missing, adds it.
95ensure_default_flag <- function(x) {
96  Map(
97    x, rlang::names2(x),
98    f = function(val, nm) {
99      # sass::font_collection() has it's own default_flag, so warn if they conflict
100      if (sass::is_font_collection(val)) {
101        if (identical(val$default_flag, FALSE)) {
102          message(
103            "Ignoring `bs_add_variables()`'s `.default_flag = TRUE` for ",
104            "the ", nm, " variable (since it has it's own `default_flag`)."
105          )
106        }
107        return(val)
108      }
109      val <- paste(as_sass(val), collapse = "\n")
110      if (grepl("!default\\s*;*\\s*$", val)) {
111        val
112      } else {
113        paste(sub(";+$", "", val), "!default")
114      }
115    }
116  )
117}
118
119#' @describeIn bs_bundle Add additional [Sass rules](https://sass-lang.com/documentation/style-rules)
120#' @param rules Sass rules. Anything understood by [sass::as_sass()] may be
121#'   provided (e.g., a list, character vector, [sass::sass_file()], etc)
122#' @export
123bs_add_rules <- function(theme, rules) {
124  bs_bundle(theme, sass_layer(rules = rules))
125}
126
127#' @describeIn bs_bundle Add additional [Sass
128#'   functions](https://rstudio.github.io/sass/articles/sass.html#functions-1)
129#' @param functions A character vector or [sass::sass_file()] containing
130#'   functions definitions.
131#' @export
132bs_add_functions <- function(theme, functions) {
133  bs_bundle(theme, sass_layer(functions = functions))
134}
135
136#' @describeIn bs_bundle Add additional [Sass
137#'   mixins](https://rstudio.github.io/sass/articles/sass.html#mixins-1)
138#' @param mixins A character vector or [sass::sass_file()] containing
139#'   mixin definitions.
140#' @export
141bs_add_mixins <- function(theme, mixins) {
142  bs_bundle(theme, sass_layer(mixins = mixins))
143}
144
145#' @describeIn bs_bundle Add additional [sass::sass_bundle()] objects to an existing `theme`.
146#' @export
147bs_bundle <- function(theme, ...) {
148  assert_bs_theme(theme)
149  structure(
150    sass_bundle(theme, ...),
151    class = class(theme)
152  )
153}
154