1
2#' Create a page with fluid layout
3#'
4#' Functions for creating fluid page layouts. A fluid page layout consists of
5#' rows which in turn include columns. Rows exist for the purpose of making sure
6#' their elements appear on the same line (if the browser has adequate width).
7#' Columns exist for the purpose of defining how much horizontal space within a
8#' 12-unit wide grid it's elements should occupy. Fluid pages scale their
9#' components in realtime to fill all available browser width.
10#'
11#' @param ... Elements to include within the page
12#' @param title The browser window title (defaults to the host URL of the page).
13#'   Can also be set as a side effect of the [titlePanel()] function.
14#' @inheritParams bootstrapPage
15#'
16#' @return A UI defintion that can be passed to the [shinyUI] function.
17#'
18#' @details To create a fluid page use the `fluidPage` function and include
19#'   instances of `fluidRow` and [column()] within it. As an
20#'   alternative to low-level row and column functions you can also use
21#'   higher-level layout functions like [sidebarLayout()].
22#'
23#' @note See the [
24#'   Shiny-Application-Layout-Guide](https://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fluid
25#'   pages.
26#'
27#' @family layout functions
28#' @seealso [column()]
29#'
30#' @examples
31#' ## Only run examples in interactive R sessions
32#' if (interactive()) {
33#'
34#' # Example of UI with fluidPage
35#' ui <- fluidPage(
36#'
37#'   # Application title
38#'   titlePanel("Hello Shiny!"),
39#'
40#'   sidebarLayout(
41#'
42#'     # Sidebar with a slider input
43#'     sidebarPanel(
44#'       sliderInput("obs",
45#'                   "Number of observations:",
46#'                   min = 0,
47#'                   max = 1000,
48#'                   value = 500)
49#'     ),
50#'
51#'     # Show a plot of the generated distribution
52#'     mainPanel(
53#'       plotOutput("distPlot")
54#'     )
55#'   )
56#' )
57#'
58#' # Server logic
59#' server <- function(input, output) {
60#'   output$distPlot <- renderPlot({
61#'     hist(rnorm(input$obs))
62#'   })
63#' }
64#'
65#' # Complete app with UI and server components
66#' shinyApp(ui, server)
67#'
68#'
69#' # UI demonstrating column layouts
70#' ui <- fluidPage(
71#'   title = "Hello Shiny!",
72#'   fluidRow(
73#'     column(width = 4,
74#'       "4"
75#'     ),
76#'     column(width = 3, offset = 2,
77#'       "3 offset 2"
78#'     )
79#'   )
80#' )
81#'
82#' shinyApp(ui, server = function(input, output) { })
83#' }
84#' @rdname fluidPage
85#' @export
86fluidPage <- function(..., title = NULL, theme = NULL, lang = NULL) {
87  bootstrapPage(div(class = "container-fluid", ...),
88                title = title,
89                theme = theme,
90                lang = lang)
91}
92
93
94#' @rdname fluidPage
95#' @export
96fluidRow <- function(...) {
97  div(class = "row", ...)
98}
99
100#' Create a page with a fixed layout
101#'
102#' Functions for creating fixed page layouts. A fixed page layout consists of
103#' rows which in turn include columns. Rows exist for the purpose of making sure
104#' their elements appear on the same line (if the browser has adequate width).
105#' Columns exist for the purpose of defining how much horizontal space within a
106#' 12-unit wide grid it's elements should occupy. Fixed pages limit their width
107#' to 940 pixels on a typical display, and 724px or 1170px on smaller and larger
108#' displays respectively.
109#'
110#' @param ... Elements to include within the container
111#' @param title The browser window title (defaults to the host URL of the page)
112#' @inheritParams bootstrapPage
113#'
114#' @return A UI defintion that can be passed to the [shinyUI] function.
115#'
116#' @details To create a fixed page use the `fixedPage` function and include
117#'   instances of `fixedRow` and [column()] within it. Note that
118#'   unlike [fluidPage()], fixed pages cannot make use of higher-level
119#'   layout functions like `sidebarLayout`, rather, all layout must be done
120#'   with `fixedRow` and `column`.
121#'
122#' @note See the [
123#'   Shiny Application Layout Guide](https://shiny.rstudio.com/articles/layout-guide.html) for additional details on laying out fixed
124#'   pages.
125#'
126#' @family layout functions
127#'
128#' @seealso [column()]
129#'
130#' @examples
131#' ## Only run examples in interactive R sessions
132#' if (interactive()) {
133#'
134#' ui <- fixedPage(
135#'   title = "Hello, Shiny!",
136#'   fixedRow(
137#'     column(width = 4,
138#'       "4"
139#'     ),
140#'     column(width = 3, offset = 2,
141#'       "3 offset 2"
142#'     )
143#'   )
144#' )
145#'
146#' shinyApp(ui, server = function(input, output) { })
147#' }
148#'
149#' @rdname fixedPage
150#' @export
151fixedPage <- function(..., title = NULL, theme = NULL, lang = NULL) {
152  bootstrapPage(div(class = "container", ...),
153                title = title,
154                theme = theme,
155                lang = lang)
156}
157
158#' @rdname fixedPage
159#' @export
160fixedRow <- function(...) {
161  div(class = "row", ...)
162}
163
164
165#' Create a column within a UI definition
166#'
167#' Create a column for use within a  [fluidRow()] or
168#' [fixedRow()]
169#'
170#' @param width The grid width of the column (must be between 1 and 12)
171#' @param ... Elements to include within the column
172#' @param offset The number of columns to offset this column from the end of the
173#'   previous column.
174#'
175#' @return A column that can be included within a
176#'   [fluidRow()] or [fixedRow()].
177#'
178#'
179#' @seealso [fluidRow()], [fixedRow()].
180#'
181#' @examples
182#' ## Only run examples in interactive R sessions
183#' if (interactive()) {
184#'
185#' ui <- fluidPage(
186#'   fluidRow(
187#'     column(4,
188#'       sliderInput("obs", "Number of observations:",
189#'                   min = 1, max = 1000, value = 500)
190#'     ),
191#'     column(8,
192#'       plotOutput("distPlot")
193#'     )
194#'   )
195#' )
196#'
197#' server <- function(input, output) {
198#'   output$distPlot <- renderPlot({
199#'     hist(rnorm(input$obs))
200#'   })
201#' }
202#'
203#' shinyApp(ui, server)
204#'
205#'
206#'
207#' ui <- fluidPage(
208#'   fluidRow(
209#'     column(width = 4,
210#'       "4"
211#'     ),
212#'     column(width = 3, offset = 2,
213#'       "3 offset 2"
214#'     )
215#'   )
216#' )
217#' shinyApp(ui, server = function(input, output) { })
218#' }
219#' @export
220column <- function(width, ..., offset = 0) {
221
222  if (!is.numeric(width) || (width < 1) || (width > 12))
223    stop("column width must be between 1 and 12")
224
225  colClass <- paste0("col-sm-", width)
226  if (offset > 0) {
227    # offset-md-x is for bootstrap 4 forward compat
228    # (every size tier has been bumped up one level)
229    # https://github.com/twbs/bootstrap/blob/74b8fe7/docs/4.3/migration/index.html#L659
230    colClass <- paste0(colClass, " offset-md-", offset, " col-sm-offset-", offset)
231  }
232  div(class = colClass, ...)
233}
234
235
236#' Create a panel containing an application title.
237#'
238#' @param title An application title to display
239#' @param windowTitle The title that should be displayed by the browser window.
240#'
241#' @details Calling this function has the side effect of including a
242#'   `title` tag within the head. You can also specify a page title
243#'   explicitly using the `title` parameter of the top-level page function.
244#'
245#' @examples
246#' ## Only run examples in interactive R sessions
247#' if (interactive()) {
248#'
249#' ui <- fluidPage(
250#'   titlePanel("Hello Shiny!")
251#' )
252#' shinyApp(ui, server = function(input, output) { })
253#' }
254#' @export
255titlePanel <- function(title, windowTitle=title) {
256  tagList(
257    tags$head(tags$title(windowTitle)),
258    h2(title)
259  )
260}
261
262#' Layout a sidebar and main area
263#'
264#' Create a layout (`sidebarLayout()`) with a sidebar (`sidebarPanel()`) and
265#' main area (`mainPanel()`). The sidebar is displayed with a distinct
266#' background color and typically contains input controls. The main
267#' area occupies 2/3 of the horizontal width and typically contains outputs.
268#'
269#' @param sidebarPanel The `sidebarPanel()` containing input controls.
270#' @param mainPanel The `mainPanel()` containing outputs.
271#' @param position The position of the sidebar relative to the main area ("left"
272#'   or "right").
273#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
274#'   layout.
275#' @param width The width of the sidebar and main panel. By default, the
276#'   sidebar takes up 1/3 of the width, and the main panel 2/3. The total
277#'   width must be 12 or less.
278#' @param ... Output elements to include in the sidebar/main panel.
279#'
280#' @family layout functions
281#'
282#' @examples
283#' ## Only run examples in interactive R sessions
284#' if (interactive()) {
285#' options(device.ask.default = FALSE)
286#'
287#' # Define UI
288#' ui <- fluidPage(
289#'
290#'   # Application title
291#'   titlePanel("Hello Shiny!"),
292#'
293#'   sidebarLayout(
294#'
295#'     # Sidebar with a slider input
296#'     sidebarPanel(
297#'       sliderInput("obs",
298#'                   "Number of observations:",
299#'                   min = 0,
300#'                   max = 1000,
301#'                   value = 500)
302#'     ),
303#'
304#'     # Show a plot of the generated distribution
305#'     mainPanel(
306#'       plotOutput("distPlot")
307#'     )
308#'   )
309#' )
310#'
311#' # Server logic
312#' server <- function(input, output) {
313#'   output$distPlot <- renderPlot({
314#'     hist(rnorm(input$obs))
315#'   })
316#' }
317#'
318#' # Complete app with UI and server components
319#' shinyApp(ui, server)
320#' }
321#' @export
322sidebarLayout <- function(sidebarPanel,
323                          mainPanel,
324                          position = c("left", "right"),
325                          fluid = TRUE) {
326
327  # determine the order
328  position <- match.arg(position)
329  if (position == "left") {
330    firstPanel <- sidebarPanel
331    secondPanel <- mainPanel
332  }
333  else if (position == "right") {
334    firstPanel <- mainPanel
335    secondPanel <- sidebarPanel
336  }
337
338  # return as as row
339  if (fluid)
340    fluidRow(firstPanel, secondPanel)
341  else
342    fixedRow(firstPanel, secondPanel)
343}
344
345#' @export
346#' @rdname sidebarLayout
347sidebarPanel <- function(..., width = 4) {
348  div(class=paste0("col-sm-", width),
349    tags$form(class="well",
350      # A11y semantic landmark for sidebar
351      role="complementary",
352      ...
353    )
354  )
355}
356
357#' @export
358#' @rdname sidebarLayout
359mainPanel <- function(..., width = 8) {
360  div(class=paste0("col-sm-", width),
361    # A11y semantic landmark for main region
362    role="main",
363    ...
364  )
365}
366
367#' Lay out UI elements vertically
368#'
369#' Create a container that includes one or more rows of content (each element
370#' passed to the container will appear on it's own line in the UI)
371#'
372#' @param ... Elements to include within the container
373#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
374#'   layout.
375#'
376#' @family layout functions
377#'
378#' @examples
379#' ## Only run examples in interactive R sessions
380#' if (interactive()) {
381#'
382#' ui <- fluidPage(
383#'   verticalLayout(
384#'     a(href="http://example.com/link1", "Link One"),
385#'     a(href="http://example.com/link2", "Link Two"),
386#'     a(href="http://example.com/link3", "Link Three")
387#'   )
388#' )
389#' shinyApp(ui, server = function(input, output) { })
390#' }
391#' @export
392verticalLayout <- function(..., fluid = TRUE) {
393  lapply(list2(...), function(row) {
394    col <- column(12, row)
395    if (fluid)
396      fluidRow(col)
397    else
398      fixedRow(col)
399  })
400}
401
402#' Flow layout
403#'
404#' Lays out elements in a left-to-right, top-to-bottom arrangement. The elements
405#' on a given row will be top-aligned with each other. This layout will not work
406#' well with elements that have a percentage-based width (e.g.
407#' [plotOutput()] at its default setting of `width = "100%"`).
408#'
409#' @param ... Unnamed arguments will become child elements of the layout. Named
410#'   arguments will become HTML attributes on the outermost tag.
411#' @param cellArgs Any additional attributes that should be used for each cell
412#'   of the layout.
413#'
414#' @family layout functions
415#'
416#' @examples
417#' ## Only run examples in interactive R sessions
418#' if (interactive()) {
419#'
420#' ui <- flowLayout(
421#'   numericInput("rows", "How many rows?", 5),
422#'   selectInput("letter", "Which letter?", LETTERS),
423#'   sliderInput("value", "What value?", 0, 100, 50)
424#' )
425#' shinyApp(ui, server = function(input, output) { })
426#' }
427#' @export
428flowLayout <- function(..., cellArgs = list()) {
429
430  children <- list2(...)
431  childIdx <- !nzchar(names(children) %||% character(length(children)))
432  attribs <- children[!childIdx]
433  children <- children[childIdx]
434
435  do.call(tags$div, c(list(class = "shiny-flow-layout"),
436    attribs,
437    lapply(children, function(x) {
438      do.call(tags$div, c(cellArgs, list(x)))
439    })
440  ))
441}
442
443#' Input panel
444#'
445#' A [flowLayout()] with a grey border and light grey background,
446#' suitable for wrapping inputs.
447#'
448#' @param ... Input controls or other HTML elements.
449#' @export
450inputPanel <- function(...) {
451  div(class = "shiny-input-panel",
452    flowLayout(...)
453  )
454}
455
456#' Split layout
457#'
458#' Lays out elements horizontally, dividing the available horizontal space into
459#' equal parts (by default).
460#'
461#' @param ... Unnamed arguments will become child elements of the layout. Named
462#'   arguments will become HTML attributes on the outermost tag.
463#' @param cellWidths Character or numeric vector indicating the widths of the
464#'   individual cells. Recycling will be used if needed. Character values will
465#'   be interpreted as CSS lengths (see [validateCssUnit()]), numeric
466#'   values as pixels.
467#' @param cellArgs Any additional attributes that should be used for each cell
468#'   of the layout.
469#'
470#' @family layout functions
471#'
472#' @examples
473#' ## Only run examples in interactive R sessions
474#' if (interactive()) {
475#' options(device.ask.default = FALSE)
476#'
477#' # Server code used for all examples
478#' server <- function(input, output) {
479#'   output$plot1 <- renderPlot(plot(cars))
480#'   output$plot2 <- renderPlot(plot(pressure))
481#'   output$plot3 <- renderPlot(plot(AirPassengers))
482#' }
483#'
484#' # Equal sizing
485#' ui <- splitLayout(
486#'   plotOutput("plot1"),
487#'   plotOutput("plot2")
488#' )
489#' shinyApp(ui, server)
490#'
491#' # Custom widths
492#' ui <- splitLayout(cellWidths = c("25%", "75%"),
493#'   plotOutput("plot1"),
494#'   plotOutput("plot2")
495#' )
496#' shinyApp(ui, server)
497#'
498#' # All cells at 300 pixels wide, with cell padding
499#' # and a border around everything
500#' ui <- splitLayout(
501#'   style = "border: 1px solid silver;",
502#'   cellWidths = 300,
503#'   cellArgs = list(style = "padding: 6px"),
504#'   plotOutput("plot1"),
505#'   plotOutput("plot2"),
506#'   plotOutput("plot3")
507#' )
508#' shinyApp(ui, server)
509#' }
510#' @export
511splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
512
513  children <- list2(...)
514  childIdx <- !nzchar(names(children) %||% character(length(children)))
515  attribs <- children[!childIdx]
516  children <- children[childIdx]
517  count <- length(children)
518
519  if (length(cellWidths) == 0 || is.na(cellWidths)) {
520    cellWidths <- sprintf("%.3f%%", 100 / count)
521  }
522  cellWidths <- rep(cellWidths, length.out = count)
523  cellWidths <- sapply(cellWidths, validateCssUnit)
524
525  do.call(tags$div, c(list(class = "shiny-split-layout"),
526    attribs,
527    mapply(children, cellWidths, FUN = function(x, w) {
528      do.call(tags$div, c(
529        list(style = sprintf("width: %s;", w)),
530        cellArgs,
531        list(x)
532      ))
533    }, SIMPLIFY = FALSE)
534  ))
535}
536
537#' Flex Box-based row/column layouts
538#'
539#' Creates row and column layouts with proportionally-sized cells, using the
540#' Flex Box layout model of CSS3. These can be nested to create arbitrary
541#' proportional-grid layouts. **Warning:** Flex Box is not well supported
542#' by Internet Explorer, so these functions should only be used where modern
543#' browsers can be assumed.
544#'
545#' @details If you try to use `fillRow` and `fillCol` inside of other
546#'   Shiny containers, such as [sidebarLayout()],
547#'   [navbarPage()], or even `tags$div`, you will probably find
548#'   that they will not appear. This is due to `fillRow` and `fillCol`
549#'   defaulting to `height="100%"`, which will only work inside of
550#'   containers that have determined their own size (rather than shrinking to
551#'   the size of their contents, as is usually the case in HTML).
552#'
553#'   To avoid this problem, you have two options:
554#'   \itemize{
555#'     \item only use `fillRow`/`fillCol` inside of `fillPage`,
556#'       `fillRow`, or `fillCol`
557#'     \item provide an explicit `height` argument to
558#'       `fillRow`/`fillCol`
559#'   }
560#'
561#' @param ... UI objects to put in each row/column cell; each argument will
562#'   occupy a single cell. (To put multiple items in a single cell, you can use
563#'   [tagList()] or [div()] to combine them.) Named
564#'   arguments will be used as attributes on the `div` element that
565#'   encapsulates the row/column.
566#' @param flex Determines how space should be distributed to the cells. Can be a
567#'   single value like `1` or `2` to evenly distribute the available
568#'   space; or use a vector of numbers to specify the proportions. For example,
569#'   `flex = c(2, 3)` would cause the space to be split 40%/60% between
570#'   two cells. NA values will cause the corresponding cell to be sized
571#'   according to its contents (without growing or shrinking).
572#' @param width,height The total amount of width and height to use for the
573#'   entire row/column. For the default height of `"100%"` to be
574#'   effective, the parent must be `fillPage`, another
575#'   `fillRow`/`fillCol`, or some other HTML element whose height is
576#'   not determined by the height of its contents.
577#'
578#' @examples
579#' # Only run this example in interactive R sessions.
580#' if (interactive()) {
581#'
582#' ui <- fillPage(fillRow(
583#'   plotOutput("plotLeft", height = "100%"),
584#'   fillCol(
585#'     plotOutput("plotTopRight", height = "100%"),
586#'     plotOutput("plotBottomRight", height = "100%")
587#'   )
588#' ))
589#'
590#' server <- function(input, output, session) {
591#'   output$plotLeft <- renderPlot(plot(cars))
592#'   output$plotTopRight <- renderPlot(plot(pressure))
593#'   output$plotBottomRight <- renderPlot(plot(AirPassengers))
594#' }
595#'
596#' shinyApp(ui, server)
597#'
598#' }
599#' @export
600fillRow <- function(..., flex = 1, width = "100%", height = "100%") {
601  flexfill(..., direction = "row", flex = flex, width = width, height = height)
602}
603
604#' @rdname fillRow
605#' @export
606fillCol <- function(..., flex = 1, width = "100%", height = "100%") {
607  flexfill(..., direction = "column", flex = flex, width = width, height = height)
608}
609
610flexfill <- function(..., direction, flex, width = width, height = height) {
611  children <- list2(...)
612  attrs <- list()
613
614  if (!is.null(names(children))) {
615    attrs <- children[names(children) != ""]
616    children <- children[names(children) == ""]
617  }
618
619  if (length(flex) > length(children)) {
620    flex <- flex[seq_along(children)]
621  }
622
623  # The dimension along the main axis
624  main <- switch(direction,
625    row = "width",
626    "row-reverse" = "width",
627    column = "height",
628    "column-reverse" = "height",
629    stop("Unexpected direction")
630  )
631  # The dimension along the cross axis
632  cross <- if (main == "width") "height" else "width"
633
634  divArgs <- list(
635    class = sprintf("flexfill-container flexfill-container-%s", direction),
636    style = css(
637      display = "-webkit-flex",
638      display = "-ms-flexbox",
639      display = "flex",
640      .webkit.flex.direction = direction,
641      .ms.flex.direction = direction,
642      flex.direction = direction,
643      width = validateCssUnit(width),
644      height = validateCssUnit(height)
645    ),
646    mapply(children, flex, FUN = function(el, flexValue) {
647      if (is.na(flexValue)) {
648        # If the flex value is NA, then put the element in a simple flex item
649        # that sizes itself (along the main axis) to its contents
650        tags$div(
651          class = "flexfill-item",
652          style = css(
653            position = "relative",
654            "-webkit-flex" = "none",
655            "-ms-flex" = "none",
656            flex = "none"
657          ),
658          style = paste0(main, ":auto;", cross, ":100%;"),
659          el
660        )
661      } else if (is.numeric(flexValue)) {
662        # If the flex value is numeric, we need *two* wrapper divs. The outer is
663        # the flex item, and the inner is an absolute-fill div that is needed to
664        # make percentage-based sizing for el work correctly. I don't understand
665        # why this is needed but the truth is probably in this SO page:
666        # http://stackoverflow.com/questions/15381172/css-flexbox-child-height-100
667        tags$div(
668          class = "flexfill-item",
669          style = css(
670            position = "relative",
671            "-webkit-flex" = flexValue,
672            "-ms-flex" = flexValue,
673            flex = flexValue,
674            width = "100%", height = "100%"
675          ),
676          tags$div(
677            class = "flexfill-item-inner",
678            style = css(
679              position = "absolute",
680              top = 0, left = 0, right = 0, bottom = 0
681            ),
682            el
683          )
684        )
685      } else {
686        stop("Unexpected flex argument: ", flexValue)
687      }
688    }, SIMPLIFY = FALSE, USE.NAMES = FALSE)
689  )
690  do.call(tags$div, c(attrs, divArgs))
691}
692